From a42d1e01f67e64eac42c76dad4981f26f12786cc Mon Sep 17 00:00:00 2001 From: Jens Leisenberg Date: Sat, 1 Jun 2013 14:02:50 +0200 Subject: [PATCH] initial fork - added basic plugin functionality (for sensors only --> virtual sensor) - added a more basic interceptor functionality for devices - rewritten chart-module - send pushover notifications additionality to eMails - added cron-service for device-status Bugs/Open: - Translation - Documentation for Plugins and Upgrade process - plugin functionality for devices - 'scene'-functionality for charts - push from telldus? - refactor telldus to act like a plugin --- HTTP/OAuth.php | 200 + HTTP/OAuth/Consumer.php | 422 + .../Consumer/Exception/InvalidResponse.php | 91 + HTTP/OAuth/Consumer/Request.php | 359 + HTTP/OAuth/Consumer/Response.php | 112 + HTTP/OAuth/Exception.php | 61 + HTTP/OAuth/Exception/NotImplemented.php | 44 + HTTP/OAuth/Message.php | 257 + HTTP/OAuth/Provider/Exception.php | 40 + .../Provider/Exception/InvalidRequest.php | 41 + HTTP/OAuth/Provider/Request.php | 366 + HTTP/OAuth/Provider/Response.php | 287 + HTTP/OAuth/Signature.php | 72 + HTTP/OAuth/Signature/Common.php | 99 + HTTP/OAuth/Signature/HMAC/SHA1.php | 66 + HTTP/OAuth/Signature/PLAINTEXT.php | 61 + HTTP/OAuth/Signature/RSA/SHA1.php | 62 + HTTP/OAuth/Store/Consumer/CacheLite.php | 231 + HTTP/OAuth/Store/Consumer/Interface.php | 78 + HTTP/OAuth/Store/Data.php | 69 + HTTP/Request2.php | 2100 ++++ HTTP/Request2/Adapter.php | 314 + HTTP/Request2/Adapter/Curl.php | 584 + HTTP/Request2/Adapter/Mock.php | 189 + HTTP/Request2/Adapter/Socket.php | 2031 ++++ HTTP/Request2/CookieJar.php | 517 + HTTP/Request2/Exception.php | 365 + HTTP/Request2/MultipartBody.php | 280 + HTTP/Request2/Observer/Log.php | 429 + HTTP/Request2/Response.php | 1307 +++ HTTP/Request2/SOCKS5.php | 158 + HTTP/Request2/SocketWrapper.php | 566 + Net/SMTP.php | 1342 +++ Net/Socket.php | 653 ++ Net/URL2.php | 942 ++ PEAR/Autoloader.php | 218 + PEAR/Builder.php | 489 + PEAR/ChannelFile.php | 1559 +++ PEAR/ChannelFile/Parser.php | 68 + PEAR/Command.php | 414 + PEAR/Command/Auth.php | 81 + PEAR/Command/Auth.xml | 30 + PEAR/Command/Build.php | 85 + PEAR/Command/Build.xml | 10 + PEAR/Command/Channels.php | 883 ++ PEAR/Command/Channels.xml | 123 + PEAR/Command/Common.php | 273 + PEAR/Command/Config.php | 414 + PEAR/Command/Config.xml | 92 + PEAR/Command/Install.php | 1268 +++ PEAR/Command/Install.xml | 276 + PEAR/Command/Mirror.php | 139 + PEAR/Command/Mirror.xml | 18 + PEAR/Command/Package.php | 1124 ++ PEAR/Command/Package.xml | 237 + PEAR/Command/Pickle.php | 421 + PEAR/Command/Pickle.xml | 36 + PEAR/Command/Registry.php | 1145 ++ PEAR/Command/Registry.xml | 58 + PEAR/Command/Remote.php | 810 ++ PEAR/Command/Remote.xml | 109 + PEAR/Command/Test.php | 337 + PEAR/Command/Test.xml | 54 + PEAR/Common.php | 837 ++ PEAR/Config.php | 2097 ++++ PEAR/Dependency2.php | 1358 +++ PEAR/DependencyDB.php | 769 ++ PEAR/Downloader.php | 1766 +++ PEAR/Downloader/Package.php | 1988 ++++ PEAR/ErrorStack.php | 985 ++ PEAR/Exception.php | 389 + PEAR/FixPHP5PEARWarnings.php | 7 + PEAR/Frontend.php | 228 + PEAR/Frontend/CLI.php | 751 ++ PEAR/Installer.php | 1823 ++++ PEAR/Installer/Role.php | 276 + PEAR/Installer/Role/Cfg.php | 106 + PEAR/Installer/Role/Cfg.xml | 15 + PEAR/Installer/Role/Common.php | 174 + PEAR/Installer/Role/Data.php | 28 + PEAR/Installer/Role/Data.xml | 15 + PEAR/Installer/Role/Doc.php | 28 + PEAR/Installer/Role/Doc.xml | 15 + PEAR/Installer/Role/Ext.php | 28 + PEAR/Installer/Role/Ext.xml | 12 + PEAR/Installer/Role/Php.php | 28 + PEAR/Installer/Role/Php.xml | 15 + PEAR/Installer/Role/Script.php | 28 + PEAR/Installer/Role/Script.xml | 15 + PEAR/Installer/Role/Src.php | 34 + PEAR/Installer/Role/Src.xml | 12 + PEAR/Installer/Role/Test.php | 28 + PEAR/Installer/Role/Test.xml | 15 + PEAR/Installer/Role/Www.php | 28 + PEAR/Installer/Role/Www.xml | 15 + PEAR/PackageFile.php | 492 + PEAR/PackageFile/Generator/v1.php | 1284 +++ PEAR/PackageFile/Generator/v2.php | 893 ++ PEAR/PackageFile/Parser/v1.php | 459 + PEAR/PackageFile/Parser/v2.php | 113 + PEAR/PackageFile/v1.php | 1612 +++ PEAR/PackageFile/v2.php | 2049 ++++ PEAR/PackageFile/v2/Validator.php | 2154 ++++ PEAR/PackageFile/v2/rw.php | 1604 +++ PEAR/Packager.php | 201 + PEAR/REST.php | 483 + PEAR/REST/10.php | 871 ++ PEAR/REST/11.php | 341 + PEAR/REST/13.php | 299 + PEAR/Registry.php | 2395 +++++ PEAR/RunTest.php | 968 ++ PEAR/Task/Common.php | 202 + PEAR/Task/Postinstallscript.php | 323 + PEAR/Task/Postinstallscript/rw.php | 169 + PEAR/Task/Replace.php | 176 + PEAR/Task/Replace/rw.php | 61 + PEAR/Task/Unixeol.php | 77 + PEAR/Task/Unixeol/rw.php | 56 + PEAR/Task/Windowseol.php | 77 + PEAR/Task/Windowseol/rw.php | 56 + PEAR/Validate.php | 629 ++ PEAR/Validator/PECL.php | 63 + PEAR/XMLParser.php | 253 + ... on server with cPanel (oAuth and API).pdf | Bin 0 -> 960051 bytes ajax_light_control.php | 40 +- cron_device_log.php | 99 + cron_schedule.php | 21 +- cron_temp_log.php | 32 +- cron_virtual_sensor.php | 111 + dev/test_device_available.php | 46 + dev/test_nmap.php | 34 + dev/test_ssh2.php | 55 + dev/wartung_delete_double_devices_log.php | 61 + .../device_as_sensor/index.php | 86 + .../device_as_sensor/light_off.png | Bin 0 -> 505 bytes .../device_as_sensor/light_on.png | Bin 0 -> 623 bytes .../device_as_sensor/plugin_info.txt | 4 + .../virtutal_devices/network_scan/host.png | Bin 0 -> 831 bytes .../virtutal_devices/network_scan/index.php | 231 + .../network_scan/plugin_info.txt | 4 + .../owl_energy_monitor/fetchSonsorData.sh | 8 + .../owl_energy_monitor/icon.png | Bin 0 -> 376 bytes .../owl_energy_monitor/index.php | 160 + .../owl_energy_monitor/plugin_info.txt | 4 + fuTelldus.sql | 95 +- images/Thumbs.db | Bin 26112 -> 26112 bytes images/ajax-loader3.gif | Bin 0 -> 8238 bytes images/host.png | Bin 0 -> 831 bytes images/light.png | Bin 0 -> 505 bytes images/moon.png | Bin 0 -> 704 bytes images/shared.png | Bin 0 -> 424 bytes inc/chart.php | 274 +- inc/charts/getChart.php | 527 + inc/charts/getChartDataJSON.php | 234 + inc/mainpage.php | 23 +- inc/sensors_exec.php | 5 +- inc/settings.php | 19 +- inc/settings_cron.php | 2 + inc/settings_exec.php | 215 +- inc/settings_general.php | 7 + inc/settings_schedule.php | 26 +- inc/settings_user.php | 45 +- inc/settings_virtualsensors.php | 217 + inc/settings_virtualsensors_types.php | 102 + inc/vSensor_exec.php | 47 + .../getVirtualSensorTypeConfig.php | 45 + include_script.inc.php | 2 +- index.php | 6 + lib/base.inc.php | 3 + lib/config.inc.php | 11 +- lib/jscripts/futelldus_lights.js | 4 +- .../jquery.bootstrap.confirm.popover.js | 109 + lib/languages/en.php | 48 +- .../bootstrap-editable-1.4.4/CHANGELOG.txt | 181 + .../bootstrap-editable-1.4.4/LICENSE-MIT | 22 + .../bootstrap-editable-1.4.4/README.md | 71 + .../css/bootstrap-editable.css | 495 + .../bootstrap-editable/img/clear.png | Bin 0 -> 509 bytes .../bootstrap-editable/img/loading.gif | Bin 0 -> 1849 bytes .../js/bootstrap-editable.js | 6192 +++++++++++ .../js/bootstrap-editable.min.js | 5 + .../inputs-ext/address/address.css | 9 + .../inputs-ext/address/address.js | 170 + .../bootstrap-wysihtml5-0.0.2.css | 102 + .../bootstrap-wysihtml5-0.0.2.js | 499 + .../bootstrap-wysihtml5-0.0.2.min.js | 1 + .../wysihtml5-0.3.0.js | 9521 +++++++++++++++++ .../wysihtml5-0.3.0.min.js | 261 + .../wysiwyg-color.css | 67 + .../inputs-ext/wysihtml5/wysihtml5.js | 124 + lib/php_functions/datetime.functions.inc.php | 2 +- lib/php_functions/global.functions.inc.php | 41 +- lib/php_functions/telldus.functions.inc.php | 78 + .../vSensorPlugin.functions.inc.php | 489 + login/index.php | 1 - 195 files changed, 79046 insertions(+), 186 deletions(-) create mode 100644 HTTP/OAuth.php create mode 100644 HTTP/OAuth/Consumer.php create mode 100644 HTTP/OAuth/Consumer/Exception/InvalidResponse.php create mode 100644 HTTP/OAuth/Consumer/Request.php create mode 100644 HTTP/OAuth/Consumer/Response.php create mode 100644 HTTP/OAuth/Exception.php create mode 100644 HTTP/OAuth/Exception/NotImplemented.php create mode 100644 HTTP/OAuth/Message.php create mode 100644 HTTP/OAuth/Provider/Exception.php create mode 100644 HTTP/OAuth/Provider/Exception/InvalidRequest.php create mode 100644 HTTP/OAuth/Provider/Request.php create mode 100644 HTTP/OAuth/Provider/Response.php create mode 100644 HTTP/OAuth/Signature.php create mode 100644 HTTP/OAuth/Signature/Common.php create mode 100644 HTTP/OAuth/Signature/HMAC/SHA1.php create mode 100644 HTTP/OAuth/Signature/PLAINTEXT.php create mode 100644 HTTP/OAuth/Signature/RSA/SHA1.php create mode 100644 HTTP/OAuth/Store/Consumer/CacheLite.php create mode 100644 HTTP/OAuth/Store/Consumer/Interface.php create mode 100644 HTTP/OAuth/Store/Data.php create mode 100644 HTTP/Request2.php create mode 100644 HTTP/Request2/Adapter.php create mode 100644 HTTP/Request2/Adapter/Curl.php create mode 100644 HTTP/Request2/Adapter/Mock.php create mode 100644 HTTP/Request2/Adapter/Socket.php create mode 100644 HTTP/Request2/CookieJar.php create mode 100644 HTTP/Request2/Exception.php create mode 100644 HTTP/Request2/MultipartBody.php create mode 100644 HTTP/Request2/Observer/Log.php create mode 100644 HTTP/Request2/Response.php create mode 100644 HTTP/Request2/SOCKS5.php create mode 100644 HTTP/Request2/SocketWrapper.php create mode 100644 Net/SMTP.php create mode 100644 Net/Socket.php create mode 100644 Net/URL2.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/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/FixPHP5PEARWarnings.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/Cfg.php create mode 100644 PEAR/Installer/Role/Cfg.xml 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/Installer/Role/Www.php create mode 100644 PEAR/Installer/Role/Www.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/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 Starting with PHP and Telldus LIVE on server with cPanel (oAuth and API).pdf create mode 100644 cron_device_log.php create mode 100644 cron_virtual_sensor.php create mode 100644 dev/test_device_available.php create mode 100644 dev/test_nmap.php create mode 100644 dev/test_ssh2.php create mode 100644 dev/wartung_delete_double_devices_log.php create mode 100644 fuPlugins/virtutal_devices/device_as_sensor/index.php create mode 100644 fuPlugins/virtutal_devices/device_as_sensor/light_off.png create mode 100644 fuPlugins/virtutal_devices/device_as_sensor/light_on.png create mode 100644 fuPlugins/virtutal_devices/device_as_sensor/plugin_info.txt create mode 100644 fuPlugins/virtutal_devices/network_scan/host.png create mode 100644 fuPlugins/virtutal_devices/network_scan/index.php create mode 100644 fuPlugins/virtutal_devices/network_scan/plugin_info.txt create mode 100644 fuPlugins/virtutal_devices/owl_energy_monitor/fetchSonsorData.sh create mode 100644 fuPlugins/virtutal_devices/owl_energy_monitor/icon.png create mode 100644 fuPlugins/virtutal_devices/owl_energy_monitor/index.php create mode 100644 fuPlugins/virtutal_devices/owl_energy_monitor/plugin_info.txt create mode 100644 images/ajax-loader3.gif create mode 100644 images/host.png create mode 100644 images/light.png create mode 100644 images/moon.png create mode 100644 images/shared.png create mode 100644 inc/charts/getChart.php create mode 100644 inc/charts/getChartDataJSON.php create mode 100644 inc/settings_virtualsensors.php create mode 100644 inc/settings_virtualsensors_types.php create mode 100644 inc/vSensor_exec.php create mode 100644 inc/virtualHosts/getVirtualSensorTypeConfig.php create mode 100644 lib/jscripts/jquery.bootstrap.confirm.popover.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/CHANGELOG.txt create mode 100644 lib/packages/bootstrap-editable-1.4.4/LICENSE-MIT create mode 100644 lib/packages/bootstrap-editable-1.4.4/README.md create mode 100644 lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/css/bootstrap-editable.css create mode 100644 lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/clear.png create mode 100644 lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/loading.gif create mode 100644 lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/js/bootstrap-editable.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/js/bootstrap-editable.min.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/address/address.css create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/address/address.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.css create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/bootstrap-wysihtml5-0.0.2.min.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysihtml5-0.3.0.min.js create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/bootstrap-wysihtml5-0.0.2/wysiwyg-color.css create mode 100644 lib/packages/bootstrap-editable-1.4.4/inputs-ext/wysihtml5/wysihtml5.js create mode 100644 lib/php_functions/telldus.functions.inc.php create mode 100644 lib/php_functions/vSensorPlugin.functions.inc.php diff --git a/HTTP/OAuth.php b/HTTP/OAuth.php new file mode 100644 index 0000000..2b48681 --- /dev/null +++ b/HTTP/OAuth.php @@ -0,0 +1,200 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +/** + * HTTP_OAuth + * + * Main HTTP_OAuth class. Contains helper encoding methods. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +abstract class HTTP_OAuth +{ + + /** + * Log instances + * + * @var array $logs Instances of PEAR Log handlers + */ + static protected $logs = array(); + + /** + * Attaches an instance of PEAR Log + * + * Attached instances of PEAR Log handlers will be logged to + * through out the use of HTTP_OAuth + * + * @param Log $log Instance of a Log + * + * @return void + */ + static public function attachLog(Log $log) + { + self::$logs[] = $log; + } + + /** + * Detaches an instance of PEAR Log + * + * @param Log $detach Instance of PEAR Log to detach + * + * @return void + */ + static public function detachLog(Log $detach) + { + foreach (self::$logs as $key => $log) { + if ($log == $detach) { + unset(self::$logs[$key]); + } + } + } + + /** + * Log a message + * + * Announces a message to log to all the attached instances of + * PEAR Log handlers. Second argument is the method on Log to call. + * + * @param string $message Message to announce to Logs + * @param string $method Method to log with on Log instances + * + * @return void + */ + public function log($message, $method) + { + foreach (self::$logs as $log) { + $log->$method($message); + } + } + + /** + * Log debug message + * + * @param string $message Debug message + * + * @return void + */ + public function debug($message) + { + $this->log($message, 'debug'); + } + + /** + * Log info message + * + * @param string $message Info message + * + * @return void + */ + public function info($message) + { + $this->log($message, 'info'); + } + + /** + * Log error message + * + * @param string $message Error message + * + * @return void + */ + public function err($message) + { + $this->log($message, 'err'); + } + + /** + * Build HTTP Query + * + * @param array $params Name => value array of parameters + * + * @return string HTTP query + */ + static public function buildHttpQuery(array $params) + { + if (empty($params)) { + return ''; + } + + $keys = self::urlencode(array_keys($params)); + $values = self::urlencode(array_values($params)); + $params = array_combine($keys, $values); + + uksort($params, 'strcmp'); + + $pairs = array(); + foreach ($params as $key => $value) { + $pairs[] = $key . '=' . $value; + } + + return implode('&', $pairs); + } + + /** + * URL Encode + * + * @param mixed $item string or array of items to url encode + * + * @return mixed url encoded string or array of strings + */ + static public function urlencode($item) + { + static $search = array('+', '%7E'); + static $replace = array('%20', '~'); + + if (is_array($item)) { + return array_map(array('HTTP_OAuth', 'urlencode'), $item); + } + + if (is_scalar($item) === false) { + return $item; + } + + return str_replace($search, $replace, rawurlencode($item)); + } + + /** + * URL Decode + * + * @param mixed $item Item to url decode + * + * @return string URL decoded string + */ + static public function urldecode($item) + { + if (is_array($item)) { + return array_map(array('HTTP_OAuth', 'urldecode'), $item); + } + + return rawurldecode($item); + } + +} + +?> diff --git a/HTTP/OAuth/Consumer.php b/HTTP/OAuth/Consumer.php new file mode 100644 index 0000000..9facd81 --- /dev/null +++ b/HTTP/OAuth/Consumer.php @@ -0,0 +1,422 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth.php'; +require_once 'HTTP/OAuth/Consumer/Request.php'; +require_once 'HTTP/OAuth/Consumer/Exception/InvalidResponse.php'; + +/** + * HTTP_OAuth_Consumer + * + * Main consumer class that assists consumers in establishing OAuth + * creditials and making OAuth requests. + * + * + * $consumer = new HTTP_OAuth_Consumer('key', 'secret'); + * $consumer->getRequestToken('http://example.com/oauth/request_token', $callback); + * + * // Store tokens + * $_SESSION['token'] = $consumer->getToken(); + * $_SESSION['token_secret'] = $consumer->getTokenSecret(); + * + * $url = $consumer->getAuthorizeUrl('http://example.com/oauth/authorize'); + * http_redirect($url); // function from pecl_http + * + * // When they come back via the $callback url + * $consumer = new HTTP_OAuth_Consumer('key', 'secret', $_SESSION['token'], + * $_SESSION['token_secret']); + * $consumer->getAccessToken('http://example.com/oauth/access_token'); + * + * // Store tokens + * $_SESSION['token'] = $consumer->getToken(); + * $_SESSION['token_secret'] = $consumer->getTokenSecret(); + * + * // $response is an instance of HTTP_OAuth_Consumer_Response + * $response = $consumer->sendRequest('http://example.com/oauth/protected_resource'); + * + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Consumer extends HTTP_OAuth +{ + + /** + * Consumer key + * + * @var string $key Consumer key + */ + protected $key = null; + + /** + * secret + * + * @var string $secret Consumer secret + */ + protected $secret = null; + + /** + * Token + * + * @var string Access/Request token + */ + protected $token = null; + + /** + * Token secret + * + * @var string $tokenSecret Access/Request token secret + */ + protected $tokenSecret = null; + + /** + * Signature method + * + * @var string $signatureMethod Signature method + */ + protected $signatureMethod = 'HMAC-SHA1'; + + /** + * Instance of HTTP_OAuth_Consumer_Request + * + * @see accept() + * @see getOAuthConsumerRequest() + * @var HTTP_OAuth_Consumer_Request + */ + protected $consumerRequest = null; + + /** + * Instance of the last request made + * + * @var HTTP_OAuth_Consumer_Request $lastRequest The last request made + */ + protected $lastRequest = null; + + /** + * Instance of the last response received + * + * @var HTTP_OAuth_Consumer_Response + */ + protected $lastResponse =null; + + /** + * Construct + * + * @param string $key Consumer key + * @param string $secret Consumer secret + * @param string $token Access/Reqest token + * @param string $tokenSecret Access/Reqest token secret + * + * @return void + */ + public function __construct($key, $secret, $token = null, $tokenSecret = null) + { + $this->key = $key; + $this->secret = $secret; + $this->setToken($token); + $this->setTokenSecret($tokenSecret); + } + + /** + * Get request token + * + * @param string $url Request token url + * @param string $callback Callback url + * @param array $additional Additional parameters to be in the request + * recommended in the spec. + * @param string $method HTTP method to use for the request + * + * @return void + * @throws HTTP_OAuth_Consumer_Exception_InvalidResponse Missing token/secret + */ + public function getRequestToken($url, $callback = 'oob', + array $additional = array(), $method = 'POST' + ) { + $this->debug('Getting request token from ' . $url); + $additional['oauth_callback'] = $callback; + + $this->debug('callback: ' . $callback); + $response = $this->sendRequest($url, $additional, $method); + $data = $response->getDataFromBody(); + if (empty($data['oauth_token']) || empty($data['oauth_token_secret'])) { + throw new HTTP_OAuth_Consumer_Exception_InvalidResponse( + 'Failed getting token and token secret from response', $response + ); + } + + $this->setToken($data['oauth_token']); + $this->setTokenSecret($data['oauth_token_secret']); + } + + /** + * Get access token + * + * @param string $url Access token url + * @param string $verifier OAuth verifier from the provider + * @param array $additional Additional parameters to be in the request + * recommended in the spec. + * @param string $method HTTP method to use for the request + * + * @return array Token and token secret + * @throws HTTP_OAuth_Consumer_Exception_InvalidResponse Mising token/secret + */ + public function getAccessToken($url, $verifier = '', + array $additional = array(), $method = 'POST' + ) { + if ($this->getToken() === null || $this->getTokenSecret() === null) { + throw new HTTP_OAuth_Exception('No token or token_secret'); + } + + $this->debug('Getting access token from ' . $url); + if ($verifier !== null) { + $additional['oauth_verifier'] = $verifier; + } + + $this->debug('verifier: ' . $verifier); + $response = $this->sendRequest($url, $additional, $method); + $data = $response->getDataFromBody(); + if (empty($data['oauth_token']) || empty($data['oauth_token_secret'])) { + throw new HTTP_OAuth_Consumer_Exception_InvalidResponse( + 'Failed getting token and token secret from response', $response + ); + } + + $this->setToken($data['oauth_token']); + $this->setTokenSecret($data['oauth_token_secret']); + } + + /** + * Get authorize url + * + * @param string $url Authorize url + * @param array $additional Additional parameters for the auth url + * + * @return string Authorize url + */ + public function getAuthorizeUrl($url, array $additional = array()) + { + $params = array('oauth_token' => $this->getToken()); + $params = array_merge($additional, $params); + + return sprintf('%s?%s', $url, HTTP_OAuth::buildHTTPQuery($params)); + } + + /** + * Send request + * + * @param string $url URL of the protected resource + * @param array $additional Additional parameters + * @param string $method HTTP method to use + * + * @return HTTP_OAuth_Consumer_Response Instance of a response class + */ + public function sendRequest($url, array $additional = array(), $method = 'POST') + { + $params = array( + 'oauth_consumer_key' => $this->key, + 'oauth_signature_method' => $this->getSignatureMethod() + ); + + if ($this->getToken()) { + $params['oauth_token'] = $this->getToken(); + } + + $params = array_merge($additional, $params); + + $req = clone $this->getOAuthConsumerRequest(); + + $req->setUrl($url); + $req->setMethod($method); + $req->setSecrets($this->getSecrets()); + $req->setParameters($params); + $this->lastResponse = $req->send(); + $this->lastRequest = $req; + + return $this->lastResponse; + } + + /** + * Get key + * + * @return string Consumer key + */ + public function getKey() + { + return $this->key; + } + + /** + * Get secret + * + * @return string Consumer secret + */ + public function getSecret() + { + return $this->secret; + } + + /** + * Get token + * + * @return string Token + */ + public function getToken() + { + return $this->token; + } + + /** + * Set token + * + * @param string $token Request/Access token + * + * @return void + */ + public function setToken($token) + { + $this->debug('token is now: ' . $token); + $this->token = $token; + } + + /** + * Get token secret + * + * @return string Accessoken secret + */ + public function getTokenSecret() + { + return $this->tokenSecret; + } + + /** + * Set token secret + * + * @param string $secret Token secret + * + * @return void + */ + public function setTokenSecret($secret) + { + $this->debug('token_secret is now: ' . $secret); + $this->tokenSecret = $secret; + } + + /** + * Get signature method + * + * @return string Signature method + */ + public function getSignatureMethod() + { + return $this->signatureMethod; + } + + /** + * Set signature method + * + * @param string $method Signature method to use + * + * @return void + */ + public function setSignatureMethod($method) + { + $this->signatureMethod = $method; + } + + /** + * Get secrets + * + * @return array Array possible secrets + */ + public function getSecrets() + { + return array($this->secret, (string) $this->tokenSecret); + } + + /** + * Accepts a custom instance of HTTP_OAuth_Consumer_Request. + * + * @param HTTP_OAuth_Consumer_Request $object Custom instance + * + * @see getOAuthConsumerRequest() + * @return void + */ + public function accept($object) + { + $class = get_class($object); + switch ($class) + { + case 'HTTP_OAuth_Consumer_Request': + $this->consumerRequest = $object; + break; + case 'HTTP_Request2': + $this->getOAuthConsumerRequest()->accept($object); + break; + default: + throw new HTTP_OAuth_Exception('Could not accept: ' . $class); + break; + } + } + + /** + * Gets instance of HTTP_OAuth_Consumer_Request + * + * @see accept() + * @return HTTP_OAuth_Consumer_Request + */ + public function getOAuthConsumerRequest() + { + if (!$this->consumerRequest instanceof HTTP_OAuth_Consumer_Request) { + $this->consumerRequest = new HTTP_OAuth_Consumer_Request; + } + return $this->consumerRequest; + } + + /** + * Gets the last request + * + * @return null|HTTP_OAuth_Consumer_Request Instance of the last request + * @see self::sendRequest() + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Gets the most recent HTTP_OAuth_Consumer_Response object + * + * @return HTTP_OAuth_Consumer_Response|null + */ + public function getLastResponse() + { + return $this->lastResponse; + } +} + +?> diff --git a/HTTP/OAuth/Consumer/Exception/InvalidResponse.php b/HTTP/OAuth/Consumer/Exception/InvalidResponse.php new file mode 100644 index 0000000..c71e45b --- /dev/null +++ b/HTTP/OAuth/Consumer/Exception/InvalidResponse.php @@ -0,0 +1,91 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Exception.php'; +require_once 'HTTP/OAuth/Consumer/Response.php'; + +/** + * HTTP_OAuth_Consumer_Exception_InvalidResponse + * + * Exception for invalid responses from OAuth providers. Supplies methods + * to get information about the request + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Consumer_Exception_InvalidResponse extends HTTP_OAuth_Exception +{ + + /** + * HTTP_OAuth_Consumer_Response instance + * + * @var HTTP_OAuth_Consumer_Response $resonse Invalid response + */ + public $response = null; + + /** + * Construct + * + * @param string $message Exception message + * @param HTTP_OAuth_Consumer_Response $response Invalid response + * + * @return void + */ + public function __construct($message, HTTP_OAuth_Consumer_Response $response) + { + parent::__construct($message); + + $this->response = $response; + } + + /** + * Call + * + * If method exists on self::$response pass to that, otherwise + * throw BadMethodCallException + * + * @param string $method Name of the method + * @param array $args Arguments for the method + * + * @return mixed Result from method + * @throws BadMethodCallException When method does not exist on + * self::$response + */ + public function __call($method, $args) + { + if (method_exists($this->response->getResponse(), $method) + || method_exists($this->response, $method) + ) { + return call_user_func_array(array($this->response, $method), $args); + } + + throw new BadMethodCallException($method); + } + + +} + +?> diff --git a/HTTP/OAuth/Consumer/Request.php b/HTTP/OAuth/Consumer/Request.php new file mode 100644 index 0000000..c29f677 --- /dev/null +++ b/HTTP/OAuth/Consumer/Request.php @@ -0,0 +1,359 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/Request2.php'; +require_once 'HTTP/Request2/Observer/Log.php'; +require_once 'HTTP/OAuth/Message.php'; +require_once 'HTTP/OAuth/Consumer/Response.php'; +require_once 'HTTP/OAuth/Signature.php'; +require_once 'HTTP/OAuth/Exception.php'; + +/** + * HTTP_OAuth_Consumer_Request + * + * Class to make OAuth requests to a provider. Given a url, consumer secret, + * token secret, and HTTP method make and sign a request to send. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Consumer_Request extends HTTP_OAuth_Message +{ + + /** + * Auth type constants + */ + const AUTH_HEADER = 1; + const AUTH_POST = 2; + const AUTH_GET = 3; + + /** + * Auth type + * + * @var int $authType Authorization type + */ + protected $authType = self::AUTH_HEADER; + + /** + * Secrets + * + * Consumer and token secrets that will be used to sign + * the request + * + * @var array $secrets Array of consumer and token secret + */ + protected $secrets = array('', ''); + + /** + * HTTP_Request2 instance + * + * @var HTTP_Request2 $request Instance of HTTP_Request2 + */ + protected $request = null; + + /** + * Construct + * + * Sets url, secrets, and http method + * + * @param string $url Url to be requested + * @param array $secrets Array of consumer and token secret + * + * @return void + */ + public function __construct($url = null, array $secrets = array()) + { + if ($url !== null) { + $this->setUrl($url); + } + + if (count($secrets)) { + $this->setSecrets($secrets); + } + } + + /** + * Accept + * + * @param mixed $object Object to accept + * + * @see getHTTPRequest2() + * @return void + */ + public function accept($object) + { + switch (get_class($object)) { + case 'HTTP_Request2': + $this->request = $object; + foreach (self::$logs as $log) { + $this->request->attach(new HTTP_Request2_Observer_Log($log)); + } + break; + default: + if ($object instanceof Log) { + HTTP_OAuth::attachLog($object); + $this->getHTTPRequest2()->attach( + new HTTP_Request2_Observer_Log($object) + ); + } + break; + } + } + + /** + * Returns $this->request if it is an instance of HTTP_Request. If not, it + * creates one. + * + * @return HTTP_Request2 + */ + protected function getHTTPRequest2() + { + if (!$this->request instanceof HTTP_Request2) { + $this->accept(new HTTP_Request2); + } + return $this->request; + } + + /** + * Sets consumer/token secrets array + * + * @param array $secrets Array of secrets to set + * + * @return void + */ + public function setSecrets(array $secrets = array()) + { + if (count($secrets) == 1) { + $secrets[1] = ''; + } + + $this->secrets = $secrets; + } + + /** + * Gets secrets + * + * @return array Secrets array + */ + public function getSecrets() + { + return $this->secrets; + } + + /** + * Sets authentication type + * + * Valid auth types are self::AUTH_HEADER, self::AUTH_POST, + * and self::AUTH_GET + * + * @param int $type Auth type defined by this class constants + * + * @return void + */ + public function setAuthType($type) + { + static $valid = array(self::AUTH_HEADER, self::AUTH_POST, + self::AUTH_GET); + if (!in_array($type, $valid)) { + throw new InvalidArgumentException('Invalid Auth Type, see class ' . + 'constants'); + } + + $this->authType = $type; + } + + /** + * Gets authentication type + * + * @return int Set auth type + */ + public function getAuthType() + { + return $this->authType; + } + + /** + * Sends request + * + * Builds and sends the request. This will sign the request with + * the given secrets at self::$secrets. + * + * @return HTTP_OAuth_Consumer_Response Response instance + * @throws HTTP_OAuth_Exception when request fails + */ + public function send() + { + $this->buildRequest(); + $request = $this->getHTTPRequest2(); + + // Hack for the OAuth's spec + => %20 and HTTP_Request2 + // HTTP_Request2 uses http_build_query() which does spaces + // as '+' and not '%20' + $headers = $request->getHeaders(); + $contentType = isset($headers['content-type']) + ? $headers['content-type'] : ''; + if ($this->getMethod() == 'POST' + && $contentType == 'application/x-www-form-urlencoded' + ) { + + $body = $this->getHTTPRequest2()->getBody(); + $body = str_replace('+', '%20', $body); + $this->getHTTPRequest2()->setBody($body); + } + + try { + $response = $this->getHTTPRequest2()->send(); + } catch (Exception $e) { + throw new HTTP_OAuth_Exception($e->getMessage(), $e->getCode()); + } + + return new HTTP_OAuth_Consumer_Response($response); + } + + /** + * Builds request for sending + * + * Adds timestamp, nonce, signs, and creates the HttpRequest object. + * + * @return HttpRequest Instance of the request object ready to send() + */ + protected function buildRequest() + { + $method = $this->getSignatureMethod(); + $this->debug('signing request with: ' . $method); + $sig = HTTP_OAuth_Signature::factory($this->getSignatureMethod()); + + $this->oauth_timestamp = time(); + $this->oauth_nonce = md5(microtime(true) . rand(1, 999)); + $this->oauth_version = '1.0'; + $params = array_merge( + $this->getParameters(), + $this->getUrl()->getQueryVariables() + ); + $this->oauth_signature = $sig->build( + $this->getMethod(), + $this->getUrl()->getURL(), + $params, + $this->secrets[0], + $this->secrets[1] + ); + + $params = $this->getOAuthParameters(); + switch ($this->getAuthType()) { + case self::AUTH_HEADER: + $auth = $this->getAuthForHeader($params); + $this->setHeader('Authorization', $auth); + break; + case self::AUTH_POST: + foreach ($params as $name => $value) { + $this->addPostParameter($name, $value); + } + break; + case self::AUTH_GET: + break; + } + + switch ($this->getMethod()) { + case 'POST': + foreach ($this->getParameters() as $name => $value) { + if (substr($name, 0, 6) == 'oauth_') { + continue; + } + + $this->addPostParameter($name, $value); + } + break; + case 'GET': + $url = $this->getUrl(); + foreach ($this->getParameters() as $name => $value) { + if (substr($name, 0, 6) == 'oauth_') { + continue; + } + + $url->setQueryVariable($name, $value); + } + + $this->setUrl($url); + break; + default: + break; + } + } + + /** + * Creates OAuth header + * + * Given the passed in OAuth parameters, put them together + * in a formated string for a Authorization header. + * + * @param array $params OAuth parameters + * + * @return void + */ + protected function getAuthForHeader(array $params) + { + $url = $this->getUrl(); + $realm = $url->getScheme() . '://' . $url->getHost() . '/'; + $header = 'OAuth realm="' . $realm . '"'; + foreach ($params as $name => $value) { + $header .= ", " . HTTP_OAuth::urlencode($name) . '="' . + HTTP_OAuth::urlencode($value) . '"'; + } + + return $header; + } + + /** + * Call + * + * If method exists on HTTP_Request2 pass to that, otherwise + * throw BadMethodCallException + * + * @param string $method Name of the method + * @param array $args Arguments for the method + * + * @return mixed Result from method + * @throws BadMethodCallException When method does not exist on HTTP_Request2 + */ + public function __call($method, $args) + { + $httpRequest2 = $this->getHTTPRequest2(); + + if (is_callable(array($httpRequest2, $method))) { + return call_user_func_array( + array($httpRequest2, $method), + $args + ); + } + + throw new BadMethodCallException($method); + } + +} + +?> diff --git a/HTTP/OAuth/Consumer/Response.php b/HTTP/OAuth/Consumer/Response.php new file mode 100644 index 0000000..4c3a0f8 --- /dev/null +++ b/HTTP/OAuth/Consumer/Response.php @@ -0,0 +1,112 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Message.php'; +require_once 'HTTP/OAuth/Exception.php'; +require_once 'HTTP/Request2/Response.php'; + +/** + * HTTP_OAuth_Consumer_Response + * + * Class to handle OAuth responses from a provider. Accepts and decorates a + * HTTP_Request2_Response instance + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Consumer_Response extends HTTP_OAuth_Message +{ + + /** + * Instance of HTTP_Request2_Response + * + * @var HTTP_Request2_Response $response Response from the HTTP_Request2 + */ + protected $response = null; + + /** + * Construct + * + * @param HTTP_Request2_Response $response Response from HTTP_Request2 + * + * @return void + */ + public function __construct(HTTP_Request2_Response $response) + { + $this->response = $response; + } + + /** + * Gets data from body + * + * If body is and OAuth specific query string, parse and return + * + * @return array Query string data + */ + public function getDataFromBody() + { + $result = array(); + parse_str($this->getBody(), $result); + return $result; + } + + /** + * Gets HTTP_Request2_Response + * + * @return HTTP_Request2_Response Instance of the current HTTP_Request2_Response + */ + public function getResponse() + { + return $this->response; + } + + /** + * Call + * + * If method exists on HTTP_Request2_Response pass to that, otherwise + * throw BadMethodCallException + * + * @param string $method Name of the method + * @param array $args Arguments for the method + * + * @return mixed Result from method + * @throws BadMethodCallException When method does not exist on + HTTP_Request2_Response + */ + public function __call($method, $args) + { + if (method_exists($this->response, $method)) { + return call_user_func_array(array($this->response, $method), $args); + } + + throw new BadMethodCallException($method); + } + +} + +?> diff --git a/HTTP/OAuth/Exception.php b/HTTP/OAuth/Exception.php new file mode 100644 index 0000000..cdcf51e --- /dev/null +++ b/HTTP/OAuth/Exception.php @@ -0,0 +1,61 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'PEAR/Exception.php'; + +/** + * HTTP_OAuth_Exception + * + * Main Exception class for HTTP_OAuth. All other HTTP_OAuth Exceptions + * extend this class. + * + * @uses PEAR_Exception + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Exception extends PEAR_Exception +{ + + /** + * Construct + * + * Allow no message to construct a exception + * + * @param string $message Exception message + * @param int|Exception|PEAR_Error|array|null $p2 Exception cause + * @param int|null $p3 Exception code or null + * + * @return void + */ + public function __construct($message = '', $p2 = null, $p3 = null) + { + parent::__construct($message, $p2, $p3); + } +} + +?> diff --git a/HTTP/OAuth/Exception/NotImplemented.php b/HTTP/OAuth/Exception/NotImplemented.php new file mode 100644 index 0000000..b1a9f87 --- /dev/null +++ b/HTTP/OAuth/Exception/NotImplemented.php @@ -0,0 +1,44 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Exception.php'; + +/** + * HTTP_OAuth_Exception_NotImplemented + * + * Exception class thrown when method are not yet implemented. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Exception_NotImplemented extends HTTP_OAuth_Exception +{ + +} + +?> diff --git a/HTTP/OAuth/Message.php b/HTTP/OAuth/Message.php new file mode 100644 index 0000000..4ea3a10 --- /dev/null +++ b/HTTP/OAuth/Message.php @@ -0,0 +1,257 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth.php'; + +/** + * HTTP_OAuth_Message + * + * Main message class for Request and Response classes to extend from. Provider + * and Consumer packages use this class as there parent for the request/response + * classes. This contains specification parameters handling and ArrayAccess, + * Countable, IteratorAggregate features. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +abstract class HTTP_OAuth_Message +extends HTTP_OAuth +implements ArrayAccess, Countable, IteratorAggregate +{ + + /** + * OAuth Parameters + * + * @var string $oauthParams OAuth parameters + */ + static protected $oauthParams = array( + 'consumer_key', + 'token', + 'token_secret', + 'signature_method', + 'signature', + 'timestamp', + 'nonce', + 'verifier', + 'version', + 'callback', + 'session_handle' + ); + + /** + * Parameters + * + * @var array $parameters Parameters + */ + protected $parameters = array(); + + /** + * Get OAuth specific parameters + * + * @return array OAuth specific parameters + */ + public function getOAuthParameters() + { + $params = array(); + foreach (self::$oauthParams as $param) { + if ($this->$param !== null) { + $params[$this->prefixParameter($param)] = $this->$param; + } + } + + ksort($params); + + return $params; + } + + /** + * Get parameters + * + * @return array Request's parameters + */ + public function getParameters() + { + $params = $this->parameters; + ksort($params); + + return $params; + } + + /** + * Set parameters + * + * @param array $params Name => value pair array of parameters + * + * @return void + */ + public function setParameters(array $params) + { + foreach ($params as $name => $value) { + $this->parameters[$this->prefixParameter($name)] = $value; + } + } + + /** + * Get signature method + * + * @return string Signature method + */ + public function getSignatureMethod() + { + if ($this->oauth_signature_method !== null) { + return $this->oauth_signature_method; + } + + return 'HMAC-SHA1'; + } + + /** + * Get + * + * @param string $var Variable to get + * + * @return mixed Parameter if exists, else null + */ + public function __get($var) + { + $var = $this->prefixParameter($var); + if (array_key_exists($var, $this->parameters)) { + return $this->parameters[$var]; + } + + $method = 'get' . ucfirst($var); + if (method_exists($this, $method)) { + return $this->$method(); + } + + return null; + } + + /** + * Set + * + * @param string $var Name of the variable + * @param mixed $val Value of the variable + * + * @return void + */ + public function __set($var, $val) + { + $this->parameters[$this->prefixParameter($var)] = $val; + } + + /** + * Offset exists + * + * @param string $offset Name of the offset + * + * @return bool Offset exists or not + */ + public function offsetExists($offset) + { + return isset($this->parameters[$this->prefixParameter($offset)]); + } + + /** + * Offset get + * + * @param string $offset Name of the offset + * + * @return string Offset value + */ + public function offsetGet($offset) + { + return $this->parameters[$this->prefixParameter($offset)]; + } + + /** + * Offset set + * + * @param string $offset Name of the offset + * @param string $value Value of the offset + * + * @return void + */ + public function offsetSet($offset, $value) + { + $this->parameters[$this->prefixParameter($offset)] = $value; + } + + /** + * Offset unset + * + * @param string $offset Name of the offset + * + * @return void + */ + public function offsetUnset($offset) + { + unset($this->parameters[$this->prefixParameter($offset)]); + } + + /** + * Count + * + * @return int Amount of parameters + */ + public function count() + { + return count($this->parameters); + } + + /** + * Get iterator + * + * @return ArrayIterator Iterator for self::$parameters + */ + public function getIterator() + { + return new ArrayIterator($this->parameters); + } + + /** + * Prefix parameter + * + * Prefixes a parameter name with oauth_ if it is a valid oauth paramter + * + * @param string $param Name of the parameter + * + * @return string Prefix parameter + */ + protected function prefixParameter($param) + { + if (in_array($param, self::$oauthParams)) { + $param = 'oauth_' . $param; + } + + return $param; + } + +} + +?> diff --git a/HTTP/OAuth/Provider/Exception.php b/HTTP/OAuth/Provider/Exception.php new file mode 100644 index 0000000..3ab6718 --- /dev/null +++ b/HTTP/OAuth/Provider/Exception.php @@ -0,0 +1,40 @@ + + * @copyright 2009 Joe Stump + * @license http://tinyurl.com/new-bsd New BSD License + * @version CVS: $Id:$ + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/joestump/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Exception.php'; + +/** + * Base exception class for all provider exceptions + * + * @category HTTP + * @package HTTP_OAuth + * @author Joe Stump + * @license http://tinyurl.com/new-bsd New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + */ +class HTTP_OAuth_Provider_Exception extends HTTP_OAuth_Exception +{ + +} + +?> diff --git a/HTTP/OAuth/Provider/Exception/InvalidRequest.php b/HTTP/OAuth/Provider/Exception/InvalidRequest.php new file mode 100644 index 0000000..e415f0f --- /dev/null +++ b/HTTP/OAuth/Provider/Exception/InvalidRequest.php @@ -0,0 +1,41 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Provider/Exception.php'; + +/** + * HTTP_OAuth_Provider_Exception_InvalidRequest + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Provider_Exception_InvalidRequest +extends HTTP_OAuth_Provider_Exception +{ + +} + +?> diff --git a/HTTP/OAuth/Provider/Request.php b/HTTP/OAuth/Provider/Request.php new file mode 100644 index 0000000..984d4bc --- /dev/null +++ b/HTTP/OAuth/Provider/Request.php @@ -0,0 +1,366 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Message.php'; +require_once 'HTTP/OAuth/Signature.php'; +require_once 'HTTP/OAuth/Provider/Exception/InvalidRequest.php'; + +/** + * HTTP_OAuth_Provider_Request + * + * NOTE: + * Currently, it's up to the developer to implement the provider side of + * timestamp and nonce checking. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Provider_Request extends HTTP_OAuth_Message +{ + + /** + * Headers from the incoming request + * + * @var array $headers Headers from the incoming request + */ + protected $headers = array(); + + /** + * Method used in the incoming request + * + * @var string Method used in the incoming request + */ + protected $method = ''; + + /** + * Construct + * + * @return void + */ + public function __construct() + { + $this->setHeaders(); + $this->setParametersFromRequest(); + } + + /** + * Set incoming request headers + * + * @param array $headers Optional headers to set + * + * @return void + */ + public function setHeaders(array $headers = array()) + { + if (count($headers)) { + $this->headers = $headers; + } else if (is_array($this->apacheRequestHeaders())) { + $this->debug('Using apache_request_headers() to get request headers'); + $this->headers = $this->apacheRequestHeaders(); + } else if (is_array($this->peclHttpHeaders())) { + $this->debug('Using pecl_http to get request headers'); + $this->headers = $this->peclHttpHeaders(); + } else { + $this->debug('Using $_SERVER to get request headers'); + foreach ($_SERVER as $name => $value) { + if (substr($name, 0, 5) == 'HTTP_') { + $name = str_replace( + ' ', '-', + ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) + ); + $this->headers[$name] = $value; + } + } + } + } + + /** + * Apache request headers + * + * If the function exists to get the request headers from apache + * use it to get them, otherwise return null. Abstracted for + * testing purposes. + * + * @return array|null Headers or null if no function + */ + protected function apacheRequestHeaders() + { + if (function_exists('apache_request_headers')) { + // @codeCoverageIgnoreStart + return apache_request_headers(); + // @codeCoverageIgnoreEnd + } + + return null; + } + + // @codeCoverageIgnoreStart + /** + * Pecl HTTP request headers + * + * If the pecl_http extension is loaded use it to get the incoming + * request headers, otherwise return null. Abstracted for testing + * purposes. + * + * @return array|null Headers or null if no extension + */ + protected function peclHttpHeaders() + { + if (extension_loaded('http') && class_exists('HttpMessage')) { + $message = HttpMessage::fromEnv(HttpMessage::TYPE_REQUEST); + return $message->getHeaders(); + } + + return null; + } + // @codeCoverageIgnoreEnd + + /** + * Set parameters from the incoming request + * + * @return void + */ + public function setParametersFromRequest() + { + $params = array(); + $auth = $this->getHeader('Authorization'); + if ($auth !== null) { + $this->debug('Using OAuth data from header'); + $parts = explode(',', $auth); + foreach ($parts as $part) { + list($key, $value) = explode('=', trim($part)); + if (strstr(strtolower($key), 'oauth ') + || strstr(strtolower($key), 'uth re') + || substr(strtolower($key), 0, 6) != 'oauth_' + ) { + continue; + } + + $value = trim($value); + $value = str_replace('"', '', $value); + + $params[$key] = $value; + } + } + + if ($this->getRequestMethod() == 'POST') { + $this->debug('getting data from POST'); + $contentType = substr($this->getHeader('Content-Type'), 0, 33); + if ($contentType !== 'application/x-www-form-urlencoded') { + throw new HTTP_OAuth_Provider_Exception_InvalidRequest('Invalid ' . + 'content type for POST request'); + } + + $params = array_merge( + $params, + $this->parseQueryString($this->getPostData()) + ); + } + + $params = array_merge( + $params, + $this->parseQueryString($this->getQueryString()) + ); + + if (empty($params)) { + throw new HTTP_OAuth_Provider_Exception_InvalidRequest('No oauth ' . + 'data found from request'); + } + + $this->setParameters(HTTP_OAuth::urldecode($params)); + } + + /** + * Is valid signature + * + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return bool Valid or not + */ + public function isValidSignature($consumerSecret, $tokenSecret = '') + { + if (!$this->oauth_signature_method) { + throw new HTTP_OAuth_Provider_Exception_InvalidRequest( + 'Missing oauth_signature_method in request' + ); + } + + $sign = HTTP_OAuth_Signature::factory($this->oauth_signature_method); + $check = $sign->build( + $this->getRequestMethod(), $this->getUrl(), + $this->getParameters(), $consumerSecret, $tokenSecret + ); + + if ($this->oauth_signature === $check) { + $this->info('Valid signature'); + return true; + } + + $this->err('Invalid signature'); + return false; + + } + + /** + * Gets incoming request query string + * + * @return string|null Query string + */ + public function getQueryString() + { + if (!empty($_SERVER['QUERY_STRING'])) { + return $_SERVER['QUERY_STRING']; + } + + return null; + } + + /** + * Get request method + * + * @return string Request method + */ + public function getRequestMethod() + { + if (!array_key_exists('REQUEST_METHOD', $_SERVER)) { + return 'HEAD'; + } + + return $_SERVER['REQUEST_METHOD']; + } + + /** + * Get url + * + * @return string URL of the request + */ + public function getUrl() + { + $schema = 'http'; + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { + $schema .= 's'; + } + + return $schema . '://' . $this->getHeader('Host') + . $this->getRequestUri(); + } + + /** + * Gets incoming request URI + * + * Checks if the schema/host is included and strips it. + * Thanks Naosumi! Bug #16800 + * + * @return string|null Request URI, null if doesn't exist + */ + public function getRequestUri() + { + if (!array_key_exists('REQUEST_URI', $_SERVER)) { + return null; + } + + $uri = $_SERVER['REQUEST_URI']; + $pos = stripos($uri, '://'); + if (!$pos) { + return $uri; + } + + return substr($uri, strpos($uri, '/', $pos + 3)); + } + + /** + * Gets a header + * + * @param string $header Which header to fetch + * + * @return string|null Header if exists, null if not + */ + public function getHeader($header) + { + foreach ($this->headers as $name => $value) { + if (strtolower($header) == strtolower($name)) { + return $value; + } + } + + return null; + } + + /** + * getHeaders + * + * @access public + * @return void + */ + public function getHeaders() + { + return $this->headers; + } + + // @codeCoverageIgnoreStart + /** + * Gets POST data + * + * @return string Post data + */ + protected function getPostData() + { + return file_get_contents('php://input'); + } + // @codeCoverageIgnoreEnd + + /** + * Parses a query string + * + * Does not urldecode the name or values like $_GET and $_POST + * + * @param string $string Query string + * + * @return array Data from the query string + */ + protected function parseQueryString($string) + { + $data = array(); + if (empty($string)) { + return $data; + } + + foreach (explode('&', $string) as $part) { + if (!strstr($part, '=')) { + continue; + } + + list($key, $value) = explode('=', $part); + $data[$key] = self::urldecode($value); + } + + return $data; + } + +} + +?> diff --git a/HTTP/OAuth/Provider/Response.php b/HTTP/OAuth/Provider/Response.php new file mode 100644 index 0000000..3bbd193 --- /dev/null +++ b/HTTP/OAuth/Provider/Response.php @@ -0,0 +1,287 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Message.php'; + +/** + * HTTP_OAuth_Provider_Response + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Provider_Response extends HTTP_OAuth_Message +{ + + const STATUS_UNSUPPORTED_PARAMETER = 0; + const STATUS_UNSUPPORTED_SIGNATURE_METHOD = 1; + const STATUS_MISSING_REQUIRED_PARAMETER = 2; + const STATUS_DUPLICATED_OAUTH_PARAMETER = 3; + + const STATUS_INVALID_CONSUMER_KEY = 4; + const STATUS_INVALID_TOKEN = 5; + const STATUS_INVALID_SIGNATURE = 6; + const STATUS_INVALID_NONCE = 7; + const STATUS_INVALID_VERIFIER = 8; + const STATUS_INVALID_TIMESTAMP = 9; + + /** + * Status map + * + * Map of what statuses have codes and body text + * + * @var array $statusMap Map of status to code and text + */ + static protected $statusMap = array( + self::STATUS_UNSUPPORTED_PARAMETER => array( + 400, 'Unsupported parameter' + ), + self::STATUS_UNSUPPORTED_SIGNATURE_METHOD => array( + 400, 'Unsupported signature method' + ), + self::STATUS_MISSING_REQUIRED_PARAMETER => array( + 400, 'Missing required parameter' + ), + self::STATUS_DUPLICATED_OAUTH_PARAMETER => array( + 400, 'Duplicated OAuth Protocol Parameter' + ), + self::STATUS_INVALID_CONSUMER_KEY => array( + 401, 'Invalid Consumer Key' + ), + self::STATUS_INVALID_TOKEN => array( + 401, 'Invalid / expired Token' + ), + self::STATUS_INVALID_SIGNATURE => array( + 401, 'Invalid signature' + ), + self::STATUS_INVALID_NONCE => array( + 401, 'Invalid / used nonce' + ), + self::STATUS_INVALID_VERIFIER => array( + 401, 'Invalid verifier' + ), + self::STATUS_INVALID_TIMESTAMP => array( + 401, 'Invalid timestamp' + ), + ); + + /** + * Headers to be sent the OAuth response + * + * @var array $headers Headers to send as an OAuth response + */ + protected $headers = array(); + + /** + * Body of the response + * + * @var string $body Body of the response + */ + protected $body = ''; + + /** + * Set realm + * + * @param string $realm Realm for the WWW-Authenticate header + * + * @return void + */ + public function setRealm($realm) + { + $header = 'OAuth realm="' . $realm . '"'; + $this->setHeader('WWW-Authenticate', $header); + } + + /** + * Set header + * + * @param string $name Name of the header + * @param string $value Value of the header + * + * @return void + */ + public function setHeader($name, $value) + { + $this->headers[$name] = $value; + } + + /** + * Get header + * + * @param string $name Name of header + * + * @return string|null Header if exists, null if not + */ + public function getHeader($name) + { + if (array_key_exists($name, $this->headers)) { + return $this->headers[$name]; + } + + return null; + } + + /** + * Get all headers + * + * @return array Current headers to send + */ + public function getHeaders() + { + return $this->headers; + } + + /** + * Set all headers + * + * @param array $headers Sets all headers to this name/value array + * + * @return void + */ + public function setHeaders(array $headers) + { + $this->headers = $headers; + } + + /** + * Set status + * + * @param int $status Status constant + * + * @return void + */ + public function setStatus($status) + { + if (!array_key_exists($status, self::$statusMap)) { + throw new HTTP_OAuth_Exception('Invalid status'); + } + + list($code, $text) = self::$statusMap[$status]; + $this->setBody($text); + + if ($this->headersSent()) { + throw new HTTP_OAuth_Exception('Status already sent'); + } + + switch ($code) { + case 400: + $this->header('HTTP/1.1 400 Bad Request'); + break; + case 401: + $this->header('HTTP/1.1 401 Unauthorized'); + break; + } + } + + // @codeCoverageIgnoreStart + /** + * Headers sent + * + * @return bool If the headers have been sent + */ + protected function headersSent() + { + return headers_sent(); + } + + /** + * Header + * + * @param string $header Header to add + * + * @return void + */ + protected function header($header) + { + return header($header); + } + // @codeCoverageIgnoreEnd + + /** + * Prepare body + * + * Sets the body if nesscary + * + * @return void + */ + protected function prepareBody() + { + if ($this->headersSent() && $this->getBody() !== '') { + $this->err('Body already sent, not setting'); + } else { + $this->setBody(HTTP_OAuth::buildHTTPQuery($this->getParameters())); + } + } + + /** + * Set body + * + * @param string $body Sets the body to send + * + * @return void + */ + public function setBody($body) + { + $this->body = $body; + } + + /** + * Get body + * + * @return string Body that will be sent + */ + public function getBody() + { + return $this->body; + } + + /** + * Send response + * + * Does a check whether or not headers have been sent in order + * to determine if it can send them. + * + * @return void + */ + public function send() + { + $this->prepareBody(); + if (!$this->headersSent()) { + $this->header('HTTP/1.1 200 OK'); + foreach ($this->getHeaders() as $name => $value) { + // @codeCoverageIgnoreStart + $this->header($name . ': ' . $value); + // @codeCoverageIgnoreEnd + } + } else { + $this->err('Headers already sent, can not send headers'); + } + + echo $this->getBody(); + } + +} + +?> diff --git a/HTTP/OAuth/Signature.php b/HTTP/OAuth/Signature.php new file mode 100644 index 0000000..befea5b --- /dev/null +++ b/HTTP/OAuth/Signature.php @@ -0,0 +1,72 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth.php'; + +/** + * HTTP_OAuth_Signature + * + * Base util class for signatures, e.g. factory. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +abstract class HTTP_OAuth_Signature +{ + + /** + * Factory + * + * @param string $method Signature method + * + * @return HTTP_OAuth_Signature_Common Signature instance + */ + static public function factory($method) + { + $method = str_replace('-', '_', $method); + $class = 'HTTP_OAuth_Signature_' . $method; + $file = str_replace('_', '/', $class) . '.php'; + + include_once $file; + + if (class_exists($class) === false) { + throw new InvalidArgumentException('No such signature class'); + } + + $instance = new $class; + if (!$instance instanceof HTTP_OAuth_Signature_Common) { + throw new InvalidArgumentException( + 'Signature class does not extend HTTP_OAuth_Signature_Common' + ); + } + + return $instance; + } +} + +?> diff --git a/HTTP/OAuth/Signature/Common.php b/HTTP/OAuth/Signature/Common.php new file mode 100644 index 0000000..e35b399 --- /dev/null +++ b/HTTP/OAuth/Signature/Common.php @@ -0,0 +1,99 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth.php'; + +/** + * HTTP_OAuth_Signature_Common + * + * Common class for signature implemenations. Holds specification logic to + * create signature base strings and keys. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +abstract class HTTP_OAuth_Signature_Common extends HTTP_OAuth +{ + + /** + * Get base + * + * @param mixed $method HTTP method used in the request + * @param mixed $url URL of the request + * @param array $params Parameters in the request + * + * @return string Base signature string + */ + public function getBase($method, $url, array $params) + { + if (array_key_exists('oauth_signature', $params)) { + unset($params['oauth_signature']); + } + + $urlParts = explode('?', $url); + + $parts = array($method, $urlParts[0], + HTTP_OAuth::buildHTTPQuery($params)); + $base = implode('&', HTTP_OAuth::urlencode($parts)); + $this->debug('Signing with base string: ' . $base); + return $base; + } + + /** + * Get key + * + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return string Signature key + */ + public function getKey($consumerSecret, $tokenSecret = '') + { + $secrets = array($consumerSecret, $tokenSecret); + $key = implode('&', HTTP_OAuth::urlencode($secrets)); + $this->debug('Signing with key: ' . $key); + return $key; + } + + /** + * Build + * + * @param string $method HTTP method used + * @param string $url URL of the request + * @param array $params Parameters of the request + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return string Signature + */ + abstract public function build($method, $url, array $params, + $consumerSecret, $tokenSecret = '' + ); +} + + diff --git a/HTTP/OAuth/Signature/HMAC/SHA1.php b/HTTP/OAuth/Signature/HMAC/SHA1.php new file mode 100644 index 0000000..b4c69f9 --- /dev/null +++ b/HTTP/OAuth/Signature/HMAC/SHA1.php @@ -0,0 +1,66 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Signature/Common.php'; + +/** + * HTTP_OAuth_Signature_HMAC_SHA1 + * + * Signature class for the HMAC-SHA1 signing method. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Signature_HMAC_SHA1 extends HTTP_OAuth_Signature_Common +{ + + /** + * Build + * + * @param string $method HTTP method used + * @param string $url URL of the request + * @param array $params Parameters of the request + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return string Signature + */ + public function build($method, $url, array $params, $consumerSecret, + $tokenSecret = '' + ) { + return base64_encode( + hash_hmac( + 'sha1', $this->getBase($method, $url, $params), + $this->getKey($consumerSecret, $tokenSecret), true + ) + ); + } + +} + +?> diff --git a/HTTP/OAuth/Signature/PLAINTEXT.php b/HTTP/OAuth/Signature/PLAINTEXT.php new file mode 100644 index 0000000..34cdb79 --- /dev/null +++ b/HTTP/OAuth/Signature/PLAINTEXT.php @@ -0,0 +1,61 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Signature/Common.php'; + +/** + * HTTP_OAuth_Signature_PLAINTEXT + * + * Signature class for the PLAINTEXT implementation. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Signature_PLAINTEXT extends HTTP_OAuth_Signature_Common +{ + + /** + * Build + * + * @param string $method HTTP method used + * @param string $url URL of the request + * @param array $params Parameters of the request + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return string Signature + */ + public function build($method, $url, array $params, $consumerSecret, + $tokenSecret = '' + ) { + return $this->getKey($consumerSecret, $tokenSecret); + } + +} + +?> diff --git a/HTTP/OAuth/Signature/RSA/SHA1.php b/HTTP/OAuth/Signature/RSA/SHA1.php new file mode 100644 index 0000000..caa50eb --- /dev/null +++ b/HTTP/OAuth/Signature/RSA/SHA1.php @@ -0,0 +1,62 @@ + + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ + +require_once 'HTTP/OAuth/Signature/Common.php'; +require_once 'HTTP/OAuth/Exception/NotImplemented.php'; + +/** + * HTTP_OAuth_Signature_RSA_SHA1 + * + * Signature class for the RSA-SHA1 signing method. + * + * @category HTTP + * @package HTTP_OAuth + * @author Jeff Hodsdon + * @copyright 2009 Jeff Hodsdon + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/HTTP_OAuth + * @link http://github.com/jeffhodsdon/HTTP_OAuth + */ +class HTTP_OAuth_Signature_RSA_SHA1 +{ + + /** + * Build + * + * @param string $method HTTP method used + * @param string $url URL of the request + * @param array $params Parameters of the request + * @param string $consumerSecret Consumer secret value + * @param string $tokenSecret Token secret value (if exists) + * + * @return string Signature + */ + public function build($method, $url, array $params, $consumerSecret, + $tokenSecret = '' + ) { + throw new HTTP_OAuth_Exception_NotImplemented; + } + +} + +?> diff --git a/HTTP/OAuth/Store/Consumer/CacheLite.php b/HTTP/OAuth/Store/Consumer/CacheLite.php new file mode 100644 index 0000000..b8e4594 --- /dev/null +++ b/HTTP/OAuth/Store/Consumer/CacheLite.php @@ -0,0 +1,231 @@ + + * @copyright 2010 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ + +require_once 'HTTP/OAuth/Store/Data.php'; +require_once 'HTTP/OAuth/Store/Consumer/Interface.php'; +require_once 'Cache/Lite.php'; + +/** + * Cache_Lite driver for HTTP_OAuth_Store_Consumer_Interface + * + * @uses HTTP_OAuth_Store_Consumer_Interface + * @category HTTP + * @package HTTP_OAuth + * @author Bill Shupp + * @copyright 2010 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ +class HTTP_OAuth_Store_Consumer_CacheLite +implements HTTP_OAuth_Store_Consumer_Interface +{ + const TYPE_REQUEST = 'requestTokens'; + const TYPE_ACCESS = 'accessTokens'; + const REQUEST_TOKEN_LIFETIME = 300; + + /** + * Instance of Cache_Lite + * + * @var Cache_Lite|null + */ + protected $cache = null; + + /** + * CacheLite options + * + * @var array + * @see $defaultOptions + */ + protected $options = array(); + + /** + * Default options for Cache_Lite + * + * @var array + */ + protected $defaultOptions = array( + 'cacheDir' => '/tmp', + 'lifeTime' => 300, + 'hashedDirectoryLevel' => 2 + ); + + + /** + * Instantiate Cache_Lite. Allows for options to be passed to Cache_Lite. + * + * @param array $options Options for Cache_Lite constructor + * + * @return void + */ + public function __construct(array $options = array()) + { + $this->options = array_merge($this->defaultOptions, $options); + $this->cache = new Cache_Lite($this->options); + } + + /** + * Sets a request token + * + * @param string $token The request token + * @param string $tokenSecret The request token secret + * @param string $providerName The name of the provider (i.e. 'twitter') + * @param string $sessionID A string representing this user's session + * + * @return true on success, false or PEAR_Error on failure + */ + public function setRequestToken($token, $tokenSecret, $providerName, $sessionID) + { + $this->setOptions(self::TYPE_REQUEST, self::REQUEST_TOKEN_LIFETIME); + $data = array( + 'token' => $token, + 'tokenSecret' => $tokenSecret, + 'providerName' => $providerName, + 'sessionID' => $sessionID + ); + + return $this->cache->save( + serialize($data), + $this->getRequestTokenKey($providerName, $sessionID) + ); + } + + /** + * Gets a request token as an array of the token, tokenSecret, providerName, + * and sessionID (array key names) + * + * @param string $providerName The provider name (i.e. 'twitter') + * @param string $sessionID A string representing this user's session + * + * @return array on success, false on failure + */ + public function getRequestToken($providerName, $sessionID) + { + $this->setOptions(self::TYPE_REQUEST, self::REQUEST_TOKEN_LIFETIME); + $result = $this->cache->get( + $this->getRequestTokenKey($providerName, $sessionID) + ); + return unserialize($result); + } + + /** + * Gets a cache key for request tokens. It's an md5 hash of the provider name + * and sessionID + * + * @param string $providerName The provider name (i.e. 'twitter') + * @param string $sessionID A string representing this user's session + * + * @return string + */ + protected function getRequestTokenKey($providerName, $sessionID) + { + return md5($providerName . ':' . $sessionID); + } + + /** + * Gets access token data in the form of an HTTP_OAuth_Store_Data object + * + * @param string $consumerUserID The end user's ID at the consumer + * @param string $providerName The provider name (i.e. 'twitter') + * + * @return HTTP_OAuth_Store_Data + */ + public function getAccessToken($consumerUserID, $providerName) + { + $this->setOptions(self::TYPE_ACCESS); + $result = $this->cache->get( + $this->getAccessTokenKey($consumerUserID, $providerName) + ); + return unserialize($result); + } + + /** + * Sets access token data from an HTTP_OAuth_Store_Data object + * + * @param HTTP_OAuth_Store_Data $data The access token data + * + * @return bool true on success, false or PEAR_Error on failure + */ + public function setAccessToken(HTTP_OAuth_Store_Data $data) + { + $this->setOptions(self::TYPE_ACCESS); + + $key = $this->getAccessTokenKey($data->consumerUserID, $data->providerName); + return $this->cache->save(serialize($data), $key); + } + + /** + * Removes an access token + * + * @param HTTP_OAuth_Store_Data $data The access token data + * + * @return bool true on success, false or PEAR_Error on failure + */ + public function removeAccessToken(HTTP_OAuth_Store_Data $data) + { + $this->setOptions(self::TYPE_ACCESS); + + $key = $this->getAccessTokenKey($data->consumerUserID, $data->providerName); + return $this->cache->remove($key); + } + + /** + * Gets an access token key for storage, based on the consumer user ID and the + * provider name + * + * @param string $consumerUserID The end user's ID at the consumer + * @param string $providerName The provider name (i.e. 'twitter') + * + * @return void + */ + protected function getAccessTokenKey($consumerUserID, $providerName) + { + return md5($consumerUserID . ':' . $providerName); + } + + /** + * Sets options for Cache_Lite based on the needs of the current method. + * Options set include the subdirectory to be used, and the expiration. + * + * @param string $key The sub-directory of the cacheDir + * @param string $expire The cache lifetime (expire) to be used + * + * @return void + */ + protected function setOptions($key, $expire = null) + { + $cacheDir = $this->options['cacheDir'] . '/oauth/'; + $cacheDir .= rtrim($key, '/') . '/'; + + $this->ensureDirectoryExists($cacheDir); + + $this->cache->setOption('cacheDir', $cacheDir); + $this->cache->setOption('lifeTime', $expire); + } + + /** + * Make sure the given sub directory exists. If not, create it. + * + * @param string $dir The full path to the sub director we plan to write to + * + * @return void + */ + protected function ensureDirectoryExists($dir) + { + if (!file_exists($dir)) { + mkdir($dir, 0777, true); + } + } +} +?> diff --git a/HTTP/OAuth/Store/Consumer/Interface.php b/HTTP/OAuth/Store/Consumer/Interface.php new file mode 100644 index 0000000..bf9609a --- /dev/null +++ b/HTTP/OAuth/Store/Consumer/Interface.php @@ -0,0 +1,78 @@ + + * @copyright 2010 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ + +/** + * A consumer storage interface for access tokens and request tokens. + * + * @category HTTP + * @package HTTP_OAuth + * @author Bill Shupp + * @copyright 2010 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ +interface HTTP_OAuth_Store_Consumer_Interface +{ + /** + * Sets a request token + * + * @param string $token The request token + * @param string $tokenSecret The request token secret + * @param string $providerName The name of the provider (i.e. 'twitter') + * @param string $sessionID A string representing this user's session + * + * @return true on success, false or failure + */ + public function setRequestToken($token, $tokenSecret, $providerName, $sessionID); + + /** + * Gets a request token as an array of the token, tokenSecret, providerName, + * and sessionID (array key names) + * + * @param string $providerName The provider name (i.e. 'twitter') + * @param string $sessionID A string representing this user's session + * + * @return array on success, false on failure + */ + public function getRequestToken($providerName, $sessionID); + + /** + * Gets access token data in the form of an HTTP_OAuth_Store_Data object + * + * @param string $consumerUserID The end user's ID at the consumer + * @param string $providerName The provider name (i.e. 'twitter') + * + * @return HTTP_OAuth_Data_Store + */ + public function getAccessToken($consumerUserID, $providerName); + + /** + * Sets access token data from an HTTP_OAuth_Store_Data object + * + * @param HTTP_OAuth_Store_Data $data The access token data + * + * @return bool true on success, false on failure + */ + public function setAccessToken(HTTP_OAuth_Store_Data $data); + + /** + * Removes an access token + * + * @param HTTP_OAuth_Store_Data $data The access token data + * + * @return bool true on success, false or PEAR_Error on failure + */ + public function removeAccessToken(HTTP_OAuth_Store_Data $data); +} +?> diff --git a/HTTP/OAuth/Store/Data.php b/HTTP/OAuth/Store/Data.php new file mode 100644 index 0000000..ae35a1c --- /dev/null +++ b/HTTP/OAuth/Store/Data.php @@ -0,0 +1,69 @@ + + * @copyright 2009 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ + +/** + * A simple structure for storing oauth access token data. + * + * @category HTTP + * @package HTTP_OAuth + * @author Bill Shupp + * @copyright 2009 Bill Shupp + * @license http://www.opensource.org/licenses/bsd-license.php FreeBSD + * @link http://pear.php.net/http_oauth + */ +class HTTP_OAuth_Store_Data +{ + /** + * The user's unique ID at the the consumer + * + * @var mixed + */ + public $consumerUserID = null; + + /** + * The user's unique ID at the provider + * + * @var mixed + */ + public $providerUserID = null; + + /** + * The name of the provider (i.e. 'twitter') + * + * @var mixed + */ + public $providerName = null; + + /** + * The access token + * + * @var string + */ + public $accessToken = null; + + /** + * The access token secret + * + * @var string + */ + public $accessTokenSecret = null; + + /** + * The token's scope + * + * @var mixed + */ + public $scope = null; +} +?> diff --git a/HTTP/Request2.php b/HTTP/Request2.php new file mode 100644 index 0000000..2b15195 --- /dev/null +++ b/HTTP/Request2.php @@ -0,0 +1,2100 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Request2.php 324936 2012-04-07 07:49:03Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** + + * A class representing an URL as per RFC 3986. + + */ + +require_once 'Net/URL2.php'; + + + +/** + + * Exception class for HTTP_Request2 package + + */ + +require_once 'HTTP/Request2/Exception.php'; + + + +/** + + * Class representing a HTTP request message + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + * @link http://tools.ietf.org/html/rfc2616#section-5 + + */ + +class HTTP_Request2 implements SplSubject + +{ + + /**#@+ + + * Constants for HTTP request methods + + * + + * @link http://tools.ietf.org/html/rfc2616#section-5.1.1 + + */ + + const METHOD_OPTIONS = 'OPTIONS'; + + const METHOD_GET = 'GET'; + + const METHOD_HEAD = 'HEAD'; + + const METHOD_POST = 'POST'; + + const METHOD_PUT = 'PUT'; + + const METHOD_DELETE = 'DELETE'; + + const METHOD_TRACE = 'TRACE'; + + const METHOD_CONNECT = 'CONNECT'; + + /**#@-*/ + + + + /**#@+ + + * Constants for HTTP authentication schemes + + * + + * @link http://tools.ietf.org/html/rfc2617 + + */ + + const AUTH_BASIC = 'basic'; + + const AUTH_DIGEST = 'digest'; + + /**#@-*/ + + + + /** + + * Regular expression used to check for invalid symbols in RFC 2616 tokens + + * @link http://pear.php.net/bugs/bug.php?id=15630 + + */ + + const REGEXP_INVALID_TOKEN = '![\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]!'; + + + + /** + + * Regular expression used to check for invalid symbols in cookie strings + + * @link http://pear.php.net/bugs/bug.php?id=15630 + + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + + */ + + const REGEXP_INVALID_COOKIE = '/[\s,;]/'; + + + + /** + + * Fileinfo magic database resource + + * @var resource + + * @see detectMimeType() + + */ + + private static $_fileinfoDb; + + + + /** + + * Observers attached to the request (instances of SplObserver) + + * @var array + + */ + + protected $observers = array(); + + + + /** + + * Request URL + + * @var Net_URL2 + + */ + + protected $url; + + + + /** + + * Request method + + * @var string + + */ + + protected $method = self::METHOD_GET; + + + + /** + + * Authentication data + + * @var array + + * @see getAuth() + + */ + + protected $auth; + + + + /** + + * Request headers + + * @var array + + */ + + protected $headers = array(); + + + + /** + + * Configuration parameters + + * @var array + + * @see setConfig() + + */ + + protected $config = array( + + 'adapter' => 'HTTP_Request2_Adapter_Socket', + + 'connect_timeout' => 10, + + 'timeout' => 0, + + 'use_brackets' => true, + + 'protocol_version' => '1.1', + + 'buffer_size' => 16384, + + 'store_body' => true, + + + + 'proxy_host' => '', + + 'proxy_port' => '', + + 'proxy_user' => '', + + 'proxy_password' => '', + + 'proxy_auth_scheme' => self::AUTH_BASIC, + + 'proxy_type' => 'http', + + + + 'ssl_verify_peer' => true, + + 'ssl_verify_host' => true, + + 'ssl_cafile' => null, + + 'ssl_capath' => null, + + 'ssl_local_cert' => null, + + 'ssl_passphrase' => null, + + + + 'digest_compat_ie' => false, + + + + 'follow_redirects' => false, + + 'max_redirects' => 5, + + 'strict_redirects' => false + + ); + + + + /** + + * Last event in request / response handling, intended for observers + + * @var array + + * @see getLastEvent() + + */ + + protected $lastEvent = array( + + 'name' => 'start', + + 'data' => null + + ); + + + + /** + + * Request body + + * @var string|resource + + * @see setBody() + + */ + + protected $body = ''; + + + + /** + + * Array of POST parameters + + * @var array + + */ + + protected $postParams = array(); + + + + /** + + * Array of file uploads (for multipart/form-data POST requests) + + * @var array + + */ + + protected $uploads = array(); + + + + /** + + * Adapter used to perform actual HTTP request + + * @var HTTP_Request2_Adapter + + */ + + protected $adapter; + + + + /** + + * Cookie jar to persist cookies between requests + + * @var HTTP_Request2_CookieJar + + */ + + protected $cookieJar = null; + + + + /** + + * Constructor. Can set request URL, method and configuration array. + + * + + * Also sets a default value for User-Agent header. + + * + + * @param string|Net_Url2 $url Request URL + + * @param string $method Request method + + * @param array $config Configuration for this Request instance + + */ + + public function __construct( + + $url = null, $method = self::METHOD_GET, array $config = array() + + ) { + + $this->setConfig($config); + + if (!empty($url)) { + + $this->setUrl($url); + + } + + if (!empty($method)) { + + $this->setMethod($method); + + } + + $this->setHeader( + + 'user-agent', 'HTTP_Request2/2.1.1 ' . + + '(http://pear.php.net/package/http_request2) PHP/' . phpversion() + + ); + + } + + + + /** + + * Sets the URL for this request + + * + + * If the URL has userinfo part (username & password) these will be removed + + * and converted to auth data. If the URL does not have a path component, + + * that will be set to '/'. + + * + + * @param string|Net_URL2 $url Request URL + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function setUrl($url) + + { + + if (is_string($url)) { + + $url = new Net_URL2( + + $url, array(Net_URL2::OPTION_USE_BRACKETS => $this->config['use_brackets']) + + ); + + } + + if (!$url instanceof Net_URL2) { + + throw new HTTP_Request2_LogicException( + + 'Parameter is not a valid HTTP URL', + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + // URL contains username / password? + + if ($url->getUserinfo()) { + + $username = $url->getUser(); + + $password = $url->getPassword(); + + $this->setAuth(rawurldecode($username), $password? rawurldecode($password): ''); + + $url->setUserinfo(''); + + } + + if ('' == $url->getPath()) { + + $url->setPath('/'); + + } + + $this->url = $url; + + + + return $this; + + } + + + + /** + + * Returns the request URL + + * + + * @return Net_URL2 + + */ + + public function getUrl() + + { + + return $this->url; + + } + + + + /** + + * Sets the request method + + * + + * @param string $method one of the methods defined in RFC 2616 + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException if the method name is invalid + + */ + + public function setMethod($method) + + { + + // Method name should be a token: http://tools.ietf.org/html/rfc2616#section-5.1.1 + + if (preg_match(self::REGEXP_INVALID_TOKEN, $method)) { + + throw new HTTP_Request2_LogicException( + + "Invalid request method '{$method}'", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + $this->method = $method; + + + + return $this; + + } + + + + /** + + * Returns the request method + + * + + * @return string + + */ + + public function getMethod() + + { + + return $this->method; + + } + + + + /** + + * Sets the configuration parameter(s) + + * + + * The following parameters are available: + + *
    + + *
  • 'adapter' - adapter to use (string)
  • + + *
  • 'connect_timeout' - Connection timeout in seconds (integer)
  • + + *
  • 'timeout' - Total number of seconds a request can take. + + * Use 0 for no limit, should be greater than + + * 'connect_timeout' if set (integer)
  • + + *
  • 'use_brackets' - Whether to append [] to array variable names (bool)
  • + + *
  • 'protocol_version' - HTTP Version to use, '1.0' or '1.1' (string)
  • + + *
  • 'buffer_size' - Buffer size to use for reading and writing (int)
  • + + *
  • 'store_body' - Whether to store response body in response object. + + * Set to false if receiving a huge response and + + * using an Observer to save it (boolean)
  • + + *
  • 'proxy_type' - Proxy type, 'http' or 'socks5' (string)
  • + + *
  • 'proxy_host' - Proxy server host (string)
  • + + *
  • 'proxy_port' - Proxy server port (integer)
  • + + *
  • 'proxy_user' - Proxy auth username (string)
  • + + *
  • 'proxy_password' - Proxy auth password (string)
  • + + *
  • 'proxy_auth_scheme' - Proxy auth scheme, one of HTTP_Request2::AUTH_* constants (string)
  • + + *
  • 'proxy' - Shorthand for proxy_* parameters, proxy given as URL, + + * e.g. 'socks5://localhost:1080/' (string)
  • + + *
  • 'ssl_verify_peer' - Whether to verify peer's SSL certificate (bool)
  • + + *
  • 'ssl_verify_host' - Whether to check that Common Name in SSL + + * certificate matches host name (bool)
  • + + *
  • 'ssl_cafile' - Cerificate Authority file to verify the peer + + * with (use with 'ssl_verify_peer') (string)
  • + + *
  • 'ssl_capath' - Directory holding multiple Certificate + + * Authority files (string)
  • + + *
  • 'ssl_local_cert' - Name of a file containing local cerificate (string)
  • + + *
  • 'ssl_passphrase' - Passphrase with which local certificate + + * was encoded (string)
  • + + *
  • 'digest_compat_ie' - Whether to imitate behaviour of MSIE 5 and 6 + + * in using URL without query string in digest + + * authentication (boolean)
  • + + *
  • 'follow_redirects' - Whether to automatically follow HTTP Redirects (boolean)
  • + + *
  • 'max_redirects' - Maximum number of redirects to follow (integer)
  • + + *
  • 'strict_redirects' - Whether to keep request method on redirects via status 301 and + + * 302 (true, needed for compatibility with RFC 2616) + + * or switch to GET (false, needed for compatibility with most + + * browsers) (boolean)
  • + + *
+ + * + + * @param string|array $nameOrConfig configuration parameter name or array + + * ('parameter name' => 'parameter value') + + * @param mixed $value parameter value if $nameOrConfig is not an array + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException If the parameter is unknown + + */ + + public function setConfig($nameOrConfig, $value = null) + + { + + if (is_array($nameOrConfig)) { + + foreach ($nameOrConfig as $name => $value) { + + $this->setConfig($name, $value); + + } + + + + } elseif ('proxy' == $nameOrConfig) { + + $url = new Net_URL2($value); + + $this->setConfig(array( + + 'proxy_type' => $url->getScheme(), + + 'proxy_host' => $url->getHost(), + + 'proxy_port' => $url->getPort(), + + 'proxy_user' => rawurldecode($url->getUser()), + + 'proxy_password' => rawurldecode($url->getPassword()) + + )); + + + + } else { + + if (!array_key_exists($nameOrConfig, $this->config)) { + + throw new HTTP_Request2_LogicException( + + "Unknown configuration parameter '{$nameOrConfig}'", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + $this->config[$nameOrConfig] = $value; + + } + + + + return $this; + + } + + + + /** + + * Returns the value(s) of the configuration parameter(s) + + * + + * @param string $name parameter name + + * + + * @return mixed value of $name parameter, array of all configuration + + * parameters if $name is not given + + * @throws HTTP_Request2_LogicException If the parameter is unknown + + */ + + public function getConfig($name = null) + + { + + if (null === $name) { + + return $this->config; + + } elseif (!array_key_exists($name, $this->config)) { + + throw new HTTP_Request2_LogicException( + + "Unknown configuration parameter '{$name}'", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + return $this->config[$name]; + + } + + + + /** + + * Sets the autentification data + + * + + * @param string $user user name + + * @param string $password password + + * @param string $scheme authentication scheme + + * + + * @return HTTP_Request2 + + */ + + public function setAuth($user, $password = '', $scheme = self::AUTH_BASIC) + + { + + if (empty($user)) { + + $this->auth = null; + + } else { + + $this->auth = array( + + 'user' => (string)$user, + + 'password' => (string)$password, + + 'scheme' => $scheme + + ); + + } + + + + return $this; + + } + + + + /** + + * Returns the authentication data + + * + + * The array has the keys 'user', 'password' and 'scheme', where 'scheme' + + * is one of the HTTP_Request2::AUTH_* constants. + + * + + * @return array + + */ + + public function getAuth() + + { + + return $this->auth; + + } + + + + /** + + * Sets request header(s) + + * + + * The first parameter may be either a full header string 'header: value' or + + * header name. In the former case $value parameter is ignored, in the latter + + * the header's value will either be set to $value or the header will be + + * removed if $value is null. The first parameter can also be an array of + + * headers, in that case method will be called recursively. + + * + + * Note that headers are treated case insensitively as per RFC 2616. + + * + + * + + * $req->setHeader('Foo: Bar'); // sets the value of 'Foo' header to 'Bar' + + * $req->setHeader('FoO', 'Baz'); // sets the value of 'Foo' header to 'Baz' + + * $req->setHeader(array('foo' => 'Quux')); // sets the value of 'Foo' header to 'Quux' + + * $req->setHeader('FOO'); // removes 'Foo' header from request + + * + + * + + * @param string|array $name header name, header string ('Header: value') + + * or an array of headers + + * @param string|array|null $value header value if $name is not an array, + + * header will be removed if value is null + + * @param bool $replace whether to replace previous header with the + + * same name or append to its value + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function setHeader($name, $value = null, $replace = true) + + { + + if (is_array($name)) { + + foreach ($name as $k => $v) { + + if (is_string($k)) { + + $this->setHeader($k, $v, $replace); + + } else { + + $this->setHeader($v, null, $replace); + + } + + } + + } else { + + if (null === $value && strpos($name, ':')) { + + list($name, $value) = array_map('trim', explode(':', $name, 2)); + + } + + // Header name should be a token: http://tools.ietf.org/html/rfc2616#section-4.2 + + if (preg_match(self::REGEXP_INVALID_TOKEN, $name)) { + + throw new HTTP_Request2_LogicException( + + "Invalid header name '{$name}'", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + // Header names are case insensitive anyway + + $name = strtolower($name); + + if (null === $value) { + + unset($this->headers[$name]); + + + + } else { + + if (is_array($value)) { + + $value = implode(', ', array_map('trim', $value)); + + } elseif (is_string($value)) { + + $value = trim($value); + + } + + if (!isset($this->headers[$name]) || $replace) { + + $this->headers[$name] = $value; + + } else { + + $this->headers[$name] .= ', ' . $value; + + } + + } + + } + + + + return $this; + + } + + + + /** + + * Returns the request headers + + * + + * The array is of the form ('header name' => 'header value'), header names + + * are lowercased + + * + + * @return array + + */ + + public function getHeaders() + + { + + return $this->headers; + + } + + + + /** + + * Adds a cookie to the request + + * + + * If the request does not have a CookieJar object set, this method simply + + * appends a cookie to "Cookie:" header. + + * + + * If a CookieJar object is available, the cookie is stored in that object. + + * Data from request URL will be used for setting its 'domain' and 'path' + + * parameters, 'expires' and 'secure' will be set to null and false, + + * respectively. If you need further control, use CookieJar's methods. + + * + + * @param string $name cookie name + + * @param string $value cookie value + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + * @see setCookieJar() + + */ + + public function addCookie($name, $value) + + { + + if (!empty($this->cookieJar)) { + + $this->cookieJar->store( + + array('name' => $name, 'value' => $value), $this->url + + ); + + + + } else { + + $cookie = $name . '=' . $value; + + if (preg_match(self::REGEXP_INVALID_COOKIE, $cookie)) { + + throw new HTTP_Request2_LogicException( + + "Invalid cookie: '{$cookie}'", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + $cookies = empty($this->headers['cookie'])? '': $this->headers['cookie'] . '; '; + + $this->setHeader('cookie', $cookies . $cookie); + + } + + + + return $this; + + } + + + + /** + + * Sets the request body + + * + + * If you provide file pointer rather than file name, it should support + + * fstat() and rewind() operations. + + * + + * @param string|resource|HTTP_Request2_MultipartBody $body Either a + + * string with the body or filename containing body or + + * pointer to an open file or object with multipart body data + + * @param bool $isFilename Whether + + * first parameter is a filename + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function setBody($body, $isFilename = false) + + { + + if (!$isFilename && !is_resource($body)) { + + if (!$body instanceof HTTP_Request2_MultipartBody) { + + $this->body = (string)$body; + + } else { + + $this->body = $body; + + } + + } else { + + $fileData = $this->fopenWrapper($body, empty($this->headers['content-type'])); + + $this->body = $fileData['fp']; + + if (empty($this->headers['content-type'])) { + + $this->setHeader('content-type', $fileData['type']); + + } + + } + + $this->postParams = $this->uploads = array(); + + + + return $this; + + } + + + + /** + + * Returns the request body + + * + + * @return string|resource|HTTP_Request2_MultipartBody + + */ + + public function getBody() + + { + + if (self::METHOD_POST == $this->method + + && (!empty($this->postParams) || !empty($this->uploads)) + + ) { + + if (0 === strpos($this->headers['content-type'], 'application/x-www-form-urlencoded')) { + + $body = http_build_query($this->postParams, '', '&'); + + if (!$this->getConfig('use_brackets')) { + + $body = preg_replace('/%5B\d+%5D=/', '=', $body); + + } + + // support RFC 3986 by not encoding '~' symbol (request #15368) + + return str_replace('%7E', '~', $body); + + + + } elseif (0 === strpos($this->headers['content-type'], 'multipart/form-data')) { + + require_once 'HTTP/Request2/MultipartBody.php'; + + return new HTTP_Request2_MultipartBody( + + $this->postParams, $this->uploads, $this->getConfig('use_brackets') + + ); + + } + + } + + return $this->body; + + } + + + + /** + + * Adds a file to form-based file upload + + * + + * Used to emulate file upload via a HTML form. The method also sets + + * Content-Type of HTTP request to 'multipart/form-data'. + + * + + * If you just want to send the contents of a file as the body of HTTP + + * request you should use setBody() method. + + * + + * If you provide file pointers rather than file names, they should support + + * fstat() and rewind() operations. + + * + + * @param string $fieldName name of file-upload field + + * @param string|resource|array $filename full name of local file, + + * pointer to open file or an array of files + + * @param string $sendFilename filename to send in the request + + * @param string $contentType content-type of file being uploaded + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function addUpload( + + $fieldName, $filename, $sendFilename = null, $contentType = null + + ) { + + if (!is_array($filename)) { + + $fileData = $this->fopenWrapper($filename, empty($contentType)); + + $this->uploads[$fieldName] = array( + + 'fp' => $fileData['fp'], + + 'filename' => !empty($sendFilename)? $sendFilename + + :(is_string($filename)? basename($filename): 'anonymous.blob') , + + 'size' => $fileData['size'], + + 'type' => empty($contentType)? $fileData['type']: $contentType + + ); + + } else { + + $fps = $names = $sizes = $types = array(); + + foreach ($filename as $f) { + + if (!is_array($f)) { + + $f = array($f); + + } + + $fileData = $this->fopenWrapper($f[0], empty($f[2])); + + $fps[] = $fileData['fp']; + + $names[] = !empty($f[1])? $f[1] + + :(is_string($f[0])? basename($f[0]): 'anonymous.blob'); + + $sizes[] = $fileData['size']; + + $types[] = empty($f[2])? $fileData['type']: $f[2]; + + } + + $this->uploads[$fieldName] = array( + + 'fp' => $fps, 'filename' => $names, 'size' => $sizes, 'type' => $types + + ); + + } + + if (empty($this->headers['content-type']) + + || 'application/x-www-form-urlencoded' == $this->headers['content-type'] + + ) { + + $this->setHeader('content-type', 'multipart/form-data'); + + } + + + + return $this; + + } + + + + /** + + * Adds POST parameter(s) to the request. + + * + + * @param string|array $name parameter name or array ('name' => 'value') + + * @param mixed $value parameter value (can be an array) + + * + + * @return HTTP_Request2 + + */ + + public function addPostParameter($name, $value = null) + + { + + if (!is_array($name)) { + + $this->postParams[$name] = $value; + + } else { + + foreach ($name as $k => $v) { + + $this->addPostParameter($k, $v); + + } + + } + + if (empty($this->headers['content-type'])) { + + $this->setHeader('content-type', 'application/x-www-form-urlencoded'); + + } + + + + return $this; + + } + + + + /** + + * Attaches a new observer + + * + + * @param SplObserver $observer any object implementing SplObserver + + */ + + public function attach(SplObserver $observer) + + { + + foreach ($this->observers as $attached) { + + if ($attached === $observer) { + + return; + + } + + } + + $this->observers[] = $observer; + + } + + + + /** + + * Detaches an existing observer + + * + + * @param SplObserver $observer any object implementing SplObserver + + */ + + public function detach(SplObserver $observer) + + { + + foreach ($this->observers as $key => $attached) { + + if ($attached === $observer) { + + unset($this->observers[$key]); + + return; + + } + + } + + } + + + + /** + + * Notifies all observers + + */ + + public function notify() + + { + + foreach ($this->observers as $observer) { + + $observer->update($this); + + } + + } + + + + /** + + * Sets the last event + + * + + * Adapters should use this method to set the current state of the request + + * and notify the observers. + + * + + * @param string $name event name + + * @param mixed $data event data + + */ + + public function setLastEvent($name, $data = null) + + { + + $this->lastEvent = array( + + 'name' => $name, + + 'data' => $data + + ); + + $this->notify(); + + } + + + + /** + + * Returns the last event + + * + + * Observers should use this method to access the last change in request. + + * The following event names are possible: + + *
    + + *
  • 'connect' - after connection to remote server, + + * data is the destination (string)
  • + + *
  • 'disconnect' - after disconnection from server
  • + + *
  • 'sentHeaders' - after sending the request headers, + + * data is the headers sent (string)
  • + + *
  • 'sentBodyPart' - after sending a part of the request body, + + * data is the length of that part (int)
  • + + *
  • 'sentBody' - after sending the whole request body, + + * data is request body length (int)
  • + + *
  • 'receivedHeaders' - after receiving the response headers, + + * data is HTTP_Request2_Response object
  • + + *
  • 'receivedBodyPart' - after receiving a part of the response + + * body, data is that part (string)
  • + + *
  • 'receivedEncodedBodyPart' - as 'receivedBodyPart', but data is still + + * encoded by Content-Encoding
  • + + *
  • 'receivedBody' - after receiving the complete response + + * body, data is HTTP_Request2_Response object
  • + + *
+ + * Different adapters may not send all the event types. Mock adapter does + + * not send any events to the observers. + + * + + * @return array The array has two keys: 'name' and 'data' + + */ + + public function getLastEvent() + + { + + return $this->lastEvent; + + } + + + + /** + + * Sets the adapter used to actually perform the request + + * + + * You can pass either an instance of a class implementing HTTP_Request2_Adapter + + * or a class name. The method will only try to include a file if the class + + * name starts with HTTP_Request2_Adapter_, it will also try to prepend this + + * prefix to the class name if it doesn't contain any underscores, so that + + * + + * $request->setAdapter('curl'); + + * + + * will work. + + * + + * @param string|HTTP_Request2_Adapter $adapter Adapter to use + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function setAdapter($adapter) + + { + + if (is_string($adapter)) { + + if (!class_exists($adapter, false)) { + + if (false === strpos($adapter, '_')) { + + $adapter = 'HTTP_Request2_Adapter_' . ucfirst($adapter); + + } + + if (!class_exists($adapter, false) + + && preg_match('/^HTTP_Request2_Adapter_([a-zA-Z0-9]+)$/', $adapter) + + ) { + + include_once str_replace('_', DIRECTORY_SEPARATOR, $adapter) . '.php'; + + } + + if (!class_exists($adapter, false)) { + + throw new HTTP_Request2_LogicException( + + "Class {$adapter} not found", + + HTTP_Request2_Exception::MISSING_VALUE + + ); + + } + + } + + $adapter = new $adapter; + + } + + if (!$adapter instanceof HTTP_Request2_Adapter) { + + throw new HTTP_Request2_LogicException( + + 'Parameter is not a HTTP request adapter', + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + $this->adapter = $adapter; + + + + return $this; + + } + + + + /** + + * Sets the cookie jar + + * + + * A cookie jar is used to maintain cookies across HTTP requests and + + * responses. Cookies from jar will be automatically added to the request + + * headers based on request URL. + + * + + * @param HTTP_Request2_CookieJar|bool $jar Existing CookieJar object, true to + + * create a new one, false to remove + + * + + * @return HTTP_Request2 + + * @throws HTTP_Request2_LogicException + + */ + + public function setCookieJar($jar = true) + + { + + if (!class_exists('HTTP_Request2_CookieJar', false)) { + + require_once 'HTTP/Request2/CookieJar.php'; + + } + + + + if ($jar instanceof HTTP_Request2_CookieJar) { + + $this->cookieJar = $jar; + + } elseif (true === $jar) { + + $this->cookieJar = new HTTP_Request2_CookieJar(); + + } elseif (!$jar) { + + $this->cookieJar = null; + + } else { + + throw new HTTP_Request2_LogicException( + + 'Invalid parameter passed to setCookieJar()', + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + + + return $this; + + } + + + + /** + + * Returns current CookieJar object or null if none + + * + + * @return HTTP_Request2_CookieJar|null + + */ + + public function getCookieJar() + + { + + return $this->cookieJar; + + } + + + + /** + + * Sends the request and returns the response + + * + + * @throws HTTP_Request2_Exception + + * @return HTTP_Request2_Response + + */ + + public function send() + + { + + // Sanity check for URL + + if (!$this->url instanceof Net_URL2 + + || !$this->url->isAbsolute() + + || !in_array(strtolower($this->url->getScheme()), array('https', 'http')) + + ) { + + throw new HTTP_Request2_LogicException( + + 'HTTP_Request2 needs an absolute HTTP(S) request URL, ' + + . ($this->url instanceof Net_URL2 + + ? "'" . $this->url->__toString() . "'" : 'none') + + . ' given', + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + if (empty($this->adapter)) { + + $this->setAdapter($this->getConfig('adapter')); + + } + + // magic_quotes_runtime may break file uploads and chunked response + + // processing; see bug #4543. Don't use ini_get() here; see bug #16440. + + if ($magicQuotes = get_magic_quotes_runtime()) { + + set_magic_quotes_runtime(false); + + } + + // force using single byte encoding if mbstring extension overloads + + // strlen() and substr(); see bug #1781, bug #10605 + + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + + $oldEncoding = mb_internal_encoding(); + + mb_internal_encoding('8bit'); + + } + + + + try { + + $response = $this->adapter->sendRequest($this); + + } catch (Exception $e) { + + } + + // cleanup in either case (poor man's "finally" clause) + + if ($magicQuotes) { + + set_magic_quotes_runtime(true); + + } + + if (!empty($oldEncoding)) { + + mb_internal_encoding($oldEncoding); + + } + + // rethrow the exception + + if (!empty($e)) { + + throw $e; + + } + + return $response; + + } + + + + /** + + * Wrapper around fopen()/fstat() used by setBody() and addUpload() + + * + + * @param string|resource $file file name or pointer to open file + + * @param bool $detectType whether to try autodetecting MIME + + * type of file, will only work if $file is a + + * filename, not pointer + + * + + * @return array array('fp' => file pointer, 'size' => file size, 'type' => MIME type) + + * @throws HTTP_Request2_LogicException + + */ + + protected function fopenWrapper($file, $detectType = false) + + { + + if (!is_string($file) && !is_resource($file)) { + + throw new HTTP_Request2_LogicException( + + "Filename or file pointer resource expected", + + HTTP_Request2_Exception::INVALID_ARGUMENT + + ); + + } + + $fileData = array( + + 'fp' => is_string($file)? null: $file, + + 'type' => 'application/octet-stream', + + 'size' => 0 + + ); + + if (is_string($file)) { + + if (!($fileData['fp'] = @fopen($file, 'rb'))) { + + $error = error_get_last(); + + throw new HTTP_Request2_LogicException( + + $error['message'], HTTP_Request2_Exception::READ_ERROR + + ); + + } + + if ($detectType) { + + $fileData['type'] = self::detectMimeType($file); + + } + + } + + if (!($stat = fstat($fileData['fp']))) { + + throw new HTTP_Request2_LogicException( + + "fstat() call failed", HTTP_Request2_Exception::READ_ERROR + + ); + + } + + $fileData['size'] = $stat['size']; + + + + return $fileData; + + } + + + + /** + + * Tries to detect MIME type of a file + + * + + * The method will try to use fileinfo extension if it is available, + + * deprecated mime_content_type() function in the other case. If neither + + * works, default 'application/octet-stream' MIME type is returned + + * + + * @param string $filename file name + + * + + * @return string file MIME type + + */ + + protected static function detectMimeType($filename) + + { + + // finfo extension from PECL available + + if (function_exists('finfo_open')) { + + if (!isset(self::$_fileinfoDb)) { + + self::$_fileinfoDb = @finfo_open(FILEINFO_MIME); + + } + + if (self::$_fileinfoDb) { + + $info = finfo_file(self::$_fileinfoDb, $filename); + + } + + } + + // (deprecated) mime_content_type function available + + if (empty($info) && function_exists('mime_content_type')) { + + return mime_content_type($filename); + + } + + return empty($info)? 'application/octet-stream': $info; + + } + +} + +?> + diff --git a/HTTP/Request2/Adapter.php b/HTTP/Request2/Adapter.php new file mode 100644 index 0000000..8654f2b --- /dev/null +++ b/HTTP/Request2/Adapter.php @@ -0,0 +1,314 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Adapter.php 324415 2012-03-21 10:50:50Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** + + * Class representing a HTTP response + + */ + +require_once 'HTTP/Request2/Response.php'; + + + +/** + + * Base class for HTTP_Request2 adapters + + * + + * HTTP_Request2 class itself only defines methods for aggregating the request + + * data, all actual work of sending the request to the remote server and + + * receiving its response is performed by adapters. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +abstract class HTTP_Request2_Adapter + +{ + + /** + + * A list of methods that MUST NOT have a request body, per RFC 2616 + + * @var array + + */ + + protected static $bodyDisallowed = array('TRACE'); + + + + /** + + * Methods having defined semantics for request body + + * + + * Content-Length header (indicating that the body follows, section 4.3 of + + * RFC 2616) will be sent for these methods even if no body was added + + * + + * @var array + + * @link http://pear.php.net/bugs/bug.php?id=12900 + + * @link http://pear.php.net/bugs/bug.php?id=14740 + + */ + + protected static $bodyRequired = array('POST', 'PUT'); + + + + /** + + * Request being sent + + * @var HTTP_Request2 + + */ + + protected $request; + + + + /** + + * Request body + + * @var string|resource|HTTP_Request2_MultipartBody + + * @see HTTP_Request2::getBody() + + */ + + protected $requestBody; + + + + /** + + * Length of the request body + + * @var integer + + */ + + protected $contentLength; + + + + /** + + * Sends request to the remote server and returns its response + + * + + * @param HTTP_Request2 $request HTTP request message + + * + + * @return HTTP_Request2_Response + + * @throws HTTP_Request2_Exception + + */ + + abstract public function sendRequest(HTTP_Request2 $request); + + + + /** + + * Calculates length of the request body, adds proper headers + + * + + * @param array &$headers associative array of request headers, this method + + * will add proper 'Content-Length' and 'Content-Type' + + * headers to this array (or remove them if not needed) + + */ + + protected function calculateRequestLength(&$headers) + + { + + $this->requestBody = $this->request->getBody(); + + + + if (is_string($this->requestBody)) { + + $this->contentLength = strlen($this->requestBody); + + } elseif (is_resource($this->requestBody)) { + + $stat = fstat($this->requestBody); + + $this->contentLength = $stat['size']; + + rewind($this->requestBody); + + } else { + + $this->contentLength = $this->requestBody->getLength(); + + $headers['content-type'] = 'multipart/form-data; boundary=' . + + $this->requestBody->getBoundary(); + + $this->requestBody->rewind(); + + } + + + + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + + || 0 == $this->contentLength + + ) { + + // No body: send a Content-Length header nonetheless (request #12900), + + // but do that only for methods that require a body (bug #14740) + + if (in_array($this->request->getMethod(), self::$bodyRequired)) { + + $headers['content-length'] = 0; + + } else { + + unset($headers['content-length']); + + // if the method doesn't require a body and doesn't have a + + // body, don't send a Content-Type header. (request #16799) + + unset($headers['content-type']); + + } + + } else { + + if (empty($headers['content-type'])) { + + $headers['content-type'] = 'application/x-www-form-urlencoded'; + + } + + $headers['content-length'] = $this->contentLength; + + } + + } + +} + +?> + diff --git a/HTTP/Request2/Adapter/Curl.php b/HTTP/Request2/Adapter/Curl.php new file mode 100644 index 0000000..5aed001 --- /dev/null +++ b/HTTP/Request2/Adapter/Curl.php @@ -0,0 +1,584 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Curl.php 324746 2012-04-03 15:09:16Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Adapter for HTTP_Request2 wrapping around cURL extension + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 2.1.1 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Adapter_Curl extends HTTP_Request2_Adapter +{ + /** + * Mapping of header names to cURL options + * @var array + */ + protected static $headerMap = array( + 'accept-encoding' => CURLOPT_ENCODING, + 'cookie' => CURLOPT_COOKIE, + 'referer' => CURLOPT_REFERER, + 'user-agent' => CURLOPT_USERAGENT + ); + + /** + * Mapping of SSL context options to cURL options + * @var array + */ + protected static $sslContextMap = array( + 'ssl_verify_peer' => CURLOPT_SSL_VERIFYPEER, + 'ssl_cafile' => CURLOPT_CAINFO, + 'ssl_capath' => CURLOPT_CAPATH, + 'ssl_local_cert' => CURLOPT_SSLCERT, + 'ssl_passphrase' => CURLOPT_SSLCERTPASSWD + ); + + /** + * Mapping of CURLE_* constants to Exception subclasses and error codes + * @var array + */ + protected static $errorMap = array( + CURLE_UNSUPPORTED_PROTOCOL => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_COULDNT_RESOLVE_PROXY => array('HTTP_Request2_ConnectionException'), + CURLE_COULDNT_RESOLVE_HOST => array('HTTP_Request2_ConnectionException'), + CURLE_COULDNT_CONNECT => array('HTTP_Request2_ConnectionException'), + // error returned from write callback + CURLE_WRITE_ERROR => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_OPERATION_TIMEOUTED => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::TIMEOUT), + CURLE_HTTP_RANGE_ERROR => array('HTTP_Request2_MessageException'), + CURLE_SSL_CONNECT_ERROR => array('HTTP_Request2_ConnectionException'), + CURLE_LIBRARY_NOT_FOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_FUNCTION_NOT_FOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_ABORTED_BY_CALLBACK => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::NON_HTTP_REDIRECT), + CURLE_TOO_MANY_REDIRECTS => array('HTTP_Request2_MessageException', + HTTP_Request2_Exception::TOO_MANY_REDIRECTS), + CURLE_SSL_PEER_CERTIFICATE => array('HTTP_Request2_ConnectionException'), + CURLE_GOT_NOTHING => array('HTTP_Request2_MessageException'), + CURLE_SSL_ENGINE_NOTFOUND => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_SSL_ENGINE_SETFAILED => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::MISCONFIGURATION), + CURLE_SEND_ERROR => array('HTTP_Request2_MessageException'), + CURLE_RECV_ERROR => array('HTTP_Request2_MessageException'), + CURLE_SSL_CERTPROBLEM => array('HTTP_Request2_LogicException', + HTTP_Request2_Exception::INVALID_ARGUMENT), + CURLE_SSL_CIPHER => array('HTTP_Request2_ConnectionException'), + CURLE_SSL_CACERT => array('HTTP_Request2_ConnectionException'), + CURLE_BAD_CONTENT_ENCODING => array('HTTP_Request2_MessageException'), + ); + + /** + * Response being received + * @var HTTP_Request2_Response + */ + protected $response; + + /** + * Whether 'sentHeaders' event was sent to observers + * @var boolean + */ + protected $eventSentHeaders = false; + + /** + * Whether 'receivedHeaders' event was sent to observers + * @var boolean + */ + protected $eventReceivedHeaders = false; + + /** + * Position within request body + * @var integer + * @see callbackReadBody() + */ + protected $position = 0; + + /** + * Information about last transfer, as returned by curl_getinfo() + * @var array + */ + protected $lastInfo; + + /** + * Creates a subclass of HTTP_Request2_Exception from curl error data + * + * @param resource $ch curl handle + * + * @return HTTP_Request2_Exception + */ + protected static function wrapCurlError($ch) + { + $nativeCode = curl_errno($ch); + $message = 'Curl error: ' . curl_error($ch); + if (!isset(self::$errorMap[$nativeCode])) { + return new HTTP_Request2_Exception($message, 0, $nativeCode); + } else { + $class = self::$errorMap[$nativeCode][0]; + $code = empty(self::$errorMap[$nativeCode][1]) + ? 0 : self::$errorMap[$nativeCode][1]; + return new $class($message, $code, $nativeCode); + } + } + + /** + * Sends request to the remote server and returns its response + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + if (!extension_loaded('curl')) { + throw new HTTP_Request2_LogicException( + 'cURL extension not available', HTTP_Request2_Exception::MISCONFIGURATION + ); + } + + $this->request = $request; + $this->response = null; + $this->position = 0; + $this->eventSentHeaders = false; + $this->eventReceivedHeaders = false; + + try { + if (false === curl_exec($ch = $this->createCurlHandle())) { + $e = self::wrapCurlError($ch); + } + } catch (Exception $e) { + } + if (isset($ch)) { + $this->lastInfo = curl_getinfo($ch); + curl_close($ch); + } + + $response = $this->response; + unset($this->request, $this->requestBody, $this->response); + + if (!empty($e)) { + throw $e; + } + + if ($jar = $request->getCookieJar()) { + $jar->addCookiesFromResponse($response, $request->getUrl()); + } + + if (0 < $this->lastInfo['size_download']) { + $request->setLastEvent('receivedBody', $response); + } + return $response; + } + + /** + * Returns information about last transfer + * + * @return array associative array as returned by curl_getinfo() + */ + public function getInfo() + { + return $this->lastInfo; + } + + /** + * Creates a new cURL handle and populates it with data from the request + * + * @return resource a cURL handle, as created by curl_init() + * @throws HTTP_Request2_LogicException + */ + protected function createCurlHandle() + { + $ch = curl_init(); + + curl_setopt_array($ch, array( + // setup write callbacks + CURLOPT_HEADERFUNCTION => array($this, 'callbackWriteHeader'), + CURLOPT_WRITEFUNCTION => array($this, 'callbackWriteBody'), + // buffer size + CURLOPT_BUFFERSIZE => $this->request->getConfig('buffer_size'), + // connection timeout + CURLOPT_CONNECTTIMEOUT => $this->request->getConfig('connect_timeout'), + // save full outgoing headers, in case someone is interested + CURLINFO_HEADER_OUT => true, + // request url + CURLOPT_URL => $this->request->getUrl()->getUrl() + )); + + // set up redirects + if (!$this->request->getConfig('follow_redirects')) { + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + } else { + if (!@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)) { + throw new HTTP_Request2_LogicException( + 'Redirect support in curl is unavailable due to open_basedir or safe_mode setting', + HTTP_Request2_Exception::MISCONFIGURATION + ); + } + curl_setopt($ch, CURLOPT_MAXREDIRS, $this->request->getConfig('max_redirects')); + // limit redirects to http(s), works in 5.2.10+ + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + } + // works in 5.3.2+, http://bugs.php.net/bug.php?id=49571 + if ($this->request->getConfig('strict_redirects') && defined('CURLOPT_POSTREDIR')) { + curl_setopt($ch, CURLOPT_POSTREDIR, 3); + } + } + + // request timeout + if ($timeout = $this->request->getConfig('timeout')) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + + // set HTTP version + switch ($this->request->getConfig('protocol_version')) { + case '1.0': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + break; + case '1.1': + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); + } + + // set request method + switch ($this->request->getMethod()) { + case HTTP_Request2::METHOD_GET: + curl_setopt($ch, CURLOPT_HTTPGET, true); + break; + case HTTP_Request2::METHOD_POST: + curl_setopt($ch, CURLOPT_POST, true); + break; + case HTTP_Request2::METHOD_HEAD: + curl_setopt($ch, CURLOPT_NOBODY, true); + break; + case HTTP_Request2::METHOD_PUT: + curl_setopt($ch, CURLOPT_UPLOAD, true); + break; + default: + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->request->getMethod()); + } + + // set proxy, if needed + if ($host = $this->request->getConfig('proxy_host')) { + if (!($port = $this->request->getConfig('proxy_port'))) { + throw new HTTP_Request2_LogicException( + 'Proxy port not provided', HTTP_Request2_Exception::MISSING_VALUE + ); + } + curl_setopt($ch, CURLOPT_PROXY, $host . ':' . $port); + if ($user = $this->request->getConfig('proxy_user')) { + curl_setopt( + $ch, CURLOPT_PROXYUSERPWD, + $user . ':' . $this->request->getConfig('proxy_password') + ); + switch ($this->request->getConfig('proxy_auth_scheme')) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_DIGEST); + } + } + if ($type = $this->request->getConfig('proxy_type')) { + switch ($type) { + case 'http': + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + break; + case 'socks5': + curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5); + break; + default: + throw new HTTP_Request2_NotImplementedException( + "Proxy type '{$type}' is not supported" + ); + } + } + } + + // set authentication data + if ($auth = $this->request->getAuth()) { + curl_setopt($ch, CURLOPT_USERPWD, $auth['user'] . ':' . $auth['password']); + switch ($auth['scheme']) { + case HTTP_Request2::AUTH_BASIC: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + break; + case HTTP_Request2::AUTH_DIGEST: + curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_DIGEST); + } + } + + // set SSL options + foreach ($this->request->getConfig() as $name => $value) { + if ('ssl_verify_host' == $name && null !== $value) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $value? 2: 0); + } elseif (isset(self::$sslContextMap[$name]) && null !== $value) { + curl_setopt($ch, self::$sslContextMap[$name], $value); + } + } + + $headers = $this->request->getHeaders(); + // make cURL automagically send proper header + if (!isset($headers['accept-encoding'])) { + $headers['accept-encoding'] = ''; + } + + if (($jar = $this->request->getCookieJar()) + && ($cookies = $jar->getMatching($this->request->getUrl(), true)) + ) { + $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; + } + + // set headers having special cURL keys + foreach (self::$headerMap as $name => $option) { + if (isset($headers[$name])) { + curl_setopt($ch, $option, $headers[$name]); + unset($headers[$name]); + } + } + + $this->calculateRequestLength($headers); + if (isset($headers['content-length'])) { + $this->workaroundPhpBug47204($ch, $headers); + } + + // set headers not having special keys + $headersFmt = array(); + foreach ($headers as $name => $value) { + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + $headersFmt[] = $canonicalName . ': ' . $value; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headersFmt); + + return $ch; + } + + /** + * Workaround for PHP bug #47204 that prevents rewinding request body + * + * The workaround consists of reading the entire request body into memory + * and setting it as CURLOPT_POSTFIELDS, so it isn't recommended for large + * file uploads, use Socket adapter instead. + * + * @param resource $ch cURL handle + * @param array &$headers Request headers + */ + protected function workaroundPhpBug47204($ch, &$headers) + { + // no redirects, no digest auth -> probably no rewind needed + if (!$this->request->getConfig('follow_redirects') + && (!($auth = $this->request->getAuth()) + || HTTP_Request2::AUTH_DIGEST != $auth['scheme']) + ) { + curl_setopt($ch, CURLOPT_READFUNCTION, array($this, 'callbackReadBody')); + + } else { + // rewind may be needed, read the whole body into memory + if ($this->requestBody instanceof HTTP_Request2_MultipartBody) { + $this->requestBody = $this->requestBody->__toString(); + + } elseif (is_resource($this->requestBody)) { + $fp = $this->requestBody; + $this->requestBody = ''; + while (!feof($fp)) { + $this->requestBody .= fread($fp, 16384); + } + } + // curl hangs up if content-length is present + unset($headers['content-length']); + curl_setopt($ch, CURLOPT_POSTFIELDS, $this->requestBody); + } + } + + /** + * Callback function called by cURL for reading the request body + * + * @param resource $ch cURL handle + * @param resource $fd file descriptor (not used) + * @param integer $length maximum length of data to return + * + * @return string part of the request body, up to $length bytes + */ + protected function callbackReadBody($ch, $fd, $length) + { + if (!$this->eventSentHeaders) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + $this->eventSentHeaders = true; + } + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + || 0 == $this->contentLength || $this->position >= $this->contentLength + ) { + return ''; + } + if (is_string($this->requestBody)) { + $string = substr($this->requestBody, $this->position, $length); + } elseif (is_resource($this->requestBody)) { + $string = fread($this->requestBody, $length); + } else { + $string = $this->requestBody->read($length); + } + $this->request->setLastEvent('sentBodyPart', strlen($string)); + $this->position += strlen($string); + return $string; + } + + /** + * Callback function called by cURL for saving the response headers + * + * @param resource $ch cURL handle + * @param string $string response header (with trailing CRLF) + * + * @return integer number of bytes saved + * @see HTTP_Request2_Response::parseHeaderLine() + */ + protected function callbackWriteHeader($ch, $string) + { + // we may receive a second set of headers if doing e.g. digest auth + if ($this->eventReceivedHeaders || !$this->eventSentHeaders) { + // don't bother with 100-Continue responses (bug #15785) + if (!$this->eventSentHeaders + || $this->response->getStatus() >= 200 + ) { + $this->request->setLastEvent( + 'sentHeaders', curl_getinfo($ch, CURLINFO_HEADER_OUT) + ); + } + $upload = curl_getinfo($ch, CURLINFO_SIZE_UPLOAD); + // if body wasn't read by a callback, send event with total body size + if ($upload > $this->position) { + $this->request->setLastEvent( + 'sentBodyPart', $upload - $this->position + ); + $this->position = $upload; + } + if ($upload && (!$this->eventSentHeaders + || $this->response->getStatus() >= 200) + ) { + $this->request->setLastEvent('sentBody', $upload); + } + $this->eventSentHeaders = true; + // we'll need a new response object + if ($this->eventReceivedHeaders) { + $this->eventReceivedHeaders = false; + $this->response = null; + } + } + if (empty($this->response)) { + $this->response = new HTTP_Request2_Response( + $string, false, curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) + ); + } else { + $this->response->parseHeaderLine($string); + if ('' == trim($string)) { + // don't bother with 100-Continue responses (bug #15785) + if (200 <= $this->response->getStatus()) { + $this->request->setLastEvent('receivedHeaders', $this->response); + } + + if ($this->request->getConfig('follow_redirects') && $this->response->isRedirect()) { + $redirectUrl = new Net_URL2($this->response->getHeader('location')); + + // for versions lower than 5.2.10, check the redirection URL protocol + if (!defined('CURLOPT_REDIR_PROTOCOLS') && $redirectUrl->isAbsolute() + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + ) { + return -1; + } + + if ($jar = $this->request->getCookieJar()) { + $jar->addCookiesFromResponse($this->response, $this->request->getUrl()); + if (!$redirectUrl->isAbsolute()) { + $redirectUrl = $this->request->getUrl()->resolve($redirectUrl); + } + if ($cookies = $jar->getMatching($redirectUrl, true)) { + curl_setopt($ch, CURLOPT_COOKIE, $cookies); + } + } + } + $this->eventReceivedHeaders = true; + } + } + return strlen($string); + } + + /** + * Callback function called by cURL for saving the response body + * + * @param resource $ch cURL handle (not used) + * @param string $string part of the response body + * + * @return integer number of bytes saved + * @throws HTTP_Request2_MessageException + * @see HTTP_Request2_Response::appendBody() + */ + protected function callbackWriteBody($ch, $string) + { + // cURL calls WRITEFUNCTION callback without calling HEADERFUNCTION if + // response doesn't start with proper HTTP status line (see bug #15716) + if (empty($this->response)) { + throw new HTTP_Request2_MessageException( + "Malformed response: {$string}", + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } + if ($this->request->getConfig('store_body')) { + $this->response->appendBody($string); + } + $this->request->setLastEvent('receivedBodyPart', $string); + return strlen($string); + } +} +?> diff --git a/HTTP/Request2/Adapter/Mock.php b/HTTP/Request2/Adapter/Mock.php new file mode 100644 index 0000000..e2aaa8e --- /dev/null +++ b/HTTP/Request2/Adapter/Mock.php @@ -0,0 +1,189 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: Mock.php 324937 2012-04-07 10:05:57Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Base class for HTTP_Request2 adapters + */ +require_once 'HTTP/Request2/Adapter.php'; + +/** + * Mock adapter intended for testing + * + * Can be used to test applications depending on HTTP_Request2 package without + * actually performing any HTTP requests. This adapter will return responses + * previously added via addResponse() + * + * $mock = new HTTP_Request2_Adapter_Mock(); + * $mock->addResponse("HTTP/1.1 ... "); + * + * $request = new HTTP_Request2(); + * $request->setAdapter($mock); + * + * // This will return the response set above + * $response = $req->send(); + * + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 2.1.1 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_Adapter_Mock extends HTTP_Request2_Adapter +{ + /** + * A queue of responses to be returned by sendRequest() + * @var array + */ + protected $responses = array(); + + /** + * Returns the next response from the queue built by addResponse() + * + * Only responses without explicit URLs or with URLs equal to request URL + * will be considered. If matching response is not found or the queue is + * empty then default empty response with status 400 will be returned, + * if an Exception object was added to the queue it will be thrown. + * + * @param HTTP_Request2 $request HTTP request message + * + * @return HTTP_Request2_Response + * @throws Exception + */ + public function sendRequest(HTTP_Request2 $request) + { + $requestUrl = (string)$request->getUrl(); + $response = null; + foreach ($this->responses as $k => $v) { + if (!$v[1] || $requestUrl == $v[1]) { + $response = $v[0]; + array_splice($this->responses, $k, 1); + break; + } + } + if (!$response) { + return self::createResponseFromString("HTTP/1.1 400 Bad Request\r\n\r\n"); + + } elseif ($response instanceof HTTP_Request2_Response) { + return $response; + + } else { + // rethrow the exception + $class = get_class($response); + $message = $response->getMessage(); + $code = $response->getCode(); + throw new $class($message, $code); + } + } + + /** + * Adds response to the queue + * + * @param mixed $response either a string, a pointer to an open file, + * an instance of HTTP_Request2_Response or Exception + * @param string $url A request URL this response should be valid for + * (see {@link http://pear.php.net/bugs/bug.php?id=19276}) + * + * @throws HTTP_Request2_Exception + */ + public function addResponse($response, $url = null) + { + if (is_string($response)) { + $response = self::createResponseFromString($response); + } elseif (is_resource($response)) { + $response = self::createResponseFromFile($response); + } elseif (!$response instanceof HTTP_Request2_Response && + !$response instanceof Exception + ) { + throw new HTTP_Request2_Exception('Parameter is not a valid response'); + } + $this->responses[] = array($response, $url); + } + + /** + * Creates a new HTTP_Request2_Response object from a string + * + * @param string $str string containing HTTP response message + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromString($str) + { + $parts = preg_split('!(\r?\n){2}!m', $str, 2); + $headerLines = explode("\n", $parts[0]); + $response = new HTTP_Request2_Response(array_shift($headerLines)); + foreach ($headerLines as $headerLine) { + $response->parseHeaderLine($headerLine); + } + $response->parseHeaderLine(''); + if (isset($parts[1])) { + $response->appendBody($parts[1]); + } + return $response; + } + + /** + * Creates a new HTTP_Request2_Response object from a file + * + * @param resource $fp file pointer returned by fopen() + * + * @return HTTP_Request2_Response + * @throws HTTP_Request2_Exception + */ + public static function createResponseFromFile($fp) + { + $response = new HTTP_Request2_Response(fgets($fp)); + do { + $headerLine = fgets($fp); + $response->parseHeaderLine($headerLine); + } while ('' != trim($headerLine)); + + while (!feof($fp)) { + $response->appendBody(fread($fp, 8192)); + } + return $response; + } +} +?> \ No newline at end of file diff --git a/HTTP/Request2/Adapter/Socket.php b/HTTP/Request2/Adapter/Socket.php new file mode 100644 index 0000000..670dbc0 --- /dev/null +++ b/HTTP/Request2/Adapter/Socket.php @@ -0,0 +1,2031 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Socket.php 324953 2012-04-08 07:24:12Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** Base class for HTTP_Request2 adapters */ + +require_once 'HTTP/Request2/Adapter.php'; + + + +/** Socket wrapper class */ + +require_once 'HTTP/Request2/SocketWrapper.php'; + + + +/** + + * Socket-based adapter for HTTP_Request2 + + * + + * This adapter uses only PHP sockets and will work on almost any PHP + + * environment. Code is based on original HTTP_Request PEAR package. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_Adapter_Socket extends HTTP_Request2_Adapter + +{ + + /** + + * Regular expression for 'token' rule from RFC 2616 + + */ + + const REGEXP_TOKEN = '[^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+'; + + + + /** + + * Regular expression for 'quoted-string' rule from RFC 2616 + + */ + + const REGEXP_QUOTED_STRING = '"(?:\\\\.|[^\\\\"])*"'; + + + + /** + + * Connected sockets, needed for Keep-Alive support + + * @var array + + * @see connect() + + */ + + protected static $sockets = array(); + + + + /** + + * Data for digest authentication scheme + + * + + * The keys for the array are URL prefixes. + + * + + * The values are associative arrays with data (realm, nonce, nonce-count, + + * opaque...) needed for digest authentication. Stored here to prevent making + + * duplicate requests to digest-protected resources after we have already + + * received the challenge. + + * + + * @var array + + */ + + protected static $challenges = array(); + + + + /** + + * Connected socket + + * @var HTTP_Request2_SocketWrapper + + * @see connect() + + */ + + protected $socket; + + + + /** + + * Challenge used for server digest authentication + + * @var array + + */ + + protected $serverChallenge; + + + + /** + + * Challenge used for proxy digest authentication + + * @var array + + */ + + protected $proxyChallenge; + + + + /** + + * Remaining length of the current chunk, when reading chunked response + + * @var integer + + * @see readChunked() + + */ + + protected $chunkLength = 0; + + + + /** + + * Remaining amount of redirections to follow + + * + + * Starts at 'max_redirects' configuration parameter and is reduced on each + + * subsequent redirect. An Exception will be thrown once it reaches zero. + + * + + * @var integer + + */ + + protected $redirectCountdown = null; + + + + /** + + * Sends request to the remote server and returns its response + + * + + * @param HTTP_Request2 $request HTTP request message + + * + + * @return HTTP_Request2_Response + + * @throws HTTP_Request2_Exception + + */ + + public function sendRequest(HTTP_Request2 $request) + + { + + $this->request = $request; + + + + try { + + $keepAlive = $this->connect(); + + $headers = $this->prepareHeaders(); + + $this->socket->write($headers); + + // provide request headers to the observer, see request #7633 + + $this->request->setLastEvent('sentHeaders', $headers); + + $this->writeBody(); + + + + $response = $this->readResponse(); + + + + if ($jar = $request->getCookieJar()) { + + $jar->addCookiesFromResponse($response, $request->getUrl()); + + } + + + + if (!$this->canKeepAlive($keepAlive, $response)) { + + $this->disconnect(); + + } + + + + if ($this->shouldUseProxyDigestAuth($response)) { + + return $this->sendRequest($request); + + } + + if ($this->shouldUseServerDigestAuth($response)) { + + return $this->sendRequest($request); + + } + + if ($authInfo = $response->getHeader('authentication-info')) { + + $this->updateChallenge($this->serverChallenge, $authInfo); + + } + + if ($proxyInfo = $response->getHeader('proxy-authentication-info')) { + + $this->updateChallenge($this->proxyChallenge, $proxyInfo); + + } + + + + } catch (Exception $e) { + + $this->disconnect(); + + } + + + + unset($this->request, $this->requestBody); + + + + if (!empty($e)) { + + $this->redirectCountdown = null; + + throw $e; + + } + + + + if (!$request->getConfig('follow_redirects') || !$response->isRedirect()) { + + $this->redirectCountdown = null; + + return $response; + + } else { + + return $this->handleRedirect($request, $response); + + } + + } + + + + /** + + * Connects to the remote server + + * + + * @return bool whether the connection can be persistent + + * @throws HTTP_Request2_Exception + + */ + + protected function connect() + + { + + $secure = 0 == strcasecmp($this->request->getUrl()->getScheme(), 'https'); + + $tunnel = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + + $headers = $this->request->getHeaders(); + + $reqHost = $this->request->getUrl()->getHost(); + + if (!($reqPort = $this->request->getUrl()->getPort())) { + + $reqPort = $secure? 443: 80; + + } + + + + $httpProxy = $socksProxy = false; + + if (!($host = $this->request->getConfig('proxy_host'))) { + + $host = $reqHost; + + $port = $reqPort; + + } else { + + if (!($port = $this->request->getConfig('proxy_port'))) { + + throw new HTTP_Request2_LogicException( + + 'Proxy port not provided', + + HTTP_Request2_Exception::MISSING_VALUE + + ); + + } + + if ('http' == ($type = $this->request->getConfig('proxy_type'))) { + + $httpProxy = true; + + } elseif ('socks5' == $type) { + + $socksProxy = true; + + } else { + + throw new HTTP_Request2_NotImplementedException( + + "Proxy type '{$type}' is not supported" + + ); + + } + + } + + + + if ($tunnel && !$httpProxy) { + + throw new HTTP_Request2_LogicException( + + "Trying to perform CONNECT request without proxy", + + HTTP_Request2_Exception::MISSING_VALUE + + ); + + } + + if ($secure && !in_array('ssl', stream_get_transports())) { + + throw new HTTP_Request2_LogicException( + + 'Need OpenSSL support for https:// requests', + + HTTP_Request2_Exception::MISCONFIGURATION + + ); + + } + + + + // RFC 2068, section 19.7.1: A client MUST NOT send the Keep-Alive + + // connection token to a proxy server... + + if ($httpProxy && !$secure && !empty($headers['connection']) + + && 'Keep-Alive' == $headers['connection'] + + ) { + + $this->request->setHeader('connection'); + + } + + + + $keepAlive = ('1.1' == $this->request->getConfig('protocol_version') && + + empty($headers['connection'])) || + + (!empty($headers['connection']) && + + 'Keep-Alive' == $headers['connection']); + + + + $options = array(); + + if ($secure || $tunnel) { + + foreach ($this->request->getConfig() as $name => $value) { + + if ('ssl_' == substr($name, 0, 4) && null !== $value) { + + if ('ssl_verify_host' == $name) { + + if ($value) { + + $options['CN_match'] = $reqHost; + + } + + } else { + + $options[substr($name, 4)] = $value; + + } + + } + + } + + ksort($options); + + } + + + + // Use global request timeout if given, see feature requests #5735, #8964 + + if ($timeout = $this->request->getConfig('timeout')) { + + $deadline = time() + $timeout; + + } else { + + $deadline = null; + + } + + + + // Changing SSL context options after connection is established does *not* + + // work, we need a new connection if options change + + $remote = ((!$secure || $httpProxy || $socksProxy)? 'tcp://': 'ssl://') + + . $host . ':' . $port; + + $socketKey = $remote . ( + + ($secure && $httpProxy || $socksProxy) + + ? "->{$reqHost}:{$reqPort}" : '' + + ) . (empty($options)? '': ':' . serialize($options)); + + unset($this->socket); + + + + // We use persistent connections and have a connected socket? + + // Ensure that the socket is still connected, see bug #16149 + + if ($keepAlive && !empty(self::$sockets[$socketKey]) + + && !self::$sockets[$socketKey]->eof() + + ) { + + $this->socket =& self::$sockets[$socketKey]; + + + + } else { + + if ($socksProxy) { + + require_once 'HTTP/Request2/SOCKS5.php'; + + + + $this->socket = new HTTP_Request2_SOCKS5( + + $remote, $this->request->getConfig('connect_timeout'), + + $options, $this->request->getConfig('proxy_user'), + + $this->request->getConfig('proxy_password') + + ); + + // handle request timeouts ASAP + + $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); + + $this->socket->connect($reqHost, $reqPort); + + if (!$secure) { + + $conninfo = "tcp://{$reqHost}:{$reqPort} via {$remote}"; + + } else { + + $this->socket->enableCrypto(); + + $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; + + } + + + + } elseif ($secure && $httpProxy && !$tunnel) { + + $this->establishTunnel(); + + $conninfo = "ssl://{$reqHost}:{$reqPort} via {$remote}"; + + + + } else { + + $this->socket = new HTTP_Request2_SocketWrapper( + + $remote, $this->request->getConfig('connect_timeout'), $options + + ); + + } + + $this->request->setLastEvent('connect', empty($conninfo)? $remote: $conninfo); + + self::$sockets[$socketKey] =& $this->socket; + + } + + $this->socket->setDeadline($deadline, $this->request->getConfig('timeout')); + + return $keepAlive; + + } + + + + /** + + * Establishes a tunnel to a secure remote server via HTTP CONNECT request + + * + + * This method will fail if 'ssl_verify_peer' is enabled. Probably because PHP + + * sees that we are connected to a proxy server (duh!) rather than the server + + * that presents its certificate. + + * + + * @link http://tools.ietf.org/html/rfc2817#section-5.2 + + * @throws HTTP_Request2_Exception + + */ + + protected function establishTunnel() + + { + + $donor = new self; + + $connect = new HTTP_Request2( + + $this->request->getUrl(), HTTP_Request2::METHOD_CONNECT, + + array_merge($this->request->getConfig(), array('adapter' => $donor)) + + ); + + $response = $connect->send(); + + // Need any successful (2XX) response + + if (200 > $response->getStatus() || 300 <= $response->getStatus()) { + + throw new HTTP_Request2_ConnectionException( + + 'Failed to connect via HTTPS proxy. Proxy response: ' . + + $response->getStatus() . ' ' . $response->getReasonPhrase() + + ); + + } + + $this->socket = $donor->socket; + + $this->socket->enableCrypto(); + + } + + + + /** + + * Checks whether current connection may be reused or should be closed + + * + + * @param boolean $requestKeepAlive whether connection could + + * be persistent in the first place + + * @param HTTP_Request2_Response $response response object to check + + * + + * @return boolean + + */ + + protected function canKeepAlive($requestKeepAlive, HTTP_Request2_Response $response) + + { + + // Do not close socket on successful CONNECT request + + if (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() + + && 200 <= $response->getStatus() && 300 > $response->getStatus() + + ) { + + return true; + + } + + + + $lengthKnown = 'chunked' == strtolower($response->getHeader('transfer-encoding')) + + || null !== $response->getHeader('content-length') + + // no body possible for such responses, see also request #17031 + + || HTTP_Request2::METHOD_HEAD == $this->request->getMethod() + + || in_array($response->getStatus(), array(204, 304)); + + $persistent = 'keep-alive' == strtolower($response->getHeader('connection')) || + + (null === $response->getHeader('connection') && + + '1.1' == $response->getVersion()); + + return $requestKeepAlive && $lengthKnown && $persistent; + + } + + + + /** + + * Disconnects from the remote server + + */ + + protected function disconnect() + + { + + if (!empty($this->socket)) { + + $this->socket = null; + + $this->request->setLastEvent('disconnect'); + + } + + } + + + + /** + + * Handles HTTP redirection + + * + + * This method will throw an Exception if redirect to a non-HTTP(S) location + + * is attempted, also if number of redirects performed already is equal to + + * 'max_redirects' configuration parameter. + + * + + * @param HTTP_Request2 $request Original request + + * @param HTTP_Request2_Response $response Response containing redirect + + * + + * @return HTTP_Request2_Response Response from a new location + + * @throws HTTP_Request2_Exception + + */ + + protected function handleRedirect( + + HTTP_Request2 $request, HTTP_Request2_Response $response + + ) { + + if (is_null($this->redirectCountdown)) { + + $this->redirectCountdown = $request->getConfig('max_redirects'); + + } + + if (0 == $this->redirectCountdown) { + + $this->redirectCountdown = null; + + // Copying cURL behaviour + + throw new HTTP_Request2_MessageException( + + 'Maximum (' . $request->getConfig('max_redirects') . ') redirects followed', + + HTTP_Request2_Exception::TOO_MANY_REDIRECTS + + ); + + } + + $redirectUrl = new Net_URL2( + + $response->getHeader('location'), + + array(Net_URL2::OPTION_USE_BRACKETS => $request->getConfig('use_brackets')) + + ); + + // refuse non-HTTP redirect + + if ($redirectUrl->isAbsolute() + + && !in_array($redirectUrl->getScheme(), array('http', 'https')) + + ) { + + $this->redirectCountdown = null; + + throw new HTTP_Request2_MessageException( + + 'Refusing to redirect to a non-HTTP URL ' . $redirectUrl->__toString(), + + HTTP_Request2_Exception::NON_HTTP_REDIRECT + + ); + + } + + // Theoretically URL should be absolute (see http://tools.ietf.org/html/rfc2616#section-14.30), + + // but in practice it is often not + + if (!$redirectUrl->isAbsolute()) { + + $redirectUrl = $request->getUrl()->resolve($redirectUrl); + + } + + $redirect = clone $request; + + $redirect->setUrl($redirectUrl); + + if (303 == $response->getStatus() + + || (!$request->getConfig('strict_redirects') + + && in_array($response->getStatus(), array(301, 302))) + + ) { + + $redirect->setMethod(HTTP_Request2::METHOD_GET); + + $redirect->setBody(''); + + } + + + + if (0 < $this->redirectCountdown) { + + $this->redirectCountdown--; + + } + + return $this->sendRequest($redirect); + + } + + + + /** + + * Checks whether another request should be performed with server digest auth + + * + + * Several conditions should be satisfied for it to return true: + + * - response status should be 401 + + * - auth credentials should be set in the request object + + * - response should contain WWW-Authenticate header with digest challenge + + * - there is either no challenge stored for this URL or new challenge + + * contains stale=true parameter (in other case we probably just failed + + * due to invalid username / password) + + * + + * The method stores challenge values in $challenges static property + + * + + * @param HTTP_Request2_Response $response response to check + + * + + * @return boolean whether another request should be performed + + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + + */ + + protected function shouldUseServerDigestAuth(HTTP_Request2_Response $response) + + { + + // no sense repeating a request if we don't have credentials + + if (401 != $response->getStatus() || !$this->request->getAuth()) { + + return false; + + } + + if (!$challenge = $this->parseDigestChallenge($response->getHeader('www-authenticate'))) { + + return false; + + } + + + + $url = $this->request->getUrl(); + + $scheme = $url->getScheme(); + + $host = $scheme . '://' . $url->getHost(); + + if ($port = $url->getPort()) { + + if ((0 == strcasecmp($scheme, 'http') && 80 != $port) + + || (0 == strcasecmp($scheme, 'https') && 443 != $port) + + ) { + + $host .= ':' . $port; + + } + + } + + + + if (!empty($challenge['domain'])) { + + $prefixes = array(); + + foreach (preg_split('/\\s+/', $challenge['domain']) as $prefix) { + + // don't bother with different servers + + if ('/' == substr($prefix, 0, 1)) { + + $prefixes[] = $host . $prefix; + + } + + } + + } + + if (empty($prefixes)) { + + $prefixes = array($host . '/'); + + } + + + + $ret = true; + + foreach ($prefixes as $prefix) { + + if (!empty(self::$challenges[$prefix]) + + && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + + ) { + + // probably credentials are invalid + + $ret = false; + + } + + self::$challenges[$prefix] =& $challenge; + + } + + return $ret; + + } + + + + /** + + * Checks whether another request should be performed with proxy digest auth + + * + + * Several conditions should be satisfied for it to return true: + + * - response status should be 407 + + * - proxy auth credentials should be set in the request object + + * - response should contain Proxy-Authenticate header with digest challenge + + * - there is either no challenge stored for this proxy or new challenge + + * contains stale=true parameter (in other case we probably just failed + + * due to invalid username / password) + + * + + * The method stores challenge values in $challenges static property + + * + + * @param HTTP_Request2_Response $response response to check + + * + + * @return boolean whether another request should be performed + + * @throws HTTP_Request2_Exception in case of unsupported challenge parameters + + */ + + protected function shouldUseProxyDigestAuth(HTTP_Request2_Response $response) + + { + + if (407 != $response->getStatus() || !$this->request->getConfig('proxy_user')) { + + return false; + + } + + if (!($challenge = $this->parseDigestChallenge($response->getHeader('proxy-authenticate')))) { + + return false; + + } + + + + $key = 'proxy://' . $this->request->getConfig('proxy_host') . + + ':' . $this->request->getConfig('proxy_port'); + + + + if (!empty(self::$challenges[$key]) + + && (empty($challenge['stale']) || strcasecmp('true', $challenge['stale'])) + + ) { + + $ret = false; + + } else { + + $ret = true; + + } + + self::$challenges[$key] = $challenge; + + return $ret; + + } + + + + /** + + * Extracts digest method challenge from (WWW|Proxy)-Authenticate header value + + * + + * There is a problem with implementation of RFC 2617: several of the parameters + + * are defined as quoted-string there and thus may contain backslash escaped + + * double quotes (RFC 2616, section 2.2). However, RFC 2617 defines unq(X) as + + * just value of quoted-string X without surrounding quotes, it doesn't speak + + * about removing backslash escaping. + + * + + * Now realm parameter is user-defined and human-readable, strange things + + * happen when it contains quotes: + + * - Apache allows quotes in realm, but apparently uses realm value without + + * backslashes for digest computation + + * - Squid allows (manually escaped) quotes there, but it is impossible to + + * authorize with either escaped or unescaped quotes used in digest, + + * probably it can't parse the response (?) + + * - Both IE and Firefox display realm value with backslashes in + + * the password popup and apparently use the same value for digest + + * + + * HTTP_Request2 follows IE and Firefox (and hopefully RFC 2617) in + + * quoted-string handling, unfortunately that means failure to authorize + + * sometimes + + * + + * @param string $headerValue value of WWW-Authenticate or Proxy-Authenticate header + + * + + * @return mixed associative array with challenge parameters, false if + + * no challenge is present in header value + + * @throws HTTP_Request2_NotImplementedException in case of unsupported challenge parameters + + */ + + protected function parseDigestChallenge($headerValue) + + { + + $authParam = '(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')'; + + $challenge = "!(?<=^|\\s|,)Digest ({$authParam}\\s*(,\\s*|$))+!"; + + if (!preg_match($challenge, $headerValue, $matches)) { + + return false; + + } + + + + preg_match_all('!' . $authParam . '!', $matches[0], $params); + + $paramsAry = array(); + + $knownParams = array('realm', 'domain', 'nonce', 'opaque', 'stale', + + 'algorithm', 'qop'); + + for ($i = 0; $i < count($params[0]); $i++) { + + // section 3.2.1: Any unrecognized directive MUST be ignored. + + if (in_array($params[1][$i], $knownParams)) { + + if ('"' == substr($params[2][$i], 0, 1)) { + + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + + } else { + + $paramsAry[$params[1][$i]] = $params[2][$i]; + + } + + } + + } + + // we only support qop=auth + + if (!empty($paramsAry['qop']) + + && !in_array('auth', array_map('trim', explode(',', $paramsAry['qop']))) + + ) { + + throw new HTTP_Request2_NotImplementedException( + + "Only 'auth' qop is currently supported in digest authentication, " . + + "server requested '{$paramsAry['qop']}'" + + ); + + } + + // we only support algorithm=MD5 + + if (!empty($paramsAry['algorithm']) && 'MD5' != $paramsAry['algorithm']) { + + throw new HTTP_Request2_NotImplementedException( + + "Only 'MD5' algorithm is currently supported in digest authentication, " . + + "server requested '{$paramsAry['algorithm']}'" + + ); + + } + + + + return $paramsAry; + + } + + + + /** + + * Parses [Proxy-]Authentication-Info header value and updates challenge + + * + + * @param array &$challenge challenge to update + + * @param string $headerValue value of [Proxy-]Authentication-Info header + + * + + * @todo validate server rspauth response + + */ + + protected function updateChallenge(&$challenge, $headerValue) + + { + + $authParam = '!(' . self::REGEXP_TOKEN . ')\\s*=\\s*(' . + + self::REGEXP_TOKEN . '|' . self::REGEXP_QUOTED_STRING . ')!'; + + $paramsAry = array(); + + + + preg_match_all($authParam, $headerValue, $params); + + for ($i = 0; $i < count($params[0]); $i++) { + + if ('"' == substr($params[2][$i], 0, 1)) { + + $paramsAry[$params[1][$i]] = substr($params[2][$i], 1, -1); + + } else { + + $paramsAry[$params[1][$i]] = $params[2][$i]; + + } + + } + + // for now, just update the nonce value + + if (!empty($paramsAry['nextnonce'])) { + + $challenge['nonce'] = $paramsAry['nextnonce']; + + $challenge['nc'] = 1; + + } + + } + + + + /** + + * Creates a value for [Proxy-]Authorization header when using digest authentication + + * + + * @param string $user user name + + * @param string $password password + + * @param string $url request URL + + * @param array &$challenge digest challenge parameters + + * + + * @return string value of [Proxy-]Authorization request header + + * @link http://tools.ietf.org/html/rfc2617#section-3.2.2 + + */ + + protected function createDigestResponse($user, $password, $url, &$challenge) + + { + + if (false !== ($q = strpos($url, '?')) + + && $this->request->getConfig('digest_compat_ie') + + ) { + + $url = substr($url, 0, $q); + + } + + + + $a1 = md5($user . ':' . $challenge['realm'] . ':' . $password); + + $a2 = md5($this->request->getMethod() . ':' . $url); + + + + if (empty($challenge['qop'])) { + + $digest = md5($a1 . ':' . $challenge['nonce'] . ':' . $a2); + + } else { + + $challenge['cnonce'] = 'Req2.' . rand(); + + if (empty($challenge['nc'])) { + + $challenge['nc'] = 1; + + } + + $nc = sprintf('%08x', $challenge['nc']++); + + $digest = md5( + + $a1 . ':' . $challenge['nonce'] . ':' . $nc . ':' . + + $challenge['cnonce'] . ':auth:' . $a2 + + ); + + } + + return 'Digest username="' . str_replace(array('\\', '"'), array('\\\\', '\\"'), $user) . '", ' . + + 'realm="' . $challenge['realm'] . '", ' . + + 'nonce="' . $challenge['nonce'] . '", ' . + + 'uri="' . $url . '", ' . + + 'response="' . $digest . '"' . + + (!empty($challenge['opaque'])? + + ', opaque="' . $challenge['opaque'] . '"': + + '') . + + (!empty($challenge['qop'])? + + ', qop="auth", nc=' . $nc . ', cnonce="' . $challenge['cnonce'] . '"': + + ''); + + } + + + + /** + + * Adds 'Authorization' header (if needed) to request headers array + + * + + * @param array &$headers request headers + + * @param string $requestHost request host (needed for digest authentication) + + * @param string $requestUrl request URL (needed for digest authentication) + + * + + * @throws HTTP_Request2_NotImplementedException + + */ + + protected function addAuthorizationHeader(&$headers, $requestHost, $requestUrl) + + { + + if (!($auth = $this->request->getAuth())) { + + return; + + } + + switch ($auth['scheme']) { + + case HTTP_Request2::AUTH_BASIC: + + $headers['authorization'] = 'Basic ' . base64_encode( + + $auth['user'] . ':' . $auth['password'] + + ); + + break; + + + + case HTTP_Request2::AUTH_DIGEST: + + unset($this->serverChallenge); + + $fullUrl = ('/' == $requestUrl[0])? + + $this->request->getUrl()->getScheme() . '://' . + + $requestHost . $requestUrl: + + $requestUrl; + + foreach (array_keys(self::$challenges) as $key) { + + if ($key == substr($fullUrl, 0, strlen($key))) { + + $headers['authorization'] = $this->createDigestResponse( + + $auth['user'], $auth['password'], + + $requestUrl, self::$challenges[$key] + + ); + + $this->serverChallenge =& self::$challenges[$key]; + + break; + + } + + } + + break; + + + + default: + + throw new HTTP_Request2_NotImplementedException( + + "Unknown HTTP authentication scheme '{$auth['scheme']}'" + + ); + + } + + } + + + + /** + + * Adds 'Proxy-Authorization' header (if needed) to request headers array + + * + + * @param array &$headers request headers + + * @param string $requestUrl request URL (needed for digest authentication) + + * + + * @throws HTTP_Request2_NotImplementedException + + */ + + protected function addProxyAuthorizationHeader(&$headers, $requestUrl) + + { + + if (!$this->request->getConfig('proxy_host') + + || !($user = $this->request->getConfig('proxy_user')) + + || (0 == strcasecmp('https', $this->request->getUrl()->getScheme()) + + && HTTP_Request2::METHOD_CONNECT != $this->request->getMethod()) + + ) { + + return; + + } + + + + $password = $this->request->getConfig('proxy_password'); + + switch ($this->request->getConfig('proxy_auth_scheme')) { + + case HTTP_Request2::AUTH_BASIC: + + $headers['proxy-authorization'] = 'Basic ' . base64_encode( + + $user . ':' . $password + + ); + + break; + + + + case HTTP_Request2::AUTH_DIGEST: + + unset($this->proxyChallenge); + + $proxyUrl = 'proxy://' . $this->request->getConfig('proxy_host') . + + ':' . $this->request->getConfig('proxy_port'); + + if (!empty(self::$challenges[$proxyUrl])) { + + $headers['proxy-authorization'] = $this->createDigestResponse( + + $user, $password, + + $requestUrl, self::$challenges[$proxyUrl] + + ); + + $this->proxyChallenge =& self::$challenges[$proxyUrl]; + + } + + break; + + + + default: + + throw new HTTP_Request2_NotImplementedException( + + "Unknown HTTP authentication scheme '" . + + $this->request->getConfig('proxy_auth_scheme') . "'" + + ); + + } + + } + + + + + + /** + + * Creates the string with the Request-Line and request headers + + * + + * @return string + + * @throws HTTP_Request2_Exception + + */ + + protected function prepareHeaders() + + { + + $headers = $this->request->getHeaders(); + + $url = $this->request->getUrl(); + + $connect = HTTP_Request2::METHOD_CONNECT == $this->request->getMethod(); + + $host = $url->getHost(); + + + + $defaultPort = 0 == strcasecmp($url->getScheme(), 'https')? 443: 80; + + if (($port = $url->getPort()) && $port != $defaultPort || $connect) { + + $host .= ':' . (empty($port)? $defaultPort: $port); + + } + + // Do not overwrite explicitly set 'Host' header, see bug #16146 + + if (!isset($headers['host'])) { + + $headers['host'] = $host; + + } + + + + if ($connect) { + + $requestUrl = $host; + + + + } else { + + if (!$this->request->getConfig('proxy_host') + + || 'http' != $this->request->getConfig('proxy_type') + + || 0 == strcasecmp($url->getScheme(), 'https') + + ) { + + $requestUrl = ''; + + } else { + + $requestUrl = $url->getScheme() . '://' . $host; + + } + + $path = $url->getPath(); + + $query = $url->getQuery(); + + $requestUrl .= (empty($path)? '/': $path) . (empty($query)? '': '?' . $query); + + } + + + + if ('1.1' == $this->request->getConfig('protocol_version') + + && extension_loaded('zlib') && !isset($headers['accept-encoding']) + + ) { + + $headers['accept-encoding'] = 'gzip, deflate'; + + } + + if (($jar = $this->request->getCookieJar()) + + && ($cookies = $jar->getMatching($this->request->getUrl(), true)) + + ) { + + $headers['cookie'] = (empty($headers['cookie'])? '': $headers['cookie'] . '; ') . $cookies; + + } + + + + $this->addAuthorizationHeader($headers, $host, $requestUrl); + + $this->addProxyAuthorizationHeader($headers, $requestUrl); + + $this->calculateRequestLength($headers); + + + + $headersStr = $this->request->getMethod() . ' ' . $requestUrl . ' HTTP/' . + + $this->request->getConfig('protocol_version') . "\r\n"; + + foreach ($headers as $name => $value) { + + $canonicalName = implode('-', array_map('ucfirst', explode('-', $name))); + + $headersStr .= $canonicalName . ': ' . $value . "\r\n"; + + } + + return $headersStr . "\r\n"; + + } + + + + /** + + * Sends the request body + + * + + * @throws HTTP_Request2_MessageException + + */ + + protected function writeBody() + + { + + if (in_array($this->request->getMethod(), self::$bodyDisallowed) + + || 0 == $this->contentLength + + ) { + + return; + + } + + + + $position = 0; + + $bufferSize = $this->request->getConfig('buffer_size'); + + while ($position < $this->contentLength) { + + if (is_string($this->requestBody)) { + + $str = substr($this->requestBody, $position, $bufferSize); + + } elseif (is_resource($this->requestBody)) { + + $str = fread($this->requestBody, $bufferSize); + + } else { + + $str = $this->requestBody->read($bufferSize); + + } + + $this->socket->write($str); + + // Provide the length of written string to the observer, request #7630 + + $this->request->setLastEvent('sentBodyPart', strlen($str)); + + $position += strlen($str); + + } + + $this->request->setLastEvent('sentBody', $this->contentLength); + + } + + + + /** + + * Reads the remote server's response + + * + + * @return HTTP_Request2_Response + + * @throws HTTP_Request2_Exception + + */ + + protected function readResponse() + + { + + $bufferSize = $this->request->getConfig('buffer_size'); + + + + do { + + $response = new HTTP_Request2_Response( + + $this->socket->readLine($bufferSize), true, $this->request->getUrl() + + ); + + do { + + $headerLine = $this->socket->readLine($bufferSize); + + $response->parseHeaderLine($headerLine); + + } while ('' != $headerLine); + + } while (in_array($response->getStatus(), array(100, 101))); + + + + $this->request->setLastEvent('receivedHeaders', $response); + + + + // No body possible in such responses + + if (HTTP_Request2::METHOD_HEAD == $this->request->getMethod() + + || (HTTP_Request2::METHOD_CONNECT == $this->request->getMethod() + + && 200 <= $response->getStatus() && 300 > $response->getStatus()) + + || in_array($response->getStatus(), array(204, 304)) + + ) { + + return $response; + + } + + + + $chunked = 'chunked' == $response->getHeader('transfer-encoding'); + + $length = $response->getHeader('content-length'); + + $hasBody = false; + + if ($chunked || null === $length || 0 < intval($length)) { + + // RFC 2616, section 4.4: + + // 3. ... If a message is received with both a + + // Transfer-Encoding header field and a Content-Length header field, + + // the latter MUST be ignored. + + $toRead = ($chunked || null === $length)? null: $length; + + $this->chunkLength = 0; + + + + while (!$this->socket->eof() && (is_null($toRead) || 0 < $toRead)) { + + if ($chunked) { + + $data = $this->readChunked($bufferSize); + + } elseif (is_null($toRead)) { + + $data = $this->socket->read($bufferSize); + + } else { + + $data = $this->socket->read(min($toRead, $bufferSize)); + + $toRead -= strlen($data); + + } + + if ('' == $data && (!$this->chunkLength || $this->socket->eof())) { + + break; + + } + + + + $hasBody = true; + + if ($this->request->getConfig('store_body')) { + + $response->appendBody($data); + + } + + if (!in_array($response->getHeader('content-encoding'), array('identity', null))) { + + $this->request->setLastEvent('receivedEncodedBodyPart', $data); + + } else { + + $this->request->setLastEvent('receivedBodyPart', $data); + + } + + } + + } + + + + if ($hasBody) { + + $this->request->setLastEvent('receivedBody', $response); + + } + + return $response; + + } + + + + /** + + * Reads a part of response body encoded with chunked Transfer-Encoding + + * + + * @param int $bufferSize buffer size to use for reading + + * + + * @return string + + * @throws HTTP_Request2_MessageException + + */ + + protected function readChunked($bufferSize) + + { + + // at start of the next chunk? + + if (0 == $this->chunkLength) { + + $line = $this->socket->readLine($bufferSize); + + if (!preg_match('/^([0-9a-f]+)/i', $line, $matches)) { + + throw new HTTP_Request2_MessageException( + + "Cannot decode chunked response, invalid chunk length '{$line}'", + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } else { + + $this->chunkLength = hexdec($matches[1]); + + // Chunk with zero length indicates the end + + if (0 == $this->chunkLength) { + + $this->socket->readLine($bufferSize); + + return ''; + + } + + } + + } + + $data = $this->socket->read(min($this->chunkLength, $bufferSize)); + + $this->chunkLength -= strlen($data); + + if (0 == $this->chunkLength) { + + $this->socket->readLine($bufferSize); // Trailing CRLF + + } + + return $data; + + } + +} + + + +?> \ No newline at end of file diff --git a/HTTP/Request2/CookieJar.php b/HTTP/Request2/CookieJar.php new file mode 100644 index 0000000..68c3314 --- /dev/null +++ b/HTTP/Request2/CookieJar.php @@ -0,0 +1,517 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: CookieJar.php 324415 2012-03-21 10:50:50Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Class representing a HTTP request message */ +require_once 'HTTP/Request2.php'; + +/** + * Stores cookies and passes them between HTTP requests + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 2.1.1 + * @link http://pear.php.net/package/HTTP_Request2 + */ +class HTTP_Request2_CookieJar implements Serializable +{ + /** + * Array of stored cookies + * + * The array is indexed by domain, path and cookie name + * .example.com + * / + * some_cookie => cookie data + * /subdir + * other_cookie => cookie data + * .example.org + * ... + * + * @var array + */ + protected $cookies = array(); + + /** + * Whether session cookies should be serialized when serializing the jar + * @var bool + */ + protected $serializeSession = false; + + /** + * Whether Public Suffix List should be used for domain matching + * @var bool + */ + protected $useList = true; + + /** + * Array with Public Suffix List data + * @var array + * @link http://publicsuffix.org/ + */ + protected static $psl = array(); + + /** + * Class constructor, sets various options + * + * @param bool $serializeSessionCookies Controls serializing session cookies, + * see {@link serializeSessionCookies()} + * @param bool $usePublicSuffixList Controls using Public Suffix List, + * see {@link usePublicSuffixList()} + */ + public function __construct( + $serializeSessionCookies = false, $usePublicSuffixList = true + ) { + $this->serializeSessionCookies($serializeSessionCookies); + $this->usePublicSuffixList($usePublicSuffixList); + } + + /** + * Returns current time formatted in ISO-8601 at UTC timezone + * + * @return string + */ + protected function now() + { + $dt = new DateTime(); + $dt->setTimezone(new DateTimeZone('UTC')); + return $dt->format(DateTime::ISO8601); + } + + /** + * Checks cookie array for correctness, possibly updating its 'domain', 'path' and 'expires' fields + * + * The checks are as follows: + * - cookie array should contain 'name' and 'value' fields; + * - name and value should not contain disallowed symbols; + * - 'expires' should be either empty parseable by DateTime; + * - 'domain' and 'path' should be either not empty or an URL where + * cookie was set should be provided. + * - if $setter is provided, then document at that URL should be allowed + * to set a cookie for that 'domain'. If $setter is not provided, + * then no domain checks will be made. + * + * 'expires' field will be converted to ISO8601 format from COOKIE format, + * 'domain' and 'path' will be set from setter URL if empty. + * + * @param array $cookie cookie data, as returned by + * {@link HTTP_Request2_Response::getCookies()} + * @param Net_URL2 $setter URL of the document that sent Set-Cookie header + * + * @return array Updated cookie array + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_MessageException + */ + protected function checkAndUpdateFields(array $cookie, Net_URL2 $setter = null) + { + if ($missing = array_diff(array('name', 'value'), array_keys($cookie))) { + throw new HTTP_Request2_LogicException( + "Cookie array should contain 'name' and 'value' fields", + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['name'])) { + throw new HTTP_Request2_LogicException( + "Invalid cookie name: '{$cookie['name']}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + if (preg_match(HTTP_Request2::REGEXP_INVALID_COOKIE, $cookie['value'])) { + throw new HTTP_Request2_LogicException( + "Invalid cookie value: '{$cookie['value']}'", + HTTP_Request2_Exception::INVALID_ARGUMENT + ); + } + $cookie += array('domain' => '', 'path' => '', 'expires' => null, 'secure' => false); + + // Need ISO-8601 date @ UTC timezone + if (!empty($cookie['expires']) + && !preg_match('/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0000$/', $cookie['expires']) + ) { + try { + $dt = new DateTime($cookie['expires']); + $dt->setTimezone(new DateTimeZone('UTC')); + $cookie['expires'] = $dt->format(DateTime::ISO8601); + } catch (Exception $e) { + throw new HTTP_Request2_LogicException($e->getMessage()); + } + } + + if (empty($cookie['domain']) || empty($cookie['path'])) { + if (!$setter) { + throw new HTTP_Request2_LogicException( + 'Cookie misses domain and/or path component, cookie setter URL needed', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + if (empty($cookie['domain'])) { + if ($host = $setter->getHost()) { + $cookie['domain'] = $host; + } else { + throw new HTTP_Request2_LogicException( + 'Setter URL does not contain host part, can\'t set cookie domain', + HTTP_Request2_Exception::MISSING_VALUE + ); + } + } + if (empty($cookie['path'])) { + $path = $setter->getPath(); + $cookie['path'] = empty($path)? '/': substr($path, 0, strrpos($path, '/') + 1); + } + } + + if ($setter && !$this->domainMatch($setter->getHost(), $cookie['domain'])) { + throw new HTTP_Request2_MessageException( + "Domain " . $setter->getHost() . " cannot set cookies for " + . $cookie['domain'] + ); + } + + return $cookie; + } + + /** + * Stores a cookie in the jar + * + * @param array $cookie cookie data, as returned by + * {@link HTTP_Request2_Response::getCookies()} + * @param Net_URL2 $setter URL of the document that sent Set-Cookie header + * + * @throws HTTP_Request2_Exception + */ + public function store(array $cookie, Net_URL2 $setter = null) + { + $cookie = $this->checkAndUpdateFields($cookie, $setter); + + if (strlen($cookie['value']) + && (is_null($cookie['expires']) || $cookie['expires'] > $this->now()) + ) { + if (!isset($this->cookies[$cookie['domain']])) { + $this->cookies[$cookie['domain']] = array(); + } + if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { + $this->cookies[$cookie['domain']][$cookie['path']] = array(); + } + $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; + + } elseif (isset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']])) { + unset($this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']]); + } + } + + /** + * Adds cookies set in HTTP response to the jar + * + * @param HTTP_Request2_Response $response HTTP response message + * @param Net_URL2 $setter original request URL, needed for + * setting default domain/path + */ + public function addCookiesFromResponse(HTTP_Request2_Response $response, Net_URL2 $setter) + { + foreach ($response->getCookies() as $cookie) { + $this->store($cookie, $setter); + } + } + + /** + * Returns all cookies matching a given request URL + * + * The following checks are made: + * - cookie domain should match request host + * - cookie path should be a prefix for request path + * - 'secure' cookies will only be sent for HTTPS requests + * + * @param Net_URL2 $url Request url + * @param bool $asString Whether to return cookies as string for "Cookie: " header + * + * @return array|string Matching cookies + */ + public function getMatching(Net_URL2 $url, $asString = false) + { + $host = $url->getHost(); + $path = $url->getPath(); + $secure = 0 == strcasecmp($url->getScheme(), 'https'); + + $matched = $ret = array(); + foreach (array_keys($this->cookies) as $domain) { + if ($this->domainMatch($host, $domain)) { + foreach (array_keys($this->cookies[$domain]) as $cPath) { + if (0 === strpos($path, $cPath)) { + foreach ($this->cookies[$domain][$cPath] as $name => $cookie) { + if (!$cookie['secure'] || $secure) { + $matched[$name][strlen($cookie['path'])] = $cookie; + } + } + } + } + } + } + foreach ($matched as $cookies) { + krsort($cookies); + $ret = array_merge($ret, $cookies); + } + if (!$asString) { + return $ret; + } else { + $str = ''; + foreach ($ret as $c) { + $str .= (empty($str)? '': '; ') . $c['name'] . '=' . $c['value']; + } + return $str; + } + } + + /** + * Returns all cookies stored in a jar + * + * @return array + */ + public function getAll() + { + $cookies = array(); + foreach (array_keys($this->cookies) as $domain) { + foreach (array_keys($this->cookies[$domain]) as $path) { + foreach ($this->cookies[$domain][$path] as $name => $cookie) { + $cookies[] = $cookie; + } + } + } + return $cookies; + } + + /** + * Sets whether session cookies should be serialized when serializing the jar + * + * @param boolean $serialize serialize? + */ + public function serializeSessionCookies($serialize) + { + $this->serializeSession = (bool)$serialize; + } + + /** + * Sets whether Public Suffix List should be used for restricting cookie-setting + * + * Without PSL {@link domainMatch()} will only prevent setting cookies for + * top-level domains like '.com' or '.org'. However, it will not prevent + * setting a cookie for '.co.uk' even though only third-level registrations + * are possible in .uk domain. + * + * With the List it is possible to find the highest level at which a domain + * may be registered for a particular top-level domain and consequently + * prevent cookies set for '.co.uk' or '.msk.ru'. The same list is used by + * Firefox, Chrome and Opera browsers to restrict cookie setting. + * + * Note that PSL is licensed differently to HTTP_Request2 package (refer to + * the license information in public-suffix-list.php), so you can disable + * its use if this is an issue for you. + * + * @param boolean $useList use the list? + * + * @link http://publicsuffix.org/learn/ + */ + public function usePublicSuffixList($useList) + { + $this->useList = (bool)$useList; + } + + /** + * Returns string representation of object + * + * @return string + * + * @see Serializable::serialize() + */ + public function serialize() + { + $cookies = $this->getAll(); + if (!$this->serializeSession) { + for ($i = count($cookies) - 1; $i >= 0; $i--) { + if (empty($cookies[$i]['expires'])) { + unset($cookies[$i]); + } + } + } + return serialize(array( + 'cookies' => $cookies, + 'serializeSession' => $this->serializeSession, + 'useList' => $this->useList + )); + } + + /** + * Constructs the object from serialized string + * + * @param string $serialized string representation + * + * @see Serializable::unserialize() + */ + public function unserialize($serialized) + { + $data = unserialize($serialized); + $now = $this->now(); + $this->serializeSessionCookies($data['serializeSession']); + $this->usePublicSuffixList($data['useList']); + foreach ($data['cookies'] as $cookie) { + if (!empty($cookie['expires']) && $cookie['expires'] <= $now) { + continue; + } + if (!isset($this->cookies[$cookie['domain']])) { + $this->cookies[$cookie['domain']] = array(); + } + if (!isset($this->cookies[$cookie['domain']][$cookie['path']])) { + $this->cookies[$cookie['domain']][$cookie['path']] = array(); + } + $this->cookies[$cookie['domain']][$cookie['path']][$cookie['name']] = $cookie; + } + } + + /** + * Checks whether a cookie domain matches a request host. + * + * The method is used by {@link store()} to check for whether a document + * at given URL can set a cookie with a given domain attribute and by + * {@link getMatching()} to find cookies matching the request URL. + * + * @param string $requestHost request host + * @param string $cookieDomain cookie domain + * + * @return bool match success + */ + public function domainMatch($requestHost, $cookieDomain) + { + if ($requestHost == $cookieDomain) { + return true; + } + // IP address, we require exact match + if (preg_match('/^(?:\d{1,3}\.){3}\d{1,3}$/', $requestHost)) { + return false; + } + if ('.' != $cookieDomain[0]) { + $cookieDomain = '.' . $cookieDomain; + } + // prevents setting cookies for '.com' and similar domains + if (!$this->useList && substr_count($cookieDomain, '.') < 2 + || $this->useList && !self::getRegisteredDomain($cookieDomain) + ) { + return false; + } + return substr('.' . $requestHost, -strlen($cookieDomain)) == $cookieDomain; + } + + /** + * Removes subdomains to get the registered domain (the first after top-level) + * + * The method will check Public Suffix List to find out where top-level + * domain ends and registered domain starts. It will remove domain parts + * to the left of registered one. + * + * @param string $domain domain name + * + * @return string|bool registered domain, will return false if $domain is + * either invalid or a TLD itself + */ + public static function getRegisteredDomain($domain) + { + $domainParts = explode('.', ltrim($domain, '.')); + + // load the list if needed + if (empty(self::$psl)) { + $path = '/home/roberkwk/php/data' . DIRECTORY_SEPARATOR . 'HTTP_Request2'; + if (0 === strpos($path, '@' . 'data_dir@')) { + $path = realpath( + dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' + . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'data' + ); + } + self::$psl = include_once $path . DIRECTORY_SEPARATOR . 'public-suffix-list.php'; + } + + if (!($result = self::checkDomainsList($domainParts, self::$psl))) { + // known TLD, invalid domain name + return false; + } + + // unknown TLD + if (!strpos($result, '.')) { + // fallback to checking that domain "has at least two dots" + if (2 > ($count = count($domainParts))) { + return false; + } + return $domainParts[$count - 2] . '.' . $domainParts[$count - 1]; + } + return $result; + } + + /** + * Recursive helper method for {@link getRegisteredDomain()} + * + * @param array $domainParts remaining domain parts + * @param mixed $listNode node in {@link HTTP_Request2_CookieJar::$psl} to check + * + * @return string|null concatenated domain parts, null in case of error + */ + protected static function checkDomainsList(array $domainParts, $listNode) + { + $sub = array_pop($domainParts); + $result = null; + + if (!is_array($listNode) || is_null($sub) + || array_key_exists('!' . $sub, $listNode) + ) { + return $sub; + + } elseif (array_key_exists($sub, $listNode)) { + $result = self::checkDomainsList($domainParts, $listNode[$sub]); + + } elseif (array_key_exists('*', $listNode)) { + $result = self::checkDomainsList($domainParts, $listNode['*']); + + } else { + return $sub; + } + + return (strlen($result) > 0) ? ($result . '.' . $sub) : null; + } +} +?> \ No newline at end of file diff --git a/HTTP/Request2/Exception.php b/HTTP/Request2/Exception.php new file mode 100644 index 0000000..6dac48b --- /dev/null +++ b/HTTP/Request2/Exception.php @@ -0,0 +1,365 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Exception.php 324415 2012-03-21 10:50:50Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** + + * Base class for exceptions in PEAR + + */ + +require_once 'PEAR/Exception.php'; + + + +/** + + * Base exception class for HTTP_Request2 package + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + * @link http://pear.php.net/pepr/pepr-proposal-show.php?id=132 + + */ + +class HTTP_Request2_Exception extends PEAR_Exception + +{ + + /** An invalid argument was passed to a method */ + + const INVALID_ARGUMENT = 1; + + /** Some required value was not available */ + + const MISSING_VALUE = 2; + + /** Request cannot be processed due to errors in PHP configuration */ + + const MISCONFIGURATION = 3; + + /** Error reading the local file */ + + const READ_ERROR = 4; + + + + /** Server returned a response that does not conform to HTTP protocol */ + + const MALFORMED_RESPONSE = 10; + + /** Failure decoding Content-Encoding or Transfer-Encoding of response */ + + const DECODE_ERROR = 20; + + /** Operation timed out */ + + const TIMEOUT = 30; + + /** Number of redirects exceeded 'max_redirects' configuration parameter */ + + const TOO_MANY_REDIRECTS = 40; + + /** Redirect to a protocol other than http(s):// */ + + const NON_HTTP_REDIRECT = 50; + + + + /** + + * Native error code + + * @var int + + */ + + private $_nativeCode; + + + + /** + + * Constructor, can set package error code and native error code + + * + + * @param string $message exception message + + * @param int $code package error code, one of class constants + + * @param int $nativeCode error code from underlying PHP extension + + */ + + public function __construct($message = null, $code = null, $nativeCode = null) + + { + + parent::__construct($message, $code); + + $this->_nativeCode = $nativeCode; + + } + + + + /** + + * Returns error code produced by underlying PHP extension + + * + + * For Socket Adapter this may contain error number returned by + + * stream_socket_client(), for Curl Adapter this will contain error number + + * returned by curl_errno() + + * + + * @return integer + + */ + + public function getNativeCode() + + { + + return $this->_nativeCode; + + } + +} + + + +/** + + * Exception thrown in case of missing features + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_NotImplementedException extends HTTP_Request2_Exception + +{ + +} + + + +/** + + * Exception that represents error in the program logic + + * + + * This exception usually implies a programmer's error, like passing invalid + + * data to methods or trying to use PHP extensions that weren't installed or + + * enabled. Usually exceptions of this kind will be thrown before request even + + * starts. + + * + + * The exception will usually contain a package error code. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_LogicException extends HTTP_Request2_Exception + +{ + +} + + + +/** + + * Exception thrown when connection to a web or proxy server fails + + * + + * The exception will not contain a package error code, but will contain + + * native error code, as returned by stream_socket_client() or curl_errno(). + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_ConnectionException extends HTTP_Request2_Exception + +{ + +} + + + +/** + + * Exception thrown when sending or receiving HTTP message fails + + * + + * The exception may contain both package error code and native error code. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_MessageException extends HTTP_Request2_Exception + +{ + +} + +?> \ No newline at end of file diff --git a/HTTP/Request2/MultipartBody.php b/HTTP/Request2/MultipartBody.php new file mode 100644 index 0000000..9e9a178 --- /dev/null +++ b/HTTP/Request2/MultipartBody.php @@ -0,0 +1,280 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: MultipartBody.php 324415 2012-03-21 10:50:50Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** + * Class for building multipart/form-data request body + * + * The class helps to reduce memory consumption by streaming large file uploads + * from disk, it also allows monitoring of upload progress (see request #7630) + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 2.1.1 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://tools.ietf.org/html/rfc1867 + */ +class HTTP_Request2_MultipartBody +{ + /** + * MIME boundary + * @var string + */ + private $_boundary; + + /** + * Form parameters added via {@link HTTP_Request2::addPostParameter()} + * @var array + */ + private $_params = array(); + + /** + * File uploads added via {@link HTTP_Request2::addUpload()} + * @var array + */ + private $_uploads = array(); + + /** + * Header for parts with parameters + * @var string + */ + private $_headerParam = "--%s\r\nContent-Disposition: form-data; name=\"%s\"\r\n\r\n"; + + /** + * Header for parts with uploads + * @var string + */ + private $_headerUpload = "--%s\r\nContent-Disposition: form-data; name=\"%s\"; filename=\"%s\"\r\nContent-Type: %s\r\n\r\n"; + + /** + * Current position in parameter and upload arrays + * + * First number is index of "current" part, second number is position within + * "current" part + * + * @var array + */ + private $_pos = array(0, 0); + + + /** + * Constructor. Sets the arrays with POST data. + * + * @param array $params values of form fields set via + * {@link HTTP_Request2::addPostParameter()} + * @param array $uploads file uploads set via + * {@link HTTP_Request2::addUpload()} + * @param bool $useBrackets whether to append brackets to array variable names + */ + public function __construct(array $params, array $uploads, $useBrackets = true) + { + $this->_params = self::_flattenArray('', $params, $useBrackets); + foreach ($uploads as $fieldName => $f) { + if (!is_array($f['fp'])) { + $this->_uploads[] = $f + array('name' => $fieldName); + } else { + for ($i = 0; $i < count($f['fp']); $i++) { + $upload = array( + 'name' => ($useBrackets? $fieldName . '[' . $i . ']': $fieldName) + ); + foreach (array('fp', 'filename', 'size', 'type') as $key) { + $upload[$key] = $f[$key][$i]; + } + $this->_uploads[] = $upload; + } + } + } + } + + /** + * Returns the length of the body to use in Content-Length header + * + * @return integer + */ + public function getLength() + { + $boundaryLength = strlen($this->getBoundary()); + $headerParamLength = strlen($this->_headerParam) - 4 + $boundaryLength; + $headerUploadLength = strlen($this->_headerUpload) - 8 + $boundaryLength; + $length = $boundaryLength + 6; + foreach ($this->_params as $p) { + $length += $headerParamLength + strlen($p[0]) + strlen($p[1]) + 2; + } + foreach ($this->_uploads as $u) { + $length += $headerUploadLength + strlen($u['name']) + strlen($u['type']) + + strlen($u['filename']) + $u['size'] + 2; + } + return $length; + } + + /** + * Returns the boundary to use in Content-Type header + * + * @return string + */ + public function getBoundary() + { + if (empty($this->_boundary)) { + $this->_boundary = '--' . md5('PEAR-HTTP_Request2-' . microtime()); + } + return $this->_boundary; + } + + /** + * Returns next chunk of request body + * + * @param integer $length Number of bytes to read + * + * @return string Up to $length bytes of data, empty string if at end + */ + public function read($length) + { + $ret = ''; + $boundary = $this->getBoundary(); + $paramCount = count($this->_params); + $uploadCount = count($this->_uploads); + while ($length > 0 && $this->_pos[0] <= $paramCount + $uploadCount) { + $oldLength = $length; + if ($this->_pos[0] < $paramCount) { + $param = sprintf( + $this->_headerParam, $boundary, $this->_params[$this->_pos[0]][0] + ) . $this->_params[$this->_pos[0]][1] . "\r\n"; + $ret .= substr($param, $this->_pos[1], $length); + $length -= min(strlen($param) - $this->_pos[1], $length); + + } elseif ($this->_pos[0] < $paramCount + $uploadCount) { + $pos = $this->_pos[0] - $paramCount; + $header = sprintf( + $this->_headerUpload, $boundary, $this->_uploads[$pos]['name'], + $this->_uploads[$pos]['filename'], $this->_uploads[$pos]['type'] + ); + if ($this->_pos[1] < strlen($header)) { + $ret .= substr($header, $this->_pos[1], $length); + $length -= min(strlen($header) - $this->_pos[1], $length); + } + $filePos = max(0, $this->_pos[1] - strlen($header)); + if ($length > 0 && $filePos < $this->_uploads[$pos]['size']) { + $ret .= fread($this->_uploads[$pos]['fp'], $length); + $length -= min($length, $this->_uploads[$pos]['size'] - $filePos); + } + if ($length > 0) { + $start = $this->_pos[1] + ($oldLength - $length) - + strlen($header) - $this->_uploads[$pos]['size']; + $ret .= substr("\r\n", $start, $length); + $length -= min(2 - $start, $length); + } + + } else { + $closing = '--' . $boundary . "--\r\n"; + $ret .= substr($closing, $this->_pos[1], $length); + $length -= min(strlen($closing) - $this->_pos[1], $length); + } + if ($length > 0) { + $this->_pos = array($this->_pos[0] + 1, 0); + } else { + $this->_pos[1] += $oldLength; + } + } + return $ret; + } + + /** + * Sets the current position to the start of the body + * + * This allows reusing the same body in another request + */ + public function rewind() + { + $this->_pos = array(0, 0); + foreach ($this->_uploads as $u) { + rewind($u['fp']); + } + } + + /** + * Returns the body as string + * + * Note that it reads all file uploads into memory so it is a good idea not + * to use this method with large file uploads and rely on read() instead. + * + * @return string + */ + public function __toString() + { + $this->rewind(); + return $this->read($this->getLength()); + } + + + /** + * Helper function to change the (probably multidimensional) associative array + * into the simple one. + * + * @param string $name name for item + * @param mixed $values item's values + * @param bool $useBrackets whether to append [] to array variables' names + * + * @return array array with the following items: array('item name', 'item value'); + */ + private static function _flattenArray($name, $values, $useBrackets) + { + if (!is_array($values)) { + return array(array($name, $values)); + } else { + $ret = array(); + foreach ($values as $k => $v) { + if (empty($name)) { + $newName = $k; + } elseif ($useBrackets) { + $newName = $name . '[' . $k . ']'; + } else { + $newName = $name; + } + $ret = array_merge($ret, self::_flattenArray($newName, $v, $useBrackets)); + } + return $ret; + } + } +} +?> diff --git a/HTTP/Request2/Observer/Log.php b/HTTP/Request2/Observer/Log.php new file mode 100644 index 0000000..84fd048 --- /dev/null +++ b/HTTP/Request2/Observer/Log.php @@ -0,0 +1,429 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author David Jean Louis + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Log.php 324415 2012-03-21 10:50:50Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** + + * Exception class for HTTP_Request2 package + + */ + +require_once 'HTTP/Request2/Exception.php'; + + + +/** + + * A debug observer useful for debugging / testing. + + * + + * This observer logs to a log target data corresponding to the various request + + * and response events, it logs by default to php://output but can be configured + + * to log to a file or via the PEAR Log package. + + * + + * A simple example: + + * + + * require_once 'HTTP/Request2.php'; + + * require_once 'HTTP/Request2/Observer/Log.php'; + + * + + * $request = new HTTP_Request2('http://www.example.com'); + + * $observer = new HTTP_Request2_Observer_Log(); + + * $request->attach($observer); + + * $request->send(); + + * + + * + + * A more complex example with PEAR Log: + + * + + * require_once 'HTTP/Request2.php'; + + * require_once 'HTTP/Request2/Observer/Log.php'; + + * require_once 'Log.php'; + + * + + * $request = new HTTP_Request2('http://www.example.com'); + + * // we want to log with PEAR log + + * $observer = new HTTP_Request2_Observer_Log(Log::factory('console')); + + * + + * // we only want to log received headers + + * $observer->events = array('receivedHeaders'); + + * + + * $request->attach($observer); + + * $request->send(); + + * + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author David Jean Louis + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + +class HTTP_Request2_Observer_Log implements SplObserver + +{ + + // properties {{{ + + + + /** + + * The log target, it can be a a resource or a PEAR Log instance. + + * + + * @var resource|Log $target + + */ + + protected $target = null; + + + + /** + + * The events to log. + + * + + * @var array $events + + */ + + public $events = array( + + 'connect', + + 'sentHeaders', + + 'sentBody', + + 'receivedHeaders', + + 'receivedBody', + + 'disconnect', + + ); + + + + // }}} + + // __construct() {{{ + + + + /** + + * Constructor. + + * + + * @param mixed $target Can be a file path (default: php://output), a resource, + + * or an instance of the PEAR Log class. + + * @param array $events Array of events to listen to (default: all events) + + * + + * @return void + + */ + + public function __construct($target = 'php://output', array $events = array()) + + { + + if (!empty($events)) { + + $this->events = $events; + + } + + if (is_resource($target) || $target instanceof Log) { + + $this->target = $target; + + } elseif (false === ($this->target = @fopen($target, 'ab'))) { + + throw new HTTP_Request2_Exception("Unable to open '{$target}'"); + + } + + } + + + + // }}} + + // update() {{{ + + + + /** + + * Called when the request notifies us of an event. + + * + + * @param HTTP_Request2 $subject The HTTP_Request2 instance + + * + + * @return void + + */ + + public function update(SplSubject $subject) + + { + + $event = $subject->getLastEvent(); + + if (!in_array($event['name'], $this->events)) { + + return; + + } + + + + switch ($event['name']) { + + case 'connect': + + $this->log('* Connected to ' . $event['data']); + + break; + + case 'sentHeaders': + + $headers = explode("\r\n", $event['data']); + + array_pop($headers); + + foreach ($headers as $header) { + + $this->log('> ' . $header); + + } + + break; + + case 'sentBody': + + $this->log('> ' . $event['data'] . ' byte(s) sent'); + + break; + + case 'receivedHeaders': + + $this->log(sprintf( + + '< HTTP/%s %s %s', $event['data']->getVersion(), + + $event['data']->getStatus(), $event['data']->getReasonPhrase() + + )); + + $headers = $event['data']->getHeader(); + + foreach ($headers as $key => $val) { + + $this->log('< ' . $key . ': ' . $val); + + } + + $this->log('< '); + + break; + + case 'receivedBody': + + $this->log($event['data']->getBody()); + + break; + + case 'disconnect': + + $this->log('* Disconnected'); + + break; + + } + + } + + + + // }}} + + // log() {{{ + + + + /** + + * Logs the given message to the configured target. + + * + + * @param string $message Message to display + + * + + * @return void + + */ + + protected function log($message) + + { + + if ($this->target instanceof Log) { + + $this->target->debug($message); + + } elseif (is_resource($this->target)) { + + fwrite($this->target, $message . "\r\n"); + + } + + } + + + + // }}} + +} + + + +?> \ No newline at end of file diff --git a/HTTP/Request2/Response.php b/HTTP/Request2/Response.php new file mode 100644 index 0000000..4b113e7 --- /dev/null +++ b/HTTP/Request2/Response.php @@ -0,0 +1,1307 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: Response.php 324936 2012-04-07 07:49:03Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** + + * Exception class for HTTP_Request2 package + + */ + +require_once 'HTTP/Request2/Exception.php'; + + + +/** + + * Class representing a HTTP response + + * + + * The class is designed to be used in "streaming" scenario, building the + + * response as it is being received: + + * + + * $statusLine = read_status_line(); + + * $response = new HTTP_Request2_Response($statusLine); + + * do { + + * $headerLine = read_header_line(); + + * $response->parseHeaderLine($headerLine); + + * } while ($headerLine != ''); + + * + + * while ($chunk = read_body()) { + + * $response->appendBody($chunk); + + * } + + * + + * var_dump($response->getHeader(), $response->getCookies(), $response->getBody()); + + * + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + * @link http://tools.ietf.org/html/rfc2616#section-6 + + */ + +class HTTP_Request2_Response + +{ + + /** + + * HTTP protocol version (e.g. 1.0, 1.1) + + * @var string + + */ + + protected $version; + + + + /** + + * Status code + + * @var integer + + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + + */ + + protected $code; + + + + /** + + * Reason phrase + + * @var string + + * @link http://tools.ietf.org/html/rfc2616#section-6.1.1 + + */ + + protected $reasonPhrase; + + + + /** + + * Effective URL (may be different from original request URL in case of redirects) + + * @var string + + */ + + protected $effectiveUrl; + + + + /** + + * Associative array of response headers + + * @var array + + */ + + protected $headers = array(); + + + + /** + + * Cookies set in the response + + * @var array + + */ + + protected $cookies = array(); + + + + /** + + * Name of last header processed by parseHederLine() + + * + + * Used to handle the headers that span multiple lines + + * + + * @var string + + */ + + protected $lastHeader = null; + + + + /** + + * Response body + + * @var string + + */ + + protected $body = ''; + + + + /** + + * Whether the body is still encoded by Content-Encoding + + * + + * cURL provides the decoded body to the callback; if we are reading from + + * socket the body is still gzipped / deflated + + * + + * @var bool + + */ + + protected $bodyEncoded; + + + + /** + + * Associative array of HTTP status code / reason phrase. + + * + + * @var array + + * @link http://tools.ietf.org/html/rfc2616#section-10 + + */ + + protected static $phrases = array( + + + + // 1xx: Informational - Request received, continuing process + + 100 => 'Continue', + + 101 => 'Switching Protocols', + + + + // 2xx: Success - The action was successfully received, understood and + + // accepted + + 200 => 'OK', + + 201 => 'Created', + + 202 => 'Accepted', + + 203 => 'Non-Authoritative Information', + + 204 => 'No Content', + + 205 => 'Reset Content', + + 206 => 'Partial Content', + + + + // 3xx: Redirection - Further action must be taken in order to complete + + // the request + + 300 => 'Multiple Choices', + + 301 => 'Moved Permanently', + + 302 => 'Found', // 1.1 + + 303 => 'See Other', + + 304 => 'Not Modified', + + 305 => 'Use Proxy', + + 307 => 'Temporary Redirect', + + + + // 4xx: Client Error - The request contains bad syntax or cannot be + + // fulfilled + + 400 => 'Bad Request', + + 401 => 'Unauthorized', + + 402 => 'Payment Required', + + 403 => 'Forbidden', + + 404 => 'Not Found', + + 405 => 'Method Not Allowed', + + 406 => 'Not Acceptable', + + 407 => 'Proxy Authentication Required', + + 408 => 'Request Timeout', + + 409 => 'Conflict', + + 410 => 'Gone', + + 411 => 'Length Required', + + 412 => 'Precondition Failed', + + 413 => 'Request Entity Too Large', + + 414 => 'Request-URI Too Long', + + 415 => 'Unsupported Media Type', + + 416 => 'Requested Range Not Satisfiable', + + 417 => 'Expectation Failed', + + + + // 5xx: Server Error - The server failed to fulfill an apparently + + // valid request + + 500 => 'Internal Server Error', + + 501 => 'Not Implemented', + + 502 => 'Bad Gateway', + + 503 => 'Service Unavailable', + + 504 => 'Gateway Timeout', + + 505 => 'HTTP Version Not Supported', + + 509 => 'Bandwidth Limit Exceeded', + + + + ); + + + + /** + + * Returns the default reason phrase for the given code or all reason phrases + + * + + * @param int $code Response code + + * + + * @return string|array|null Default reason phrase for $code if $code is given + + * (null if no phrase is available), array of all + + * reason phrases if $code is null + + * @link http://pear.php.net/bugs/18716 + + */ + + public static function getDefaultReasonPhrase($code = null) + + { + + if (null === $code) { + + return self::$phrases; + + } else { + + return isset(self::$phrases[$code]) ? self::$phrases[$code] : null; + + } + + } + + + + /** + + * Constructor, parses the response status line + + * + + * @param string $statusLine Response status line (e.g. "HTTP/1.1 200 OK") + + * @param bool $bodyEncoded Whether body is still encoded by Content-Encoding + + * @param string $effectiveUrl Effective URL of the response + + * + + * @throws HTTP_Request2_MessageException if status line is invalid according to spec + + */ + + public function __construct($statusLine, $bodyEncoded = true, $effectiveUrl = null) + + { + + if (!preg_match('!^HTTP/(\d\.\d) (\d{3})(?: (.+))?!', $statusLine, $m)) { + + throw new HTTP_Request2_MessageException( + + "Malformed response: {$statusLine}", + + HTTP_Request2_Exception::MALFORMED_RESPONSE + + ); + + } + + $this->version = $m[1]; + + $this->code = intval($m[2]); + + $this->reasonPhrase = !empty($m[3]) ? trim($m[3]) : self::getDefaultReasonPhrase($this->code); + + $this->bodyEncoded = (bool)$bodyEncoded; + + $this->effectiveUrl = (string)$effectiveUrl; + + } + + + + /** + + * Parses the line from HTTP response filling $headers array + + * + + * The method should be called after reading the line from socket or receiving + + * it into cURL callback. Passing an empty string here indicates the end of + + * response headers and triggers additional processing, so be sure to pass an + + * empty string in the end. + + * + + * @param string $headerLine Line from HTTP response + + */ + + public function parseHeaderLine($headerLine) + + { + + $headerLine = trim($headerLine, "\r\n"); + + + + if ('' == $headerLine) { + + // empty string signals the end of headers, process the received ones + + if (!empty($this->headers['set-cookie'])) { + + $cookies = is_array($this->headers['set-cookie'])? + + $this->headers['set-cookie']: + + array($this->headers['set-cookie']); + + foreach ($cookies as $cookieString) { + + $this->parseCookie($cookieString); + + } + + unset($this->headers['set-cookie']); + + } + + foreach (array_keys($this->headers) as $k) { + + if (is_array($this->headers[$k])) { + + $this->headers[$k] = implode(', ', $this->headers[$k]); + + } + + } + + + + } elseif (preg_match('!^([^\x00-\x1f\x7f-\xff()<>@,;:\\\\"/\[\]?={}\s]+):(.+)$!', $headerLine, $m)) { + + // string of the form header-name: header value + + $name = strtolower($m[1]); + + $value = trim($m[2]); + + if (empty($this->headers[$name])) { + + $this->headers[$name] = $value; + + } else { + + if (!is_array($this->headers[$name])) { + + $this->headers[$name] = array($this->headers[$name]); + + } + + $this->headers[$name][] = $value; + + } + + $this->lastHeader = $name; + + + + } elseif (preg_match('!^\s+(.+)$!', $headerLine, $m) && $this->lastHeader) { + + // continuation of a previous header + + if (!is_array($this->headers[$this->lastHeader])) { + + $this->headers[$this->lastHeader] .= ' ' . trim($m[1]); + + } else { + + $key = count($this->headers[$this->lastHeader]) - 1; + + $this->headers[$this->lastHeader][$key] .= ' ' . trim($m[1]); + + } + + } + + } + + + + /** + + * Parses a Set-Cookie header to fill $cookies array + + * + + * @param string $cookieString value of Set-Cookie header + + * + + * @link http://web.archive.org/web/20080331104521/http://cgi.netscape.com/newsref/std/cookie_spec.html + + */ + + protected function parseCookie($cookieString) + + { + + $cookie = array( + + 'expires' => null, + + 'domain' => null, + + 'path' => null, + + 'secure' => false + + ); + + + + if (!strpos($cookieString, ';')) { + + // Only a name=value pair + + $pos = strpos($cookieString, '='); + + $cookie['name'] = trim(substr($cookieString, 0, $pos)); + + $cookie['value'] = trim(substr($cookieString, $pos + 1)); + + + + } else { + + // Some optional parameters are supplied + + $elements = explode(';', $cookieString); + + $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; + + } + + + + /** + + * Appends a string to the response body + + * + + * @param string $bodyChunk part of response body + + */ + + public function appendBody($bodyChunk) + + { + + $this->body .= $bodyChunk; + + } + + + + /** + + * Returns the effective URL of the response + + * + + * This may be different from the request URL if redirects were followed. + + * + + * @return string + + * @link http://pear.php.net/bugs/bug.php?id=18412 + + */ + + public function getEffectiveUrl() + + { + + return $this->effectiveUrl; + + } + + + + /** + + * Returns the status code + + * + + * @return integer + + */ + + public function getStatus() + + { + + return $this->code; + + } + + + + /** + + * Returns the reason phrase + + * + + * @return string + + */ + + public function getReasonPhrase() + + { + + return $this->reasonPhrase; + + } + + + + /** + + * Whether response is a redirect that can be automatically handled by HTTP_Request2 + + * + + * @return bool + + */ + + public function isRedirect() + + { + + return in_array($this->code, array(300, 301, 302, 303, 307)) + + && isset($this->headers['location']); + + } + + + + /** + + * Returns either the named header or all response headers + + * + + * @param string $headerName Name of header to return + + * + + * @return string|array Value of $headerName header (null if header is + + * not present), array of all response headers if + + * $headerName is null + + */ + + public function getHeader($headerName = null) + + { + + if (null === $headerName) { + + return $this->headers; + + } else { + + $headerName = strtolower($headerName); + + return isset($this->headers[$headerName])? $this->headers[$headerName]: null; + + } + + } + + + + /** + + * Returns cookies set in response + + * + + * @return array + + */ + + public function getCookies() + + { + + return $this->cookies; + + } + + + + /** + + * Returns the body of the response + + * + + * @return string + + * @throws HTTP_Request2_Exception if body cannot be decoded + + */ + + public function getBody() + + { + + if (0 == strlen($this->body) || !$this->bodyEncoded + + || !in_array(strtolower($this->getHeader('content-encoding')), array('gzip', 'deflate')) + + ) { + + return $this->body; + + + + } else { + + if (extension_loaded('mbstring') && (2 & ini_get('mbstring.func_overload'))) { + + $oldEncoding = mb_internal_encoding(); + + mb_internal_encoding('8bit'); + + } + + + + try { + + switch (strtolower($this->getHeader('content-encoding'))) { + + case 'gzip': + + $decoded = self::decodeGzip($this->body); + + break; + + case 'deflate': + + $decoded = self::decodeDeflate($this->body); + + } + + } catch (Exception $e) { + + } + + + + if (!empty($oldEncoding)) { + + mb_internal_encoding($oldEncoding); + + } + + if (!empty($e)) { + + throw $e; + + } + + return $decoded; + + } + + } + + + + /** + + * Get the HTTP version of the response + + * + + * @return string + + */ + + public function getVersion() + + { + + return $this->version; + + } + + + + /** + + * 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 + + * + + * @param string $data gzip-encoded data + + * + + * @return string decoded data + + * @throws HTTP_Request2_LogicException + + * @throws HTTP_Request2_MessageException + + * @link http://tools.ietf.org/html/rfc1952 + + */ + + public static function decodeGzip($data) + + { + + $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; + + } + + if (!function_exists('gzinflate')) { + + throw new HTTP_Request2_LogicException( + + 'Unable to decode body: gzip extension not available', + + HTTP_Request2_Exception::MISCONFIGURATION + + ); + + } + + $method = ord(substr($data, 2, 1)); + + if (8 != $method) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: unknown compression method', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $flags = ord(substr($data, 3, 1)); + + if ($flags & 224) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: reserved bits are set', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + + + // 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) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $extraLength = unpack('v', substr($data, 10, 2)); + + if ($length - $headerLength - 2 - $extraLength[1] < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $headerLength += $extraLength[1] + 2; + + } + + // file name, need to skip that + + if ($flags & 8) { + + if ($length - $headerLength - 1 < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $filenameLength = strpos(substr($data, $headerLength), chr(0)); + + if (false === $filenameLength || $length - $headerLength - $filenameLength - 1 < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $headerLength += $filenameLength + 1; + + } + + // comment, need to skip that also + + if ($flags & 16) { + + if ($length - $headerLength - 1 < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $commentLength = strpos(substr($data, $headerLength), chr(0)); + + if (false === $commentLength || $length - $headerLength - $commentLength - 1 < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $headerLength += $commentLength + 1; + + } + + // have a CRC for header. let's check + + if ($flags & 2) { + + if ($length - $headerLength - 2 < 8) { + + throw new HTTP_Request2_MessageException( + + 'Error parsing gzip header: data too short', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $crcReal = 0xffff & crc32(substr($data, 0, $headerLength)); + + $crcStored = unpack('v', substr($data, $headerLength, 2)); + + if ($crcReal != $crcStored[1]) { + + throw new HTTP_Request2_MessageException( + + 'Header CRC check failed', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + $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 + + // don't pass $dataSize to gzinflate, see bugs #13135, #14370 + + $unpacked = gzinflate(substr($data, $headerLength, -8)); + + if (false === $unpacked) { + + throw new HTTP_Request2_MessageException( + + 'gzinflate() call failed', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } elseif ($dataSize != strlen($unpacked)) { + + throw new HTTP_Request2_MessageException( + + 'Data size check failed', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } elseif ((0xffffffff & $dataCrc) != (0xffffffff & crc32($unpacked))) { + + throw new HTTP_Request2_Exception( + + 'Data CRC check failed', + + HTTP_Request2_Exception::DECODE_ERROR + + ); + + } + + return $unpacked; + + } + + + + /** + + * Decodes the message-body encoded by deflate + + * + + * @param string $data deflate-encoded data + + * + + * @return string decoded data + + * @throws HTTP_Request2_LogicException + + */ + + public static function decodeDeflate($data) + + { + + if (!function_exists('gzuncompress')) { + + throw new HTTP_Request2_LogicException( + + 'Unable to decode body: gzip extension not available', + + HTTP_Request2_Exception::MISCONFIGURATION + + ); + + } + + // RFC 2616 defines 'deflate' encoding as zlib format from RFC 1950, + + // while many applications send raw deflate stream from RFC 1951. + + // We should check for presence of zlib header and use gzuncompress() or + + // gzinflate() as needed. See bug #15305 + + $header = unpack('n', substr($data, 0, 2)); + + return (0 == $header[1] % 31)? gzuncompress($data): gzinflate($data); + + } + +} + +?> \ No newline at end of file diff --git a/HTTP/Request2/SOCKS5.php b/HTTP/Request2/SOCKS5.php new file mode 100644 index 0000000..50d8e94 --- /dev/null +++ b/HTTP/Request2/SOCKS5.php @@ -0,0 +1,158 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The names of the authors may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version SVN: $Id: SOCKS5.php 324953 2012-04-08 07:24:12Z avb $ + * @link http://pear.php.net/package/HTTP_Request2 + */ + +/** Socket wrapper class used by Socket Adapter */ +require_once 'HTTP/Request2/SocketWrapper.php'; + +/** + * SOCKS5 proxy connection class (used by Socket Adapter) + * + * @category HTTP + * @package HTTP_Request2 + * @author Alexey Borzov + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 2.1.1 + * @link http://pear.php.net/package/HTTP_Request2 + * @link http://pear.php.net/bugs/bug.php?id=19332 + * @link http://tools.ietf.org/html/rfc1928 + */ +class HTTP_Request2_SOCKS5 extends HTTP_Request2_SocketWrapper +{ + /** + * Constructor, tries to connect and authenticate to a SOCKS5 proxy + * + * @param string $address Proxy address, e.g. 'tcp://localhost:1080' + * @param int $timeout Connection timeout (seconds) + * @param array $sslOptions SSL context options + * @param string $username Proxy user name + * @param string $password Proxy password + * + * @throws HTTP_Request2_LogicException + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + */ + public function __construct( + $address, $timeout = 10, array $sslOptions = array(), + $username = null, $password = null + ) { + parent::__construct($address, $timeout, $sslOptions); + + if (strlen($username)) { + $request = pack('C4', 5, 2, 0, 2); + } else { + $request = pack('C3', 5, 1, 0); + } + $this->write($request); + $response = unpack('Cversion/Cmethod', $this->read(3)); + if (5 != $response['version']) { + throw new HTTP_Request2_MessageException( + 'Invalid version received from SOCKS5 proxy: ' . $response['version'], + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } + switch ($response['method']) { + case 2: + $this->performAuthentication($username, $password); + case 0: + break; + default: + throw new HTTP_Request2_ConnectionException( + "Connection rejected by proxy due to unsupported auth method" + ); + } + } + + /** + * Performs username/password authentication for SOCKS5 + * + * @param string $username Proxy user name + * @param string $password Proxy password + * + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + * @link http://tools.ietf.org/html/rfc1929 + */ + protected function performAuthentication($username, $password) + { + $request = pack('C2', 1, strlen($username)) . $username + . pack('C', strlen($password)) . $password; + + $this->write($request); + $response = unpack('Cvn/Cstatus', $this->read(3)); + if (1 != $response['vn'] || 0 != $response['status']) { + throw new HTTP_Request2_ConnectionException( + 'Connection rejected by proxy due to invalid username and/or password' + ); + } + } + + /** + * Connects to a remote host via proxy + * + * @param string $remoteHost Remote host + * @param int $remotePort Remote port + * + * @throws HTTP_Request2_ConnectionException + * @throws HTTP_Request2_MessageException + */ + public function connect($remoteHost, $remotePort) + { + $request = pack('C5', 0x05, 0x01, 0x00, 0x03, strlen($remoteHost)) + . $remoteHost . pack('n', $remotePort); + + $this->write($request); + $response = unpack('Cversion/Creply/Creserved', $this->read(1024)); + if (5 != $response['version'] || 0 != $response['reserved']) { + throw new HTTP_Request2_MessageException( + 'Invalid response received from SOCKS5 proxy', + HTTP_Request2_Exception::MALFORMED_RESPONSE + ); + } elseif (0 != $response['reply']) { + throw new HTTP_Request2_ConnectionException( + "Unable to connect to {$remoteHost}:{$remotePort} through SOCKS5 proxy", + 0, $response['reply'] + ); + } + } +} +?> \ No newline at end of file diff --git a/HTTP/Request2/SocketWrapper.php b/HTTP/Request2/SocketWrapper.php new file mode 100644 index 0000000..30ba049 --- /dev/null +++ b/HTTP/Request2/SocketWrapper.php @@ -0,0 +1,566 @@ + + + * All rights reserved. + + * + + * Redistribution and use in source and binary forms, with or without + + * modification, are permitted provided that the following conditions + + * are met: + + * + + * * Redistributions of source code must retain the above copyright + + * notice, this list of conditions and the following disclaimer. + + * * Redistributions in binary form must reproduce the above copyright + + * notice, this list of conditions and the following disclaimer in the + + * documentation and/or other materials provided with the distribution. + + * * The names of the authors may not be used to endorse or promote products + + * derived from this software without specific prior written permission. + + * + + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS + + * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version SVN: $Id: SocketWrapper.php 324935 2012-04-07 07:10:50Z avb $ + + * @link http://pear.php.net/package/HTTP_Request2 + + */ + + + +/** Exception classes for HTTP_Request2 package */ + +require_once 'HTTP/Request2/Exception.php'; + + + +/** + + * Socket wrapper class used by Socket Adapter + + * + + * Needed to properly handle connection errors, global timeout support and + + * similar things. Loosely based on Net_Socket used by older HTTP_Request. + + * + + * @category HTTP + + * @package HTTP_Request2 + + * @author Alexey Borzov + + * @license http://opensource.org/licenses/bsd-license.php New BSD License + + * @version Release: 2.1.1 + + * @link http://pear.php.net/package/HTTP_Request2 + + * @link http://pear.php.net/bugs/bug.php?id=19332 + + * @link http://tools.ietf.org/html/rfc1928 + + */ + +class HTTP_Request2_SocketWrapper + +{ + + /** + + * PHP warning messages raised during stream_socket_client() call + + * @var array + + */ + + protected $connectionWarnings = array(); + + + + /** + + * Connected socket + + * @var resource + + */ + + protected $socket; + + + + /** + + * Sum of start time and global timeout, exception will be thrown if request continues past this time + + * @var integer + + */ + + protected $deadline; + + + + /** + + * Global timeout value, mostly for exception messages + + * @var integer + + */ + + protected $timeout; + + + + /** + + * Class constructor, tries to establish connection + + * + + * @param string $address Address for stream_socket_client() call, + + * e.g. 'tcp://localhost:80' + + * @param int $timeout Connection timeout (seconds) + + * @param array $sslOptions SSL context options + + * + + * @throws HTTP_Request2_LogicException + + * @throws HTTP_Request2_ConnectionException + + */ + + public function __construct($address, $timeout, array $sslOptions = array()) + + { + + $context = stream_context_create(); + + foreach ($sslOptions as $name => $value) { + + if (!stream_context_set_option($context, 'ssl', $name, $value)) { + + throw new HTTP_Request2_LogicException( + + "Error setting SSL context option '{$name}'" + + ); + + } + + } + + set_error_handler(array($this, 'connectionWarningsHandler')); + + $this->socket = stream_socket_client( + + $address, $errno, $errstr, $timeout, STREAM_CLIENT_CONNECT, $context + + ); + + restore_error_handler(); + + if (!$this->socket) { + + $error = $errstr ? $errstr : implode("\n", $this->connectionWarnings); + + throw new HTTP_Request2_ConnectionException( + + "Unable to connect to {$address}. Error: {$error}", 0, $errno + + ); + + } + + } + + + + /** + + * Destructor, disconnects socket + + */ + + public function __destruct() + + { + + fclose($this->socket); + + } + + + + /** + + * Wrapper around fread(), handles global request timeout + + * + + * @param int $length Reads up to this number of bytes + + * + + * @return string Data read from socket + + * @throws HTTP_Request2_MessageException In case of timeout + + */ + + public function read($length) + + { + + if ($this->deadline) { + + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + + } + + $data = fread($this->socket, $length); + + $this->checkTimeout(); + + return $data; + + } + + + + /** + + * Reads until either the end of the socket or a newline, whichever comes first + + * + + * Strips the trailing newline from the returned data, handles global + + * request timeout. Method idea borrowed from Net_Socket PEAR package. + + * + + * @param int $bufferSize buffer size to use for reading + + * + + * @return string Available data up to the newline (not including newline) + + * @throws HTTP_Request2_MessageException In case of timeout + + */ + + public function readLine($bufferSize) + + { + + $line = ''; + + while (!feof($this->socket)) { + + if ($this->deadline) { + + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + + } + + $line .= @fgets($this->socket, $bufferSize); + + $this->checkTimeout(); + + if (substr($line, -1) == "\n") { + + return rtrim($line, "\r\n"); + + } + + } + + return $line; + + } + + + + /** + + * Wrapper around fwrite(), handles global request timeout + + * + + * @param string $data String to be written + + * + + * @return int + + * @throws HTTP_Request2_MessageException + + */ + + public function write($data) + + { + + if ($this->deadline) { + + stream_set_timeout($this->socket, max($this->deadline - time(), 1)); + + } + + $written = fwrite($this->socket, $data); + + $this->checkTimeout(); + + // http://www.php.net/manual/en/function.fwrite.php#96951 + + if ($written < strlen($data)) { + + throw new HTTP_Request2_MessageException('Error writing request'); + + } + + return $written; + + } + + + + /** + + * Tests for end-of-file on a socket + + * + + * @return bool + + */ + + public function eof() + + { + + return feof($this->socket); + + } + + + + /** + + * Sets request deadline + + * + + * @param int $deadline Exception will be thrown if request continues + + * past this time + + * @param int $timeout Original request timeout value, to use in + + * Exception message + + */ + + public function setDeadline($deadline, $timeout) + + { + + $this->deadline = $deadline; + + $this->timeout = $timeout; + + } + + + + /** + + * Turns on encryption on a socket + + * + + * @throws HTTP_Request2_ConnectionException + + */ + + public function enableCrypto() + + { + + $modes = array( + + STREAM_CRYPTO_METHOD_TLS_CLIENT, + + STREAM_CRYPTO_METHOD_SSLv3_CLIENT, + + STREAM_CRYPTO_METHOD_SSLv23_CLIENT, + + STREAM_CRYPTO_METHOD_SSLv2_CLIENT + + ); + + + + foreach ($modes as $mode) { + + if (stream_socket_enable_crypto($this->socket, true, $mode)) { + + return; + + } + + } + + throw new HTTP_Request2_ConnectionException( + + 'Failed to enable secure connection when connecting through proxy' + + ); + + } + + + + /** + + * Throws an Exception if stream timed out + + * + + * @throws HTTP_Request2_MessageException + + */ + + protected function checkTimeout() + + { + + $info = stream_get_meta_data($this->socket); + + if ($info['timed_out'] || $this->deadline && time() > $this->deadline) { + + $reason = $this->deadline + + ? "after {$this->timeout} second(s)" + + : 'due to default_socket_timeout php.ini setting'; + + throw new HTTP_Request2_MessageException( + + "Request timed out {$reason}", HTTP_Request2_Exception::TIMEOUT + + ); + + } + + } + + + + /** + + * Error handler to use during stream_socket_client() call + + * + + * One stream_socket_client() call may produce *multiple* PHP warnings + + * (especially OpenSSL-related), we keep them in an array to later use for + + * the message of HTTP_Request2_ConnectionException + + * + + * @param int $errno error level + + * @param string $errstr error message + + * + + * @return bool + + */ + + protected function connectionWarningsHandler($errno, $errstr) + + { + + if ($errno & E_WARNING) { + + array_unshift($this->connectionWarnings, $errstr); + + } + + return true; + + } + +} + +?> + diff --git a/Net/SMTP.php b/Net/SMTP.php new file mode 100644 index 0000000..8d0682f --- /dev/null +++ b/Net/SMTP.php @@ -0,0 +1,1342 @@ + | +// | Jon Parise | +// | Damian Alejandro Fernandez Sosa | +// +----------------------------------------------------------------------+ +// +// $Id: SMTP.php 314875 2011-08-13 17:03:30Z jon $ + +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(); + + /** + * Use SMTP command pipelining (specified in RFC 2920) if the SMTP + * server supports it. + * + * When pipeling is enabled, rcptTo(), mailFrom(), sendFrom(), + * somlFrom() and samlFrom() do not wait for a response from the + * SMTP server but return immediately. + * + * @var bool + * @access public + */ + var $pipelining = false; + + /** + * Number of pipelined commands. + * @var int + * @access private + */ + var $_pipelined_commands = 0; + + /** + * Should debugging output be enabled? + * @var boolean + * @access private + */ + var $_debug = false; + + /** + * Debug output handler. + * @var callback + * @access private + */ + var $_debug_handler = null; + + /** + * The socket resource being used to connect to the SMTP server. + * @var resource + * @access private + */ + var $_socket = null; + + /** + * Array of socket options that will be passed to Net_Socket::connect(). + * @see stream_context_create() + * @var array + * @access private + */ + var $_socket_options = null; + + /** + * The socket I/O timeout value in seconds. + * @var int + * @access private + */ + var $_timeout = 0; + + /** + * 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 the SMTP server's greeting string. + * @var string + * @access private + */ + var $_greeting = null; + + /** + * 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. + * @param boolean $pipeling Use SMTP command pipelining + * @param integer $timeout Socket I/O timeout in seconds. + * @param array $socket_options Socket stream_context_create() options. + * + * @access public + * @since 1.0 + */ + function Net_SMTP($host = null, $port = null, $localhost = null, + $pipelining = false, $timeout = 0, $socket_options = null) + { + if (isset($host)) { + $this->host = $host; + } + if (isset($port)) { + $this->port = $port; + } + if (isset($localhost)) { + $this->localhost = $localhost; + } + $this->pipelining = $pipelining; + + $this->_socket = new Net_Socket(); + $this->_socket_options = $socket_options; + $this->_timeout = $timeout; + + /* Include the Auth_SASL package. If the package is available, we + * enable the authentication methods that depend upon it. */ + if (@include_once 'Auth/SASL.php') { + $this->setAuthMethod('CRAM-MD5', array($this, '_authCram_MD5')); + $this->setAuthMethod('DIGEST-MD5', array($this, '_authDigest_MD5')); + } + + /* These standard authentication methods are always available. */ + $this->setAuthMethod('LOGIN', array($this, '_authLogin'), false); + $this->setAuthMethod('PLAIN', array($this, '_authPlain'), false); + } + + /** + * Set the socket I/O timeout value in seconds plus microseconds. + * + * @param integer $seconds Timeout value in seconds. + * @param integer $microseconds Additional value in microseconds. + * + * @access public + * @since 1.5.0 + */ + function setTimeout($seconds, $microseconds = 0) { + return $this->_socket->setTimeout($seconds, $microseconds); + } + + /** + * 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, $handler = null) + { + $this->_debug = $debug; + $this->_debug_handler = $handler; + } + + /** + * Write the given debug text to the current debug output handler. + * + * @param string $message Debug mesage text. + * + * @access private + * @since 1.3.3 + */ + function _debug($message) + { + if ($this->_debug) { + if ($this->_debug_handler) { + call_user_func_array($this->_debug_handler, + array(&$this, $message)); + } else { + echo "DEBUG: $message\n"; + } + } + } + + /** + * Send the given string of data to the server. + * + * @param string $data The string of data to send. + * + * @return mixed The number of bytes that were actually written, + * or a PEAR_Error object on failure. + * + * @access private + * @since 1.1.0 + */ + function _send($data) + { + $this->_debug("Send: $data"); + + $result = $this->_socket->write($data); + if (!$result || PEAR::isError($result)) { + $msg = ($result) ? $result->getMessage() : "unknown error"; + return PEAR::raiseError("Failed to write to socket: $msg", + null, PEAR_ERROR_RETURN); + } + + return $result; + } + + /** + * 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', + null, PEAR_ERROR_RETURN); + } + + 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. + * @param bool $later Do not parse the response now, but wait + * until the last command in the pipelined + * command group + * + * @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, $later = false) + { + $this->_code = -1; + $this->_arguments = array(); + + if ($later) { + $this->_pipelined_commands++; + return true; + } + + for ($i = 0; $i <= $this->_pipelined_commands; $i++) { + while ($line = $this->_socket->readLine()) { + $this->_debug("Recv: $line"); + + /* If we receive an empty line, the connection was closed. */ + if (empty($line)) { + $this->disconnect(); + return PEAR::raiseError('Connection was closed', + null, PEAR_ERROR_RETURN); + } + + /* 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; + } + } + } + + $this->_pipelined_commands = 0; + + /* Compare the server's response code with the valid code/codes. */ + if (is_int($valid) && ($this->_code === $valid)) { + return true; + } elseif (is_array($valid) && in_array($this->_code, $valid, true)) { + return true; + } + + return PEAR::raiseError('Invalid response code received from server', + $this->_code, PEAR_ERROR_RETURN); + } + + /** + * Issue an SMTP command and verify its response. + * + * @param string $command The SMTP command string or data. + * @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 on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function command($command, $valid) + { + if (PEAR::isError($error = $this->_put($command))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse($valid))) { + return $error; + } + + return true; + } + + /** + * 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)); + } + + /** + * Return the SMTP server's greeting string. + * + * @return string A string containing the greeting string, or null if a + * greeting has not been received. + * + * @access public + * @since 1.3.3 + */ + function getGreeting() + { + return $this->_greeting; + } + + /** + * Attempt to connect to the SMTP server. + * + * @param int $timeout The timeout value (in seconds) for the + * socket connection attempt. + * @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) + { + $this->_greeting = null; + $result = $this->_socket->connect($this->host, $this->port, + $persistent, $timeout, + $this->_socket_options); + if (PEAR::isError($result)) { + return PEAR::raiseError('Failed to connect socket: ' . + $result->getMessage()); + } + + /* + * Now that we're connected, reset the socket's timeout value for + * future I/O operations. This allows us to have different socket + * timeout values for the initial connection (our $timeout parameter) + * and all other socket operations. + */ + if ($this->_timeout > 0) { + if (PEAR::isError($error = $this->setTimeout($this->_timeout))) { + return $error; + } + } + + if (PEAR::isError($error = $this->_parseResponse(220))) { + return $error; + } + + /* Extract and store a copy of the server's greeting string. */ + list(, $this->_greeting) = $this->getResponse(); + + 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, + PEAR_ERROR_RETURN); + } + + 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; + } + + if (!isset($this->_esmtp['PIPELINING'])) { + $this->pipelining = false; + } + + 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 => $callback) { + if (in_array($method, $available_methods)) { + return $method; + } + } + + return PEAR::raiseError('No supported authentication methods', + null, PEAR_ERROR_RETURN); + } + + /** + * 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. + * @param bool Flag indicating whether or not TLS should be attempted. + * @param string An optional authorization identifier. If specified, this + * identifier will be used as the authorization proxy. + * + * @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 = '', $tls = true, $authz = '') + { + /* We can only attempt a TLS connection if one has been requested, + * we're running PHP 5.1.0 or later, have access to the OpenSSL + * extension, are connected to an SMTP server which supports the + * STARTTLS extension, and aren't already connected over a secure + * (SSL) socket connection. */ + if ($tls && version_compare(PHP_VERSION, '5.1.0', '>=') && + extension_loaded('openssl') && isset($this->_esmtp['STARTTLS']) && + strncasecmp($this->host, 'ssl://', 6) !== 0) { + /* Start the TLS connection attempt. */ + 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'); + } + + /* 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 (!array_key_exists($method, $this->auth_methods)) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + } + + if (!isset($this->auth_methods[$method])) { + return PEAR::raiseError("$method is not a supported authentication method"); + } + + if (!is_callable($this->auth_methods[$method], false)) { + return PEAR::raiseError("$method authentication method cannot be called"); + } + + if (is_array($this->auth_methods[$method])) { + list($object, $method) = $this->auth_methods[$method]; + $result = $object->{$method}($uid, $pwd, $authz, $this); + } else { + $func = $this->auth_methods[$method]; + $result = $func($uid, $pwd, $authz, $this); + } + + /* If an error was encountered, return the PEAR_Error object. */ + if (PEAR::isError($result)) { + return $result; + } + + return true; + } + + /** + * Add a new authentication method. + * + * @param string The authentication method name (e.g. 'PLAIN') + * @param mixed The authentication callback (given as the name of a + * function or as an (object, method name) array). + * @param bool Should the new method be prepended to the list of + * available methods? This is the default behavior, + * giving the new method the highest priority. + * + * @return mixed True on success or a PEAR_Error object on failure. + * + * @access public + * @since 1.6.0 + */ + function setAuthMethod($name, $callback, $prepend = true) + { + if (!is_string($name)) { + return PEAR::raiseError('Method name is not a string'); + } + + if (!is_string($callback) && !is_array($callback)) { + return PEAR::raiseError('Method callback must be string or array'); + } + + if (is_array($callback)) { + if (!is_object($callback[0]) || !is_string($callback[1])) + return PEAR::raiseError('Bad mMethod callback array'); + } + + if ($prepend) { + $this->auth_methods = array_merge(array($name => $callback), + $this->auth_methods); + } else { + $this->auth_methods[$name] = $callback; + } + + return true; + } + + /** + * Authenticates the user using the DIGEST-MD5 method. + * + * @param string The userid to authenticate as. + * @param string The password to authenticate with. + * @param string The optional authorization proxy identifier. + * + * @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, $authz = '') + { + 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", + $authz)); + + 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. + * @param string The optional authorization proxy identifier. + * + * @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, $authz = '') + { + 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. + * @param string The optional authorization proxy identifier. + * + * @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, $authz = '') + { + 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. + * @param string The optional authorization proxy identifier. + * + * @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, $authz = '') + { + 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($authz . 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; + } + + /** + * Return the list of SMTP service extensions advertised by the server. + * + * @return array The list of SMTP service extensions. + * @access public + * @since 1.3 + */ + function getServiceExtensions() + { + return $this->_esmtp; + } + + /** + * 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) && !empty($params)) { + $args .= ' ' . $params; + } + + if (PEAR::isError($error = $this->_put('MAIL', $args))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + 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), $this->pipelining))) { + 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 ($limit > 0 && $size >= $limit) { + $this->disconnect(); + return PEAR::raiseError('Message size exceeds server limit'); + } + + /* Initiate the DATA command. */ + if (PEAR::isError($error = $this->_put('DATA'))) { + return $error; + } + if (PEAR::isError($error = $this->_parseResponse(354))) { + return $error; + } + + /* If we have a separate headers string, send it first. */ + if (!is_null($headers)) { + $this->quotedata($headers); + if (PEAR::isError($result = $this->_send($headers . "\r\n\r\n"))) { + return $result; + } + } + + /* Now we can send the message body data. */ + if (is_resource($data)) { + /* Stream the contents of the file resource out over our socket + * connection, line by line. Each line must be run through the + * quoting routine. */ + while (strlen($line = fread($data, 8192)) > 0) { + /* If the last character is an newline, we need to grab the + * next character to check to see if it is a period. */ + while (!feof($data)) { + $char = fread($data, 1); + $line .= $char; + if ($char != "\n") { + break; + } + } + $this->quotedata($line); + if (PEAR::isError($result = $this->_send($line))) { + return $result; + } + } + } else { + /* + * Break up the data by sending one chunk (up to 512k) at a time. + * This approach reduces our peak memory usage. + */ + for ($offset = 0; $offset < $size;) { + $end = $offset + 512000; + + /* + * Ensure we don't read beyond our data size or span multiple + * lines. quotedata() can't properly handle character data + * that's split across two line break boundaries. + */ + if ($end >= $size) { + $end = $size; + } else { + for (; $end < $size; $end++) { + if ($data[$end] != "\n") { + break; + } + } + } + + /* Extract our chunk and run it through the quoting routine. */ + $chunk = substr($data, $offset, $end - $offset); + $this->quotedata($chunk); + + /* If we run into a problem along the way, abort. */ + if (PEAR::isError($result = $this->_send($chunk))) { + return $result; + } + + /* Advance the offset to the end of this chunk. */ + $offset = $end; + } + } + + /* Finally, send the DATA terminator sequence. */ + if (PEAR::isError($result = $this->_send("\r\n.\r\n"))) { + return $result; + } + + /* Verify that the data was successfully received by the server. */ + if (PEAR::isError($error = $this->_parseResponse(250, $this->pipelining))) { + 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, $this->pipelining))) { + 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, $this->pipelining))) { + 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, $this->pipelining))) { + 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, $this->pipelining))) { + 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..8a5ca5f --- /dev/null +++ b/Net/Socket.php @@ -0,0 +1,653 @@ + + * Chuck Hagenbuch + * + * @category Net + * @package Net_Socket + * @author Stig Bakken + * @author Chuck Hagenbuch + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @version CVS: $Id: Socket.php 304428 2010-10-15 13:51:46Z clockwerx $ + * @link http://pear.php.net/packages/Net_Socket + */ + +require_once 'PEAR.php'; + +define('NET_SOCKET_READ', 1); +define('NET_SOCKET_WRITE', 2); +define('NET_SOCKET_ERROR', 4); + +/** + * Generalized Socket class. + * + * @category Net + * @package Net_Socket + * @author Stig Bakken + * @author Chuck Hagenbuch + * @copyright 1997-2003 The PHP Group + * @license http://www.php.net/license/2_02.txt PHP 2.02 + * @link http://pear.php.net/packages/Net_Socket + */ +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; + + /** + * The string to use as a newline terminator. Usually "\r\n" or "\n". + * @var string $newline + */ + var $newline = "\r\n"; + + /** + * 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 = ''; + + $old_track_errors = @ini_set('track_errors', 1); + + if ($options && function_exists('stream_context_create')) { + if ($this->timeout) { + $timeout = $this->timeout; + } else { + $timeout = 0; + } + $context = stream_context_create($options); + + // Since PHP 5 fsockopen doesn't allow context specification + if (function_exists('stream_socket_client')) { + $flags = STREAM_CLIENT_CONNECT; + + if ($this->persistent) { + $flags = STREAM_CLIENT_PERSISTENT; + } + + $addr = $this->addr . ':' . $this->port; + $fp = stream_socket_client($addr, $errno, $errstr, + $timeout, $flags, $context); + } else { + $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) { + if ($errno == 0 && !strlen($errstr) && isset($php_errormsg)) { + $errstr = $php_errormsg; + } + @ini_set('track_errors', $old_track_errors); + return $this->raiseError($errstr, $errno); + } + + @ini_set('track_errors', $old_track_errors); + $this->fp = $fp; + + return $this->setBlocking($this->blocking); + } + + /** + * Disconnects from the peer, closes the socket. + * + * @access public + * @return mixed true on success or a PEAR_Error instance otherwise + */ + function disconnect() + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + @fclose($this->fp); + $this->fp = null; + return true; + } + + /** + * Set the newline character/sequence to use. + * + * @param string $newline Newline character(s) + * @return boolean True + */ + function setNewline($newline) + { + $this->newline = $newline; + 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 a PEAR_Error instance otherwise + */ + function setBlocking($mode) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + $this->blocking = $mode; + stream_set_blocking($this->fp, (int)$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 a PEAR_Error instance 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, $size); + 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 a PEAR_Error instance 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 + * + * @param int $size ?? + * + * @access public + * @return $size bytes of data from the socket, or a PEAR_Error if + * not connected. + */ + function gets($size = null) + { + if (!is_resource($this->fp)) { + return $this->raiseError('not connected'); + } + + if (is_null($size)) { + return @fgets($this->fp); + } else { + 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 If the socket is not connected, returns an instance of + * PEAR_Error + * If the write succeeds, returns the number of bytes written + * If the write fails, returns false. + */ + 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) { + return $written; + } + $pos += $written; + } + + return $pos; + } + } + + /** + * Write a line of data to the socket, followed by a trailing newline. + * + * @param string $data Data to write + * + * @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 . $this->newline); + } + + /** + * 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 formatted string + * + * @access public + * @return Dot formatted 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('%d.%d.%d.%d', 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, $this->newline); + } + } + 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 stream_socket_enable_crypto() + * for values. + * + * @see http://se.php.net/manual/en/function.stream-socket-enable-crypto.php + * @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 { + $msg = 'Net_Socket::enableCrypto() requires php version >= 5.1.0'; + return $this->raiseError($msg); + } + } + +} diff --git a/Net/URL2.php b/Net/URL2.php new file mode 100644 index 0000000..9989404 --- /dev/null +++ b/Net/URL2.php @@ -0,0 +1,942 @@ + + * @copyright 2007-2009 Peytz & Co. A/S + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: URL2.php 309223 2011-03-14 14:26:32Z till $ + * @link http://www.rfc-editor.org/rfc/rfc3986.txt + */ + +/** + * Represents a URL as per RFC 3986. + * + * @category Networking + * @package Net_URL2 + * @author Christian Schmidt + * @copyright 2007-2009 Peytz & Co. A/S + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/Net_URL2 + */ +class Net_URL2 +{ + /** + * Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default + * is true. + */ + const OPTION_STRICT = 'strict'; + + /** + * Represent arrays in query using PHP's [] notation. Default is true. + */ + const OPTION_USE_BRACKETS = 'use_brackets'; + + /** + * URL-encode query variable keys. Default is true. + */ + const OPTION_ENCODE_KEYS = 'encode_keys'; + + /** + * Query variable separators when parsing the query string. Every character + * is considered a separator. Default is "&". + */ + const OPTION_SEPARATOR_INPUT = 'input_separator'; + + /** + * Query variable separator used when generating the query string. Default + * is "&". + */ + const OPTION_SEPARATOR_OUTPUT = 'output_separator'; + + /** + * Default options corresponds to how PHP handles $_GET. + */ + private $_options = array( + self::OPTION_STRICT => true, + self::OPTION_USE_BRACKETS => true, + self::OPTION_ENCODE_KEYS => true, + self::OPTION_SEPARATOR_INPUT => '&', + self::OPTION_SEPARATOR_OUTPUT => '&', + ); + + /** + * @var string|bool + */ + private $_scheme = false; + + /** + * @var string|bool + */ + private $_userinfo = false; + + /** + * @var string|bool + */ + private $_host = false; + + /** + * @var string|bool + */ + private $_port = false; + + /** + * @var string + */ + private $_path = ''; + + /** + * @var string|bool + */ + private $_query = false; + + /** + * @var string|bool + */ + private $_fragment = false; + + /** + * Constructor. + * + * @param string $url an absolute or relative URL + * @param array $options an array of OPTION_xxx constants + * + * @return $this + * @uses self::parseUrl() + */ + public function __construct($url, array $options = array()) + { + foreach ($options as $optionName => $value) { + if (array_key_exists($optionName, $this->_options)) { + $this->_options[$optionName] = $value; + } + } + + $this->parseUrl($url); + } + + /** + * Magic Setter. + * + * This method will magically set the value of a private variable ($var) + * with the value passed as the args + * + * @param string $var The private variable to set. + * @param mixed $arg An argument of any type. + * @return void + */ + public function __set($var, $arg) + { + $method = 'set' . $var; + if (method_exists($this, $method)) { + $this->$method($arg); + } + } + + /** + * Magic Getter. + * + * This is the magic get method to retrieve the private variable + * that was set by either __set() or it's setter... + * + * @param string $var The property name to retrieve. + * @return mixed $this->$var Either a boolean false if the + * property is not set or the value + * of the private property. + */ + public function __get($var) + { + $method = 'get' . $var; + if (method_exists($this, $method)) { + return $this->$method(); + } + + return false; + } + + /** + * Returns the scheme, e.g. "http" or "urn", or false if there is no + * scheme specified, i.e. if this is a relative URL. + * + * @return string|bool + */ + public function getScheme() + { + return $this->_scheme; + } + + /** + * Sets the scheme, e.g. "http" or "urn". Specify false if there is no + * scheme specified, i.e. if this is a relative URL. + * + * @param string|bool $scheme e.g. "http" or "urn", or false if there is no + * scheme specified, i.e. if this is a relative + * URL + * + * @return $this + * @see getScheme() + */ + public function setScheme($scheme) + { + $this->_scheme = $scheme; + return $this; + } + + /** + * Returns the user part of the userinfo part (the part preceding the first + * ":"), or false if there is no userinfo part. + * + * @return string|bool + */ + public function getUser() + { + return $this->_userinfo !== false + ? preg_replace('@:.*$@', '', $this->_userinfo) + : false; + } + + /** + * Returns the password part of the userinfo part (the part after the first + * ":"), or false if there is no userinfo part (i.e. the URL does not + * contain "@" in front of the hostname) or the userinfo part does not + * contain ":". + * + * @return string|bool + */ + public function getPassword() + { + return $this->_userinfo !== false + ? substr(strstr($this->_userinfo, ':'), 1) + : false; + } + + /** + * Returns the userinfo part, or false if there is none, i.e. if the + * authority part does not contain "@". + * + * @return string|bool + */ + public function getUserinfo() + { + return $this->_userinfo; + } + + /** + * Sets the userinfo part. If two arguments are passed, they are combined + * in the userinfo part as username ":" password. + * + * @param string|bool $userinfo userinfo or username + * @param string|bool $password optional password, or false + * + * @return $this + */ + public function setUserinfo($userinfo, $password = false) + { + $this->_userinfo = $userinfo; + if ($password !== false) { + $this->_userinfo .= ':' . $password; + } + return $this; + } + + /** + * Returns the host part, or false if there is no authority part, e.g. + * relative URLs. + * + * @return string|bool a hostname, an IP address, or false + */ + public function getHost() + { + return $this->_host; + } + + /** + * Sets the host part. Specify false if there is no authority part, e.g. + * relative URLs. + * + * @param string|bool $host a hostname, an IP address, or false + * + * @return $this + */ + public function setHost($host) + { + $this->_host = $host; + return $this; + } + + /** + * Returns the port number, or false if there is no port number specified, + * i.e. if the default port is to be used. + * + * @return string|bool + */ + public function getPort() + { + return $this->_port; + } + + /** + * Sets the port number. Specify false if there is no port number specified, + * i.e. if the default port is to be used. + * + * @param string|bool $port a port number, or false + * + * @return $this + */ + public function setPort($port) + { + $this->_port = $port; + return $this; + } + + /** + * Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or + * false if there is no authority. + * + * @return string|bool + */ + public function getAuthority() + { + if (!$this->_host) { + return false; + } + + $authority = ''; + + if ($this->_userinfo !== false) { + $authority .= $this->_userinfo . '@'; + } + + $authority .= $this->_host; + + if ($this->_port !== false) { + $authority .= ':' . $this->_port; + } + + return $authority; + } + + /** + * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify + * false if there is no authority. + * + * @param string|false $authority a hostname or an IP addresse, possibly + * with userinfo prefixed and port number + * appended, e.g. "foo:bar@example.org:81". + * + * @return $this + */ + public function setAuthority($authority) + { + $this->_userinfo = false; + $this->_host = false; + $this->_port = false; + if (preg_match('@^(([^\@]*)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) { + if ($reg[1]) { + $this->_userinfo = $reg[2]; + } + + $this->_host = $reg[3]; + if (isset($reg[5])) { + $this->_port = $reg[5]; + } + } + return $this; + } + + /** + * Returns the path part (possibly an empty string). + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Sets the path part (possibly an empty string). + * + * @param string $path a path + * + * @return $this + */ + public function setPath($path) + { + $this->_path = $path; + return $this; + } + + /** + * Returns the query string (excluding the leading "?"), or false if "?" + * is not present in the URL. + * + * @return string|bool + * @see self::getQueryVariables() + */ + public function getQuery() + { + return $this->_query; + } + + /** + * Sets the query string (excluding the leading "?"). Specify false if "?" + * is not present in the URL. + * + * @param string|bool $query a query string, e.g. "foo=1&bar=2" + * + * @return $this + * @see self::setQueryVariables() + */ + public function setQuery($query) + { + $this->_query = $query; + return $this; + } + + /** + * Returns the fragment name, or false if "#" is not present in the URL. + * + * @return string|bool + */ + public function getFragment() + { + return $this->_fragment; + } + + /** + * Sets the fragment name. Specify false if "#" is not present in the URL. + * + * @param string|bool $fragment a fragment excluding the leading "#", or + * false + * + * @return $this + */ + public function setFragment($fragment) + { + $this->_fragment = $fragment; + return $this; + } + + /** + * Returns the query string like an array as the variables would appear in + * $_GET in a PHP script. If the URL does not contain a "?", an empty array + * is returned. + * + * @return array + */ + public function getQueryVariables() + { + $pattern = '/[' . + preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') . + ']/'; + $parts = preg_split($pattern, $this->_query, -1, PREG_SPLIT_NO_EMPTY); + $return = array(); + + foreach ($parts as $part) { + if (strpos($part, '=') !== false) { + list($key, $value) = explode('=', $part, 2); + } else { + $key = $part; + $value = null; + } + + if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + $key = rawurldecode($key); + } + $value = rawurldecode($value); + + if ($this->getOption(self::OPTION_USE_BRACKETS) && + 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->getOption(self::OPTION_USE_BRACKETS) + && !empty($return[$key]) + ) { + $return[$key] = (array) $return[$key]; + $return[$key][] = $value; + } else { + $return[$key] = $value; + } + } + + return $return; + } + + /** + * Sets the query string to the specified variable in the query string. + * + * @param array $array (name => value) array + * + * @return $this + */ + public function setQueryVariables(array $array) + { + if (!$array) { + $this->_query = false; + } else { + $this->_query = $this->buildQuery( + $array, + $this->getOption(self::OPTION_SEPARATOR_OUTPUT) + ); + } + return $this; + } + + /** + * Sets the specified variable in the query string. + * + * @param string $name variable name + * @param mixed $value variable value + * + * @return $this + */ + public function setQueryVariable($name, $value) + { + $array = $this->getQueryVariables(); + $array[$name] = $value; + $this->setQueryVariables($array); + return $this; + } + + /** + * Removes the specifed variable from the query string. + * + * @param string $name a query string variable, e.g. "foo" in "?foo=1" + * + * @return void + */ + public function unsetQueryVariable($name) + { + $array = $this->getQueryVariables(); + unset($array[$name]); + $this->setQueryVariables($array); + } + + /** + * Returns a string representation of this URL. + * + * @return string + */ + public function getURL() + { + // See RFC 3986, section 5.3 + $url = ""; + + if ($this->_scheme !== false) { + $url .= $this->_scheme . ':'; + } + + $authority = $this->getAuthority(); + if ($authority !== false) { + $url .= '//' . $authority; + } + $url .= $this->_path; + + if ($this->_query !== false) { + $url .= '?' . $this->_query; + } + + if ($this->_fragment !== false) { + $url .= '#' . $this->_fragment; + } + + return $url; + } + + /** + * Returns a string representation of this URL. + * + * @return string + * @see toString() + */ + public function __toString() + { + return $this->getURL(); + } + + /** + * Returns a normalized string representation of this URL. This is useful + * for comparison of URLs. + * + * @return string + */ + public function getNormalizedURL() + { + $url = clone $this; + $url->normalize(); + return $url->getUrl(); + } + + /** + * Returns a normalized Net_URL2 instance. + * + * @return Net_URL2 + */ + public function normalize() + { + // See RFC 3886, section 6 + + // Schemes are case-insensitive + if ($this->_scheme) { + $this->_scheme = strtolower($this->_scheme); + } + + // Hostnames are case-insensitive + if ($this->_host) { + $this->_host = strtolower($this->_host); + } + + // Remove default port number for known schemes (RFC 3986, section 6.2.3) + if ($this->_port && + $this->_scheme && + $this->_port == getservbyname($this->_scheme, 'tcp')) { + + $this->_port = false; + } + + // Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) + foreach (array('_userinfo', '_host', '_path') as $part) { + if ($this->$part) { + $this->$part = preg_replace('/%[0-9a-f]{2}/ie', + 'strtoupper("\0")', + $this->$part); + } + } + + // Path segment normalization (RFC 3986, section 6.2.2.3) + $this->_path = self::removeDotSegments($this->_path); + + // Scheme based normalization (RFC 3986, section 6.2.3) + if ($this->_host && !$this->_path) { + $this->_path = '/'; + } + } + + /** + * Returns whether this instance represents an absolute URL. + * + * @return bool + */ + public function isAbsolute() + { + return (bool) $this->_scheme; + } + + /** + * Returns an Net_URL2 instance representing an absolute URL relative to + * this URL. + * + * @param Net_URL2|string $reference relative URL + * + * @return Net_URL2 + */ + public function resolve($reference) + { + if (!$reference instanceof Net_URL2) { + $reference = new self($reference); + } + if (!$this->isAbsolute()) { + throw new Exception('Base-URL must be absolute'); + } + + // A non-strict parser may ignore a scheme in the reference if it is + // identical to the base URI's scheme. + if (!$this->getOption(self::OPTION_STRICT) && $reference->_scheme == $this->_scheme) { + $reference->_scheme = false; + } + + $target = new self(''); + if ($reference->_scheme !== false) { + $target->_scheme = $reference->_scheme; + $target->setAuthority($reference->getAuthority()); + $target->_path = self::removeDotSegments($reference->_path); + $target->_query = $reference->_query; + } else { + $authority = $reference->getAuthority(); + if ($authority !== false) { + $target->setAuthority($authority); + $target->_path = self::removeDotSegments($reference->_path); + $target->_query = $reference->_query; + } else { + if ($reference->_path == '') { + $target->_path = $this->_path; + if ($reference->_query !== false) { + $target->_query = $reference->_query; + } else { + $target->_query = $this->_query; + } + } else { + if (substr($reference->_path, 0, 1) == '/') { + $target->_path = self::removeDotSegments($reference->_path); + } else { + // Merge paths (RFC 3986, section 5.2.3) + if ($this->_host !== false && $this->_path == '') { + $target->_path = '/' . $this->_path; + } else { + $i = strrpos($this->_path, '/'); + if ($i !== false) { + $target->_path = substr($this->_path, 0, $i + 1); + } + $target->_path .= $reference->_path; + } + $target->_path = self::removeDotSegments($target->_path); + } + $target->_query = $reference->_query; + } + $target->setAuthority($this->getAuthority()); + } + $target->_scheme = $this->_scheme; + } + + $target->_fragment = $reference->_fragment; + + return $target; + } + + /** + * Removes dots as described in RFC 3986, section 5.2.4, e.g. + * "/foo/../bar/baz" => "/bar/baz" + * + * @param string $path a path + * + * @return string a path + */ + public static function removeDotSegments($path) + { + $output = ''; + + // Make sure not to be trapped in an infinite loop due to a bug in this + // method + $j = 0; + while ($path && $j++ < 100) { + if (substr($path, 0, 2) == './') { + // Step 2.A + $path = substr($path, 2); + } elseif (substr($path, 0, 3) == '../') { + // Step 2.A + $path = substr($path, 3); + } elseif (substr($path, 0, 3) == '/./' || $path == '/.') { + // Step 2.B + $path = '/' . substr($path, 3); + } elseif (substr($path, 0, 4) == '/../' || $path == '/..') { + // Step 2.C + $path = '/' . substr($path, 4); + $i = strrpos($output, '/'); + $output = $i === false ? '' : substr($output, 0, $i); + } elseif ($path == '.' || $path == '..') { + // Step 2.D + $path = ''; + } else { + // Step 2.E + $i = strpos($path, '/'); + if ($i === 0) { + $i = strpos($path, '/', 1); + } + if ($i === false) { + $i = strlen($path); + } + $output .= substr($path, 0, $i); + $path = substr($path, $i); + } + } + + return $output; + } + + /** + * Percent-encodes all non-alphanumeric characters except these: _ . - ~ + * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP + * 5.2.x and earlier. + * + * @param $raw the string to encode + * @return string + */ + public static function urlencode($string) + { + $encoded = rawurlencode($string); + + // This is only necessary in PHP < 5.3. + $encoded = str_replace('%7E', '~', $encoded); + return $encoded; + } + + /** + * Returns a Net_URL2 instance representing the canonical URL of the + * currently executing PHP script. + * + * @return string + */ + public static function getCanonical() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['PHP_SELF']); + $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + $url->_host = $_SERVER['SERVER_NAME']; + $port = $_SERVER['SERVER_PORT']; + if ($url->_scheme == 'http' && $port != 80 || + $url->_scheme == 'https' && $port != 443) { + + $url->_port = $port; + } + return $url; + } + + /** + * Returns the URL used to retrieve the current request. + * + * @return string + */ + public static function getRequestedURL() + { + return self::getRequested()->getUrl(); + } + + /** + * Returns a Net_URL2 instance representing the URL used to retrieve the + * current request. + * + * @return Net_URL2 + */ + public static function getRequested() + { + if (!isset($_SERVER['REQUEST_METHOD'])) { + // ALERT - no current URL + throw new Exception('Script was not called through a webserver'); + } + + // Begin with a relative URL + $url = new self($_SERVER['REQUEST_URI']); + $url->_scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http'; + // Set host and possibly port + $url->setAuthority($_SERVER['HTTP_HOST']); + return $url; + } + + /** + * Returns the value of the specified option. + * + * @param string $optionName The name of the option to retrieve + * + * @return mixed + */ + public function getOption($optionName) + { + return isset($this->_options[$optionName]) + ? $this->_options[$optionName] : false; + } + + /** + * A simple version of http_build_query in userland. The encoded string is + * percentage encoded according to RFC 3986. + * + * @param array $data An array, which has to be converted into + * QUERY_STRING. Anything is possible. + * @param string $seperator See {@link self::OPTION_SEPARATOR_OUTPUT} + * @param string $key For stacked values (arrays in an array). + * + * @return string + */ + protected function buildQuery(array $data, $separator, $key = null) + { + $query = array(); + foreach ($data as $name => $value) { + if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) { + $name = rawurlencode($name); + } + if ($key !== null) { + if ($this->getOption(self::OPTION_USE_BRACKETS) === true) { + $name = $key . '[' . $name . ']'; + } else { + $name = $key; + } + } + if (is_array($value)) { + $query[] = $this->buildQuery($value, $separator, $name); + } else { + $query[] = $name . '=' . rawurlencode($value); + } + } + return implode($separator, $query); + } + + /** + * This method uses a funky regex to parse the url into the designated parts. + * + * @param string $url + * + * @return void + * @uses self::$_scheme, self::setAuthority(), self::$_path, self::$_query, + * self::$_fragment + * @see self::__construct() + */ + protected function parseUrl($url) + { + // The regular expression is copied verbatim from RFC 3986, appendix B. + // The expression does not validate the URL but matches any string. + preg_match('!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!', + $url, + $matches); + + // "path" is always present (possibly as an empty string); the rest + // are optional. + $this->_scheme = !empty($matches[1]) ? $matches[2] : false; + $this->setAuthority(!empty($matches[3]) ? $matches[4] : false); + $this->_path = $matches[5]; + $this->_query = !empty($matches[6]) ? $matches[7] : false; + $this->_fragment = !empty($matches[8]) ? $matches[9] : false; + } +} diff --git a/PEAR/Autoloader.php b/PEAR/Autoloader.php new file mode 100644 index 0000000..0ed707e --- /dev/null +++ b/PEAR/Autoloader.php @@ -0,0 +1,218 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Autoloader.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..90f3a14 --- /dev/null +++ b/PEAR/Builder.php @@ -0,0 +1,489 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Builder.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 +{ + 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; + + /** + * 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 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)) { + return $this->raiseError("could not chdir to " . realpath($vdir)); + } + + $dir = getcwd(); + } + + $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); + } + + /** + * @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 an extension from source. Runs "phpize" in the source + * directory, but compiles in a temporary directory + * (TMPDIR/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) + { + if (preg_match('/(\\/|\\\\|^)([^\\/\\\\]+)?php(.+)?$/', + $this->config->get('php_bin'), $matches)) { + if (isset($matches[2]) && strlen($matches[2]) && + trim($matches[2]) != trim($this->config->get('php_prefix'))) { + $this->log(0, 'WARNING: php_bin ' . $this->config->get('php_bin') . + ' appears to have a prefix ' . $matches[2] . ', but' . + ' config variable php_prefix does not match'); + } + + if (isset($matches[3]) && strlen($matches[3]) && + trim($matches[3]) != trim($this->config->get('php_suffix'))) { + $this->log(0, 'WARNING: php_bin ' . $this->config->get('php_bin') . + ' appears to have a suffix ' . $matches[3] . ', but' . + ' config variable php_suffix does not match'); + } + } + + $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(); + if (is_a($pkg, 'PEAR_PackageFile_v1')) { + $dir = dirname($descfile); + } else { + $dir = $pkg->_config->get('temp_dir') . '/' . $pkg->getName(); + // automatically delete at session end + $this->addTempFile($dir); + } + } else { + $pf = &new PEAR_PackageFile($this->config); + $pkg = &$pf->fromPackageFile($descfile, PEAR_VALIDATE_NORMAL); + if (PEAR::isError($pkg)) { + return $pkg; + } + $dir = dirname($descfile); + } + + // Find config. outside of normal path - e.g. config.m4 + foreach (array_keys($pkg->getInstallationFileList()) as $item) { + if (stristr(basename($item), 'config.m4') && dirname($item) != '.') { + $dir .= DIRECTORY_SEPARATOR . dirname($item); + break; + } + } + + $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($this->config->get('php_prefix') + . "phpize" . + $this->config->get('php_suffix'), + 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'; + } + + $tmpdir = $this->config->get('temp_dir'); + $build_basedir = System::mktemp(' -t "' . $tmpdir . '" -d "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); + + $make_command = getenv('MAKE') ? getenv('MAKE') : 'make'; + + $to_run = array( + $configure_command, + $make_command, + "$make_command INSTALL_ROOT=\"$inst_dir\" install", + "find \"$inst_dir\" | xargs ls -dils" + ); + 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.9.4'); + 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($this->config->get('php_prefix') + . "php-config" . + $this->config->get('php_suffix') . " --prefix"); + $this->_harvestInstDir($prefix, $inst_dir . DIRECTORY_SEPARATOR . $prefix, $built_files); + chdir($old_cwd); + return $built_files; + } + + /** + * 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); + } + } + } + + /** + * 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; + } + + $exitcode = is_resource($pp) ? pclose($pp) : -1; + return ($exitcode == 0); + } + + 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); + } +} \ No newline at end of file diff --git a/PEAR/ChannelFile.php b/PEAR/ChannelFile.php new file mode 100644 index 0000000..f2c02ab --- /dev/null +++ b/PEAR/ChannelFile.php @@ -0,0 +1,1559 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: ChannelFile.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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 protocol + */ +define('PEAR_CHANNELFILE_ERROR_NO_HOST', 10); +/** + * Error code when channel server is invalid for 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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 .= ' _makeRestXml($channelInfo['servers']['primary']['rest'], ' '); + } + $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 _makeRestXml($info, $indent) + { + $ret = $indent . "\n"; + if (isset($info['baseurl']) && !isset($info['baseurl'][0])) { + $info['baseurl'] = array($info['baseurl']); + } + + if (isset($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 .= ' _makeRestXml($mirror['rest'], ' '); + } + $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'], $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'], $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']['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['rest'])) { + $this->_validateFunctions('rest', $mirror['rest']['baseurl'], + $mirror['attribs']['host']); + } + } + } + return $this->_isValid; + } + + /** + * @param string rest - 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_NOBASEURLTYPE, + 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']; + } + + return false; + } + + /** + * @return string|false + */ + function getServer() + { + if (isset($this->_channelInfo['name'])) { + return $this->_channelInfo['name']; + } + + 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']; + } + + 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; + } + + 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']; + } + + return false; + } + + /** + * @param string protocol type + * @param string Mirror name + * @return array|false + */ + function getFunctions($protocol, $mirror = false) + { + if ($this->getName() == '__uri') { + return false; + } + + $function = $protocol == 'rest' ? 'baseurl' : '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]; + } + + 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) { + $mir = $this->getMirror($mirror); + if (!$mir) { + return false; + } + + $rest = $mir['rest']; + } 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 + * @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; + } + + return false; + } + + 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->resetREST($mirror); + + if (!isset($this->_channelInfo['servers'])) { + $this->_channelInfo['servers'] = array('primary' => + array('rest' => array())); + } elseif (!isset($this->_channelInfo['servers']['primary'])) { + $this->_channelInfo['servers']['primary'] = array('rest' => array()); + } + + 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; + } + + 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; + } + + /** + * @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; + } + + 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(); + } +} \ No newline at end of file diff --git a/PEAR/ChannelFile/Parser.php b/PEAR/ChannelFile/Parser.php new file mode 100644 index 0000000..e630ace --- /dev/null +++ b/PEAR/ChannelFile/Parser.php @@ -0,0 +1,68 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Parser.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..db39b8f --- /dev/null +++ b/PEAR/Command.php @@ -0,0 +1,414 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Command.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 ($file = readdir($dp)) { + if ($file{0} == '.' || substr($file, -4) != '.xml') { + continue; + } + + $f = substr($file, 0, -4); + $class = "PEAR_Command_" . $f; + // List of commands + if (empty($GLOBALS['_PEAR_Command_objects'][$class])) { + $GLOBALS['_PEAR_Command_objects'][$class] = "$dir/" . $f . '.php'; + } + + $parser->parse(file_get_contents("$dir/$file")); + $implements = $parser->getData(); + 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; + } + // }}} +} \ No newline at end of file diff --git a/PEAR/Command/Auth.php b/PEAR/Command/Auth.php new file mode 100644 index 0000000..63cd152 --- /dev/null +++ b/PEAR/Command/Auth.php @@ -0,0 +1,81 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Auth.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + * @deprecated since 1.8.0alpha1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Channels.php'; + +/** + * PEAR commands for login/logout + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + * @deprecated since 1.8.0alpha1 + */ +class PEAR_Command_Auth extends PEAR_Command_Channels +{ + var $commands = array( + 'login' => array( + 'summary' => 'Connects and authenticates to remote server [Deprecated in favor of channel-login]', + 'shortcut' => 'li', + 'function' => 'doLogin', + 'options' => array(), + 'doc' => ' +WARNING: This function is deprecated in favor of using channel-login + +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 [Deprecated in favor of channel-logout]', + 'shortcut' => 'lo', + 'function' => 'doLogout', + 'options' => array(), + 'doc' => ' +WARNING: This function is deprecated in favor of using channel-logout + +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.', + ) + + ); + + /** + * PEAR_Command_Auth constructor. + * + * @access public + */ + function PEAR_Command_Auth(&$ui, &$config) + { + parent::PEAR_Command_Channels($ui, $config); + } +} \ No newline at end of file diff --git a/PEAR/Command/Auth.xml b/PEAR/Command/Auth.xml new file mode 100644 index 0000000..590193d --- /dev/null +++ b/PEAR/Command/Auth.xml @@ -0,0 +1,30 @@ + + + Connects and authenticates to remote server [Deprecated in favor of channel-login] + doLogin + li + + <channel name> +WARNING: This function is deprecated in favor of using channel-login + +Log in to a remote channel server. If <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 [Deprecated in favor of channel-logout] + doLogout + lo + + +WARNING: This function is deprecated in favor of using channel-logout + +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..1de7320 --- /dev/null +++ b/PEAR/Command/Build.php @@ -0,0 +1,85 @@ + + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Build.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Build extends PEAR_Command_Common +{ + 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.' + ), + ); + + /** + * PEAR_Command_Build constructor. + * + * @access public + */ + function PEAR_Command_Build(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + 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; + } + + function buildCallback($what, $data) + { + if (($what == 'cmdoutput' && $this->debug > 1) || + ($what == 'output' && $this->debug > 0)) { + $this->ui->outputData(rtrim($data), 'build'); + } + } +} \ No newline at end of file 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..fcf01b5 --- /dev/null +++ b/PEAR/Command/Channels.php @@ -0,0 +1,883 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Channels.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * base class + */ +require_once 'PEAR/Command/Common.php'; + +define('PEAR_COMMAND_CHANNELS_CHANNEL_EXISTS', -500); + +/** + * PEAR commands for managing channels. + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Command_Channels extends PEAR_Command_Common +{ + 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. +' + ), + 'channel-login' => array( + 'summary' => 'Connects and authenticates to remote channel server', + 'shortcut' => 'cli', + '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.', + ), + 'channel-logout' => array( + 'summary' => 'Logs out from the remote channel server', + 'shortcut' => 'clo', + 'function' => 'doLogout', + 'options' => array(), + 'doc' => ' +Logs out from a remote channel server. If is not supplied, +the default channel is used. This command does not actually connect to the +remote server, it only deletes the stored username and password from your user +configuration.', + ), + ); + + /** + * PEAR_Command_Registry constructor. + * + * @access public + */ + function PEAR_Command_Channels(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + 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', 'Alias', 'Summary') + ); + foreach ($registered as $channel) { + $data['data'][] = array($channel->getName(), + $channel->getAlias(), + $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 (count($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])) { + return $this->raiseError('Unknown channel "' . $channel . '"'); + } + + $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'; + } + + $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) { + return $this->raiseError('Serious error: Channel "' . $params[0] . + '" has a corrupted registry entry'); + } + + $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'); + if ($chan->supportsREST()) { + 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'); + if ($chan->supportsREST($mirror['attribs']['host'])) { + 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'); + } + + // }}} + + function doDelete($command, $options, $params) + { + if (count($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 == 'doc.php.net') { + return $this->raiseError('Cannot delete the doc.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 (count($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() . ')'); + } + + 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', PEAR_COMMAND_CHANNELS_CHANNEL_EXISTS); + } + + $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) + { + if (count($params) !== 1) { + return $this->raiseError("No channel file specified"); + } + + $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(); + $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)) { + // Attempt to fall back to https + $this->ui->outputData("Channel \"$params[0]\" is not responding over http://, failed with message: " . $contents->getMessage()); + $this->ui->outputData("Trying channel \"$params[0]\" over https:// instead"); + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $contents = $dl->downloadHttp('https://' . $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'])) { + return $this->raiseError('ERROR: downloaded channel definition file' . + ' for channel "' . $channel->getName() . '" from channel "' . + strtolower($c->getName()) . '"'); + } + + $this->ui->log(0, 'WARNING: 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() . ')'); + } + + 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) + { + if (count($params) === 1) { + return $this->raiseError('No channel alias specified'); + } + + if (count($params) !== 2 || (!empty($params[1]) && $params[1]{0} == '-')) { + return $this->raiseError( + 'Invalid format, correct is: channel-alias channel alias'); + } + + $reg = &$this->config->getRegistry(); + if (!$reg->channelExists($params[0], true)) { + $extra = ''; + if ($reg->isAlias($params[0])) { + $extra = ' (use "channel-alias ' . $reg->channelName($params[0]) . ' ' . + strtolower($params[1]) . '")'; + } + + 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) + { + if (count($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]; + } + + $reg = &$this->config->getRegistry(); + if ($reg->channelExists($channel)) { + if (!$reg->isAlias($channel)) { + return $this->raiseError("Channel \"$channel\" is already initialized", PEAR_COMMAND_CHANNELS_CHANNEL_EXISTS); + } + + return $this->raiseError("A channel alias named \"$channel\" " . + 'already exists, aliasing channel "' . $reg->channelName($channel) + . '"'); + } + + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->doAdd($command, $options, array('http://' . $channel . '/channel.xml')); + $this->popErrorHandling(); + if (PEAR::isError($err)) { + if ($err->getCode() === PEAR_COMMAND_CHANNELS_CHANNEL_EXISTS) { + return $this->raiseError("Discovery of channel \"$channel\" failed (" . + $err->getMessage() . ')'); + } + // Attempt fetch via https + $this->ui->outputData("Discovering channel $channel over http:// failed with message: " . $err->getMessage()); + $this->ui->outputData("Trying to discover channel $channel over https:// instead"); + $this->pushErrorHandling(PEAR_ERROR_RETURN); + $err = $this->doAdd($command, $options, array('https://' . $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); + } + + /** + * 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 + $channel = isset($params[0]) ? $params[0] : $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); + $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; + } + + if ($ok !== true) { + return $this->raiseError('Login failed!'); + } + + $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(); + + return true; + } + + /** + * 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(); + + // If a parameter is supplied, use that as the channel to log in to + $channel = isset($params[0]) ? $params[0] : $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); + $this->ui->outputData("Logging out from $server.", $command); + $this->config->remove('username', 'user', $channel); + $this->config->remove('password', 'user', $channel); + $this->config->store(); + return true; + } +} \ No newline at end of file diff --git a/PEAR/Command/Channels.xml b/PEAR/Command/Channels.xml new file mode 100644 index 0000000..47b7206 --- /dev/null +++ b/PEAR/Command/Channels.xml @@ -0,0 +1,123 @@ + + + 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 + will force download of new channel.xml if an existing channel name is used + CHANNEL + + + [<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. + + + + Connects and authenticates to remote channel server + doLogin + cli + + <channel name> +Log in to a remote channel server. If <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 channel server + doLogout + clo + + <channel name> +Logs out from a remote channel server. If <channel name> is not supplied, +the default channel is used. 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/Common.php b/PEAR/Command/Common.php new file mode 100644 index 0000000..279a716 --- /dev/null +++ b/PEAR/Command/Common.php @@ -0,0 +1,273 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Common.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Common extends PEAR +{ + /** + * 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' + ); + + /** + * PEAR_Command_Common constructor. + * + * @access public + */ + function PEAR_Command_Common(&$ui, &$config) + { + parent::PEAR(); + $this->config = &$config; + $this->ui = &$ui; + } + + /** + * 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; + } + + /** + * 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; + } + + 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']; + } + + return null; + } + + 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; + } + } + + /** + * 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)); + } + + /** + * 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; + } + + 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; + + //$command = $this->commands[$cmd]['function']; + break; + } + } + } else { + $func = $this->commands[$command]['function']; + } + + return $this->$func($command, $options, $params); + } +} \ No newline at end of file diff --git a/PEAR/Command/Config.php b/PEAR/Command/Config.php new file mode 100644 index 0000000..a761b27 --- /dev/null +++ b/PEAR/Command/Config.php @@ -0,0 +1,414 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Config.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Config extends PEAR_Command_Common +{ + 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). +', + ), + ); + + /** + * PEAR_Command_Config constructor. + * + * @access public + */ + function PEAR_Command_Config(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + function doConfigShow($command, $options, $params) + { + $layer = null; + if (is_array($params)) { + $layer = isset($params[0]) ? $params[0] : 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'); + } + + $channel = $reg->channelName($channel); + $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; + } + + function doConfigGet($command, $options, $params) + { + $args_cnt = is_array($params) ? count($params) : 0; + 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"); + } + + $reg = &$this->config->getRegistry(); + $channel = isset($options['channel']) ? $options['channel'] : $this->config->get('default_channel'); + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + + $channel = $reg->channelName($channel); + $this->ui->outputData($this->config->get($config_key, $layer, $channel), $command); + return true; + } + + function doConfigSet($command, $options, $params) + { + // $param[0] -> a parameter to set + // $param[1] -> the value for the parameter + // $param[2] -> the layer + $failmsg = ''; + if (count($params) < 2 || count($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'); + } + + $channel = $reg->channelName($channel); + if ($params[0] == 'default_channel' && !$reg->channelExists($params[1])) { + return $this->raiseError('Channel "' . $params[1] . '" does not exist'); + } + + if ($params[0] == 'preferred_mirror' + && ( + !$reg->mirrorExists($channel, $params[1]) && + (!$reg->channelExists($params[1]) || $channel != $params[1]) + ) + ) { + $msg = 'Channel Mirror "' . $params[1] . '" does not exist'; + $msg .= ' in your registry for channel "' . $channel . '".'; + $msg .= "\n" . 'Attempt to run "pear channel-update ' . $channel .'"'; + $msg .= ' if you believe this mirror should exist as you may'; + $msg .= ' have outdated channel information.'; + return $this->raiseError($msg); + } + + 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; + } + + 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); + } + + 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'])) { + return PEAR::raiseError('Root directory must be an absolute path beginning ' . + 'with "/", was: "' . $root . '"'); + } + + if (!preg_match('/^[A-Za-z]:/', $root)) { + return PEAR::raiseError('Root directory must be an absolute path beginning ' . + 'with "\\" or "C:\\", was: "' . $root . '"'); + } + } + + $windows = isset($options['windows']); + if ($windows) { + $root = str_replace('/', '\\', $root); + } + + if (!file_exists($params[1]) && !@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('www_dir', $windows ? "$root\\pear\\www" : "$root/pear/www"); + $config->set('cfg_dir', $windows ? "$root\\pear\\cfg" : "$root/pear/cfg"); + $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; + } + + /** + * 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; + } +} \ No newline at end of file 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..c035f6d --- /dev/null +++ b/PEAR/Command/Install.php @@ -0,0 +1,1268 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Install.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'upgrade packages from a specific channel', + 'arg' => 'CHAN', + ), + '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 [Deprecated in favor of calling upgrade with no parameters]', + 'function' => 'doUpgradeAll', + 'shortcut' => 'ua', + 'options' => array( + 'channel' => array( + 'shortopt' => 'c', + 'doc' => 'upgrade packages from a specific channel', + 'arg' => 'CHAN', + ), + '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' => ' +WARNING: This function is deprecated in favor of using the upgrade command with no params + +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; + } + $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)); + $fp = @fopen($phpini, 'wb'); + if (!$fp) { + return PEAR::raiseError('cannot open php.ini "' . $phpini . '" for writing'); + } + 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)) { + return PEAR::raiseError('php.ini "' . $filename . '" does not exist'); + } + + 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, + ); + } + + // {{{ doInstall() + + function doInstall($command, $options, $params) + { + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + + if (isset($options['installroot']) && isset($options['packagingroot'])) { + return $this->raiseError('ERROR: cannot use both --installroot and --packagingroot'); + } + + $reg = &$this->config->getRegistry(); + $channel = isset($options['channel']) ? $options['channel'] : $this->config->get('default_channel'); + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + + if (empty($this->installer)) { + $this->installer = &$this->getInstaller($this->ui); + } + + if ($command == 'upgrade' || $command == 'upgrade-all') { + // If people run the upgrade command but pass nothing, emulate a upgrade-all + if ($command == 'upgrade' && empty($params)) { + return $this->doUpgradeAll($command, $options, $params); + } + $options['upgrade'] = true; + } else { + $packages = $params; + } + + $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 = $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; + } + + $exists = $reg->packageExists($pf->getPackage(), $pf->getChannel()); + $pversion = $reg->packageInfo($pf->getPackage(), 'version', $pf->getChannel()); + $version_compare = version_compare($pf->getVersion(), $pversion, '<='); + if ($exists && $version_compare) { + 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, $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 { + $count = count($abstractpackages); + foreach ($abstractpackages as $i => $package) { + if (isset($package['group'])) { + // do not filter out install groups + continue; + } + + if ($instreg->packageExists($package['package'], $package['channel'])) { + if ($count > 1) { + if ($this->config->get('verbose')) { + $this->ui->outputData('Ignoring installed package ' . + $reg->parsedPackageNameToString($package, true)); + } + unset($abstractpackages[$i]); + } elseif ($count === 1) { + // Lets try to upgrade it since it's already installed + $options['upgrade'] = true; + } + } + } + } + $abstractpackages = + array_map(array($reg, 'parsedPackageNameToString'), $abstractpackages); + } elseif (count($abstractpackages)) { + $abstractpackages = + array_map(array($reg, 'parsedPackageNameToString'), $abstractpackages); + } + + $packages = array_merge($abstractpackages, $otherpackages); + if (!count($packages)) { + $c = ''; + if (isset($options['channel'])){ + $c .= ' in channel "' . $options['channel'] . '"'; + } + $this->ui->outputData('Nothing to ' . $command . $c); + return true; + } + + $this->downloader = &$this->getDownloader($this->ui, $options, $this->config); + $errors = $downloaded = $binaries = 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) { + if ($error !== null) { + $err['data'][] = array($error); + } + } + + if (!empty($err['data'])) { + $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; + } + + $binaries = $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)) { + return $this->raiseError("$command failed"); + } + + 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; + } + } + + if (count($binaries)) { + 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) { + $chan = $param->getChannel(); + $label = $reg->parsedPackageNameToString( + array( + 'channel' => $chan, + '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'; + } + } + } + + if (count($extrainfo)) { + foreach ($extrainfo as $info) { + $this->ui->outputData($info); + } + } + + return true; + } + + // }}} + // {{{ doUpgradeAll() + + function doUpgradeAll($command, $options, $params) + { + $reg = &$this->config->getRegistry(); + $upgrade = array(); + + if (isset($options['channel'])) { + $channels = array($options['channel']); + } else { + $channels = $reg->listChannels(); + } + + foreach ($channels as $channel) { + if ($channel == '__uri') { + continue; + } + + // parse name with channel + foreach ($reg->listPackages($channel) as $name) { + $upgrade[] = $reg->parsedPackageNameToString(array( + 'channel' => $channel, + 'package' => $name + )); + } + } + + $err = $this->doInstall($command, $options, $upgrade); + if (PEAR::isError($err)) { + $this->ui->outputData($err->getMessage(), $command); + } + } + + // }}} + // {{{ doUninstall() + + function doUninstall($command, $options, $params) + { + if (count($params) < 1) { + return $this->raiseError("Please supply the package(s) you want to uninstall"); + } + + 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); + } + } + + $reg = &$this->config->getRegistry(); + $newparams = array(); + $binaries = 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); + $binaries = array(); + 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; + } + } + if (count($binaries)) { + 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)) { + return $this->raiseError("uninstall failed: $pkg"); + } + $pkg = $reg->parsedPackageNameToString($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) + { + $opts = array( + 'force' => true, + 'nodeps' => true, + 'soft' => true, + 'downloadonly' => true + ); + $downloader = &$this->getDownloader($this->ui, $opts, $this->config); + $reg = &$this->config->getRegistry(); + if (count($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(); + $dir = $pwd . DIRECTORY_SEPARATOR . 'ext'; + $dest = is_dir($dir) ? $dir : $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; + } + if (!isset($result[0])) { + return $this->raiseError('unable to unpack ' . $params[0]); + } + $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)) { + return $this->raiseError('Could not retrieve package "' . $params[0] . '" from registry'); + } + + $package->setConfig($this->config); + $package->runPostinstallScripts(); + $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); + } + + $base2 = false; + $preferred_mirror = $this->config->get('preferred_mirror', null, $channel); + if ($chan->supportsREST($preferred_mirror) && + ( + //($base2 = $chan->getBaseURL('REST1.4', $preferred_mirror)) || + ($base = $chan->getBaseURL('REST1.0', $preferred_mirror)) + ) + ) { + $dorest = true; + } + + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + if (!isset($package['state'])) { + $state = $this->config->get('preferred_state', null, $channel); + } else { + $state = $package['state']; + } + + if ($dorest) { + if ($base2) { + $rest = &$this->config->getREST('1.4', array()); + $base = $base2; + } else { + $rest = &$this->config->getREST('1.0', array()); + } + + $installed = array_flip($reg->listPackages($channel)); + $latest = $rest->listLatestUpgrades($base, $state, $installed, $channel, $reg); + } + + 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 + $name_lower = strtolower($name); + if (isset($latestReleases[$channel][$name_lower])) { + // if not set, up to date + $inst_version = $reg->packageInfo($name, 'version', $channel); + $channel_version = $latestReleases[$channel][$name_lower]['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; + } +} \ No newline at end of file diff --git a/PEAR/Command/Install.xml b/PEAR/Command/Install.xml new file mode 100644 index 0000000..1b1e933 --- /dev/null +++ b/PEAR/Command/Install.xml @@ -0,0 +1,276 @@ + + + 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 + root directory used when installing files (ala PHP's INSTALL_ROOT), use packagingroot for RPM + DIR + + + P + root directory used when packaging files, like RPM packaging + DIR + + + + 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 + + + c + upgrade packages from a specific channel + CHAN + + + 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 + root directory used when installing files (ala PHP's INSTALL_ROOT) + DIR + + + + 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 [Deprecated in favor of calling upgrade with no parameters] + doUpgradeAll + ua + + + c + upgrade packages from a specific channel + CHAN + + + 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 + root directory used when installing files (ala PHP's INSTALL_ROOT), use packagingroot for RPM + DIR + + + + force install even if there were errors + + + + do not check for recommended dependency version + + + +WARNING: This function is deprecated in favor of using the upgrade command with no params + +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 + root directory used when installing files (ala PHP's INSTALL_ROOT) + DIR + + + + 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 + Optional destination directory for unpacking (defaults to current path or "ext" if exists) + DIR + + + 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..4d157c6 --- /dev/null +++ b/PEAR/Command/Mirror.php @@ -0,0 +1,139 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Mirror.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.2.0 + */ +class PEAR_Command_Mirror extends PEAR_Command_Common +{ + 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' + ), + ); + + /** + * 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; + } + + /** + * 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, $channel)); + } + + 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; + } +} \ No newline at end of file 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..ae6d52b --- /dev/null +++ b/PEAR/Command/Package.php @@ -0,0 +1,1124 @@ + + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Package.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ + +class PEAR_Command_Package extends PEAR_Command_Common +{ + 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. +', + ), + 'svntag' => array( + 'summary' => 'Set SVN Release Tag', + 'function' => 'doSvnTag', + 'shortcut' => 'sv', + 'options' => array( + 'quiet' => array( + 'shortopt' => 'q', + 'doc' => 'Be 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 SVN 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 svntag 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. + ', + ), + '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' => ' or or +List all dependencies the package has. +Can take a tgz / tar file, package.xml or a package name of an installed package.' + ), + '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; + + /** + * PEAR_Command_Package constructor. + * + * @access public + */ + function PEAR_Command_Package(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + 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 && count($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) + { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + $a = &new PEAR_PackageFile($config, $debug); + $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] : 'package.xml'; + $pkg2 = isset($params[1]) ? $params[1] : null; + if (!$pkg2 && !isset($params[0]) && 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; + } + + function doPackageValidate($command, $options, $params) + { + $this->output = ''; + if (count($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 = $warn = array(); + if ($errors = $info->getValidationWarnings()) { + foreach ($errors 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; + } + + function doSvnTag($command, $options, $params) + { + $this->output = ''; + $_cmd = $command; + if (count($params) < 1) { + $help = $this->getHelp($command); + return $this->raiseError("$command: missing parameter: $help[0]"); + } + + $packageFile = realpath($params[0]); + $dir = dirname($packageFile); + $dir = substr($dir, strrpos($dir, DIRECTORY_SEPARATOR) + 1); + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($packageFile, 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('SVN tag failed'); + } + + $version = $info->getVersion(); + $package = $info->getName(); + $svntag = "$package-$version"; + + if (isset($options['delete'])) { + return $this->_svnRemoveTag($version, $package, $svntag, $packageFile, $options); + } + + $path = $this->_svnFindPath($packageFile); + + // Check if there are any modified files + $fp = popen('svn st --xml ' . dirname($packageFile), "r"); + $out = ''; + while ($line = fgets($fp, 1024)) { + $out .= rtrim($line)."\n"; + } + pclose($fp); + + if (!isset($options['quiet']) && strpos($out, 'item="modified"')) { + $params = array(array( + 'name' => 'modified', + 'type' => 'yesno', + 'default' => 'no', + 'prompt' => 'You have files in your SVN checkout (' . $path['from'] . ') that have been modified but not commited, do you still want to tag ' . $version . '?', + )); + $answers = $this->ui->confirmDialog($params); + + if (!in_array($answers['modified'], array('y', 'yes', 'on', '1'))) { + return true; + } + } + + if (isset($options['slide'])) { + $this->_svnRemoveTag($version, $package, $svntag, $packageFile, $options); + } + + // Check if tag already exists + $releaseTag = $path['local']['base'] . 'tags' . DIRECTORY_SEPARATOR . $svntag; + $existsCommand = 'svn ls ' . $path['base'] . 'tags/'; + + $fp = popen($existsCommand, "r"); + $out = ''; + while ($line = fgets($fp, 1024)) { + $out .= rtrim($line)."\n"; + } + pclose($fp); + + if (in_array($svntag . DIRECTORY_SEPARATOR, explode("\n", $out))) { + $this->ui->outputData($this->output, $command); + return $this->raiseError('SVN tag ' . $svntag . ' for ' . $package . ' already exists.'); + } elseif (file_exists($path['local']['base'] . 'tags') === false) { + return $this->raiseError('Can not locate the tags directory at ' . $path['local']['base'] . 'tags'); + } elseif (is_writeable($path['local']['base'] . 'tags') === false) { + return $this->raiseError('Can not write to the tag directory at ' . $path['local']['base'] . 'tags'); + } else { + $makeCommand = 'svn mkdir ' . $releaseTag; + $this->output .= "+ $makeCommand\n"; + if (empty($options['dry-run'])) { + // We need to create the tag dir. + $fp = popen($makeCommand, "r"); + $out = ''; + while ($line = fgets($fp, 1024)) { + $out .= rtrim($line)."\n"; + } + pclose($fp); + $this->output .= "$out\n"; + } + } + + $command = 'svn'; + if (isset($options['quiet'])) { + $command .= ' -q'; + } + + $command .= ' copy --parents '; + + $dir = dirname($packageFile); + $dir = substr($dir, strrpos($dir, DIRECTORY_SEPARATOR) + 1); + $files = array_keys($info->getFilelist()); + if (!in_array(basename($packageFile), $files)) { + $files[] = basename($packageFile); + } + + array_shift($params); + if (count($params)) { + // add in additional files to be tagged (package files and such) + $files = array_merge($files, $params); + } + + $commands = array(); + foreach ($files as $file) { + if (!file_exists($file)) { + $file = $dir . DIRECTORY_SEPARATOR . $file; + } + $commands[] = $command . ' ' . escapeshellarg($file) . ' ' . + escapeshellarg($releaseTag . DIRECTORY_SEPARATOR . $file); + } + + $this->output .= implode("\n", $commands) . "\n"; + if (empty($options['dry-run'])) { + foreach ($commands as $command) { + $fp = popen($command, "r"); + while ($line = fgets($fp, 1024)) { + $this->output .= rtrim($line)."\n"; + } + pclose($fp); + } + } + + $command = 'svn ci -m "Tagging the ' . $version . ' release" ' . $releaseTag . "\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; + } + + function _svnFindPath($file) + { + $xml = ''; + $command = "svn info --xml $file"; + $fp = popen($command, "r"); + while ($line = fgets($fp, 1024)) { + $xml .= rtrim($line)."\n"; + } + pclose($fp); + $url_tag = strpos($xml, ''); + $url = substr($xml, $url_tag + 5, strpos($xml, '', $url_tag + 5) - ($url_tag + 5)); + + $path = array(); + $path['from'] = substr($url, 0, strrpos($url, '/')); + $path['base'] = substr($path['from'], 0, strrpos($path['from'], '/') + 1); + + // Figure out the local paths - see http://pear.php.net/bugs/17463 + $pos = strpos($file, DIRECTORY_SEPARATOR . 'trunk' . DIRECTORY_SEPARATOR); + if ($pos === false) { + $pos = strpos($file, DIRECTORY_SEPARATOR . 'branches' . DIRECTORY_SEPARATOR); + } + $path['local']['base'] = substr($file, 0, $pos + 1); + + return $path; + } + + function _svnRemoveTag($version, $package, $tag, $packageFile, $options) + { + $command = 'svn'; + + if (isset($options['quiet'])) { + $command .= ' -q'; + } + + $command .= ' remove'; + $command .= ' -m "Removing tag for the ' . $version . ' release."'; + + $path = $this->_svnFindPath($packageFile); + $command .= ' ' . $path['base'] . 'tags/' . $tag; + + + 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, $command); + return true; + } + + function doCvsTag($command, $options, $params) + { + $this->output = ''; + $_cmd = $command; + if (count($params) < 1) { + $help = $this->getHelp($command); + return $this->raiseError("$command: missing parameter: $help[0]"); + } + + $packageFile = realpath($params[0]); + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($packageFile, 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); + } + + $dir = dirname($packageFile); + $dir = substr($dir, strrpos($dir, '/') + 1); + foreach ($files as $file) { + if (!file_exists($file)) { + $file = $dir . DIRECTORY_SEPARATOR . $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; + } + + function doCvsDiff($command, $options, $params) + { + $this->output = ''; + if (sizeof($params) < 1) { + $help = $this->getHelp($command); + return $this->raiseError("$command: missing parameter: $help[0]"); + } + + $file = realpath($params[0]); + $obj = &$this->getPackageFile($this->config, $this->_debug); + $info = $obj->fromAnyFile($file, 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; + } + + function doPackageDependencies($command, $options, $params) + { + // $params[0] -> the PEAR package to list its information + if (count($params) !== 1) { + return $this->raiseError("bad parameter(s), try \"help $command\""); + } + + $obj = &$this->getPackageFile($this->config, $this->_debug); + if (is_file($params[0]) || strpos($params[0], '.xml') > 0) { + $info = $obj->fromAnyFile($params[0], PEAR_VALIDATE_NORMAL); + } else { + $reg = $this->config->getRegistry(); + $info = $obj->fromArray($reg->packageInfo($params[0])); + } + + 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); + } + + function doSign($command, $options, $params) + { + // should move most of this code into PEAR_Packager + // so it'll be easy to implement "pear package --sign" + if (count($params) !== 1) { + return $this->raiseError("bad parameter(s), try \"help $command\""); + } + + require_once 'System.php'; + require_once 'Archive/Tar.php'; + + 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 = $this->config->get('temp_dir'); + $tmpdir = System::mktemp(' -t "' . $tmpdir . '" -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; + } + + 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); + } + + $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_array($pf->getUserInfo())) { + foreach ($pf->getUserInfo() as $warning) { + $this->ui->outputData($warning['message']); + } + } + return $this->raiseError($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; + } +} \ No newline at end of file diff --git a/PEAR/Command/Package.xml b/PEAR/Command/Package.xml new file mode 100644 index 0000000..d1aff9d --- /dev/null +++ b/PEAR/Command/Package.xml @@ -0,0 +1,237 @@ + + + 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 SVN Release Tag + doSvnTag + sv + + + q + Be quiet + + + F + Move (slide) tag if it exists + + + d + Remove tag + + + n + Don't do anything, just pretend + + + <package.xml> [files...] + Sets a SVN 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 svntag 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. + + + + 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> [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. + + + + Show package dependencies + doPackageDependencies + pd + + <package-file> or <package.xml> or <install-package-name> +List all dependencies the package has. +Can take a tgz / tar file, package.xml or a package name of an installed package. + + + Sign a package distribution file + doSign + si + + + v + Display GnuPG output + + + <package-file> +Signs a package distribution (.tar or .tgz) file with GnuPG. + + + Builds an RPM spec file from a PEAR package + doMakeRPM + rpm + + + t + Use FILE as RPM spec file template + FILE + + + p + Use FORMAT as format string for RPM package name, %s is replaced +by the PEAR package name, defaults to "PEAR::%s". + FORMAT + + + <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. + + + \ No newline at end of file diff --git a/PEAR/Command/Pickle.php b/PEAR/Command/Pickle.php new file mode 100644 index 0000000..87aa25e --- /dev/null +++ b/PEAR/Command/Pickle.php @@ -0,0 +1,421 @@ + + * @copyright 2005-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Pickle.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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) + { + if (!class_exists('PEAR_Common')) { + require_once 'PEAR/Common.php'; + } + + if (!class_exists('PEAR_PackageFile')) { + require_once 'PEAR/PackageFile.php'; + } + + $a = &new PEAR_PackageFile($config, $debug); + $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' + && $dep['channel'] != 'doc.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' + && $dep['channel'] != 'doc.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) { + $default = isset($option['default']) ? $option['default'] : false; + $pf->addConfigureOption($option['name'], $option['prompt'], $default); + } + } + + 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('.'); + } +} \ No newline at end of file diff --git a/PEAR/Command/Pickle.xml b/PEAR/Command/Pickle.xml new file mode 100644 index 0000000..721ecea --- /dev/null +++ b/PEAR/Command/Pickle.xml @@ -0,0 +1,36 @@ + + + Build PECL Package + doPackage + pi + + + Z + Do not gzip the package file + + + n + Print the name of the packaged file. + + + [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. + + + \ No newline at end of file diff --git a/PEAR/Command/Registry.php b/PEAR/Command/Registry.php new file mode 100644 index 0000000..4304db5 --- /dev/null +++ b/PEAR/Command/Registry.php @@ -0,0 +1,1145 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Registry.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Registry extends PEAR_Command_Common +{ + 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.' + ) + ); + + /** + * PEAR_Command_Registry constructor. + * + * @access public + */ + function PEAR_Command_Registry(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + 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'])) { + return $this->raiseError('Channel "' . $options['channel'] .'" does not exist'); + } + + $channel = $reg->channelName($options['channel']); + } 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'); + } + + if (count($installed) && !isset($data['data'])) { + $data['data'] = array(); + } + + 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' => array(array('(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) { + $p = isset($package['package']) ? $package['package'] : $package['name']; + $pobj = $reg->getPackage($p, $channel); + $data['data'][] = array($pobj->getPackage(), $pobj->getVersion(), + $pobj->getState() ? $pobj->getState() : null); + } + + // Adds a blank line after each section + $data['data'][] = array(); + + if (count($packages) === 0) { + $data = array( + 'caption' => 'Installed packages, channel ' . $channel . ':', + 'border' => true, + 'data' => array(array('(no packages installed)'), array()), + '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; + } + + 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 (count($params) === 1) { + if (!$reg->packageExists($package, $channel)) { + exit(1); + } + // "pear shell-test Foo 1.0" + } elseif (count($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 (count($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); + } + } + + 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 (is_file($params[0]) && !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); + $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') { + return $this->_doInfo2($command, $options, $params, $obj, false); + } + + $info = $obj->toArray(); + } 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"; + } + + $active = $lead['active'] == 'no' ? ', inactive' : ''; + $info['Maintainers'] .= $lead['name'] . ' <'; + $info['Maintainers'] .= $lead['email'] . "> ($role$active)"; + } + } + + $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'); + } +} \ No newline at end of file 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..74478d8 --- /dev/null +++ b/PEAR/Command/Remote.php @@ -0,0 +1,810 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Remote.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Command_Remote extends PEAR_Command_Common +{ + 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 REST cache. See also the cache_ttl configuration +parameter. +', + ), + ); + + /** + * 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 "' . PEAR_RUNTYPE . ' channel-update ' . $channel . + '" to update'); + } + } + + 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; + } + + $mirror = $this->config->get('preferred_mirror'); + if ($chan->supportsREST($mirror) && $base = $chan->getBaseURL('REST1.0', $mirror)) { + $rest = &$this->config->getREST('1.0', array()); + $info = $rest->packageInfo($base, $parsed['package'], $channel); + } + + if (!isset($info)) { + return $this->raiseError('No supported protocol was found'); + } + + 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; + } + + 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)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + + $this->config->set('default_channel', $channel); + } + + $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; + } + + $available = array(); + 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, true, false, false, $chan->getName()); + } 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, true, false, false, $chan->getName()); + } + + 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) { + $version = (isset($info['stable']) && $info['stable']) ? $info['stable'] : '-n/a-'; + $data['data'][] = array($name, $version); + } + } + $this->ui->outputData($data, $command); + $this->config->set('default_channel', $savechannel); + return true; + } + + 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)) { + return $this->raiseError("Channel \"$channel\" does not exist"); + } + + $this->config->set('default_channel', $channel); + } + + $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, false, false, $chan->getName()); + } 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, false, false, $chan->getName()); + } + + 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; + } + + 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 = strtolower($params[0]); + $summary = isset($params[1]) ? $params[1] : false; + if (isset($options['channel'])) { + $reg = &$this->config->getRegistry(); + $channel = $options['channel']; + if (!$reg->channelExists($channel)) { + return $this->raiseError('Channel "' . $channel . '" does not exist'); + } + + $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()); + $available = $rest->listAll($base, false, false, $package, $summary, $chan->getName()); + } + + 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; + } + + 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) { + if ($error !== null) { + $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; + } + } + + 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); + $state = empty($params[0]) ? $this->config->get('preferred_state') : $params[0]; + + $caption = $channel . ' Available Upgrades'; + $chan = $reg->getChannel($channel); + if (PEAR::isError($e = $this->_checkChannelForStatus($channel, $chan))) { + return $e; + } + + $latest = array(); + $base2 = false; + $preferred_mirror = $this->config->get('preferred_mirror'); + if ($chan->supportsREST($preferred_mirror) && + ( + //($base2 = $chan->getBaseURL('REST1.4', $preferred_mirror)) || + ($base = $chan->getBaseURL('REST1.0', $preferred_mirror)) + ) + + ) { + if ($base2) { + $rest = &$this->config->getREST('1.4', array()); + $base = $base2; + } else { + $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(); + } + + 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; + } + + 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('/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; + } +} \ No newline at end of file diff --git a/PEAR/Command/Remote.xml b/PEAR/Command/Remote.xml new file mode 100644 index 0000000..b4f6100 --- /dev/null +++ b/PEAR/Command/Remote.xml @@ -0,0 +1,109 @@ + + + Information About Remote Packages + doRemoteInfo + ri + + <package> +Get details on a package from the server. + + + List Available Upgrades + doListUpgrades + lu + + + i + output fully channel-aware data, even on failure + + + [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 + + + +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..a757d9e --- /dev/null +++ b/PEAR/Command/Test.php @@ -0,0 +1,337 @@ + + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Test.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ + +class PEAR_Command_Test extends PEAR_Command_Common +{ + 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; + + /** + * PEAR_Command_Test constructor. + * + * @access public + */ + function PEAR_Command_Test(&$ui, &$config) + { + parent::PEAR_Command_Common($ui, $config); + } + + 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']) ? 14 : 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[] = $atts['installed_as']; + continue; + } 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)) { + foreach ($dir as $p) { + $p = realpath($p); + if (!count($tests) || + (count($tests) && strlen($p) < strlen($tests[0]))) { + // this is in a higher-level directory, use this one instead. + $tests = array($p); + } + } + } + continue; + } + + $args = array($p, '-type', 'f', '-name', '*.phpt'); + } else { + if (isset($options['phpunit'])) { + if (preg_match('/AllTests\.php\\z/i', $p)) { + $p = realpath($p); + if (!count($tests) || + (count($tests) && strlen($p) < strlen($tests[0]))) { + // this is in a higher-level directory, use this one instead. + $tests = array($p); + } + } + continue; + } + + if (file_exists($p) && preg_match('/\.phpt$/', $p)) { + $tests[] = $p; + continue; + } + + if (!preg_match('/\.phpt\\z/', $p)) { + $p .= '.phpt'; + } + + $args = array(dirname($p), '-type', 'f', '-name', $p); + } + + if (!isset($options['recur'])) { + $args[] = '-maxdepth'; + $args[] = 1; + } + + $dir = System::find($args); + $tests = array_merge($tests, $dir); + } + + $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..bbe9fcc --- /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..3a8c7e8 --- /dev/null +++ b/PEAR/Common.php @@ -0,0 +1,837 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Common.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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'; + +/** + * 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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 +{ + /** + * 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; + + /** 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(); + + var $current_path = null; + + /** + * Flag variable used to mark a valid package file + * @var boolean + * @access private + */ + var $_validPackageFile; + + /** + * PEAR_Common constructor + * + * @access public + */ + function PEAR_Common() + { + parent::PEAR(); + $this->config = &PEAR_Config::singleton(); + $this->debug = $this->config->get('verbose'); + } + + /** + * 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); + } + } + } + + /** + * 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); + } + + /** + * 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) + { + // Only used in Installer - move it there ? + $this->log(2, "+ create dir $dir"); + if (!class_exists('System')) { + require_once 'System.php'; + } + return System::mkDir(array('-p', $dir)); + } + + /** + * 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"; + } + } + } + + /** + * 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 = '') + { + $topt = $tmpdir ? array('-t', $tmpdir) : 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; + } + + /** + * 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; + } + + /** + * 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); + } + + /** + * Get the valid roles for a PEAR package maintainer + * + * @return array + * @static + */ + function getUserRoles() + { + return $GLOBALS['_PEAR_Common_maintainer_roles']; + } + + /** + * Get the valid package release states of packages + * + * @return array + * @static + */ + function getReleaseStates() + { + return $GLOBALS['_PEAR_Common_release_states']; + } + + /** + * Get the implemented dependency types (php, ext, pkg etc.) + * + * @return array + * @static + */ + function getDependencyTypes() + { + return $GLOBALS['_PEAR_Common_dependency_types']; + } + + /** + * Get the implemented dependency relations (has, lt, ge etc.) + * + * @return array + * @static + */ + function getDependencyRelations() + { + return $GLOBALS['_PEAR_Common_dependency_relations']; + } + + /** + * Get the implemented file roles + * + * @return array + * @static + */ + function getFileRoles() + { + return $GLOBALS['_PEAR_Common_file_roles']; + } + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getReplacementTypes() + { + return $GLOBALS['_PEAR_Common_replacement_types']; + } + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getProvideTypes() + { + return $GLOBALS['_PEAR_Common_provide_types']; + } + + /** + * Get the implemented file replacement types in + * + * @return array + * @static + */ + function getScriptPhases() + { + return $GLOBALS['_PEAR_Common_script_phases']; + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * @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; + } + + function _postProcessChecks($pf) + { + if (!PEAR::isError($pf)) { + return $this->_postProcessValidPackagexml($pf); + } + + $errs = $pf->getUserinfo(); + if (is_array($errs)) { + foreach ($errs as $error) { + $e = $this->raiseError($error['message'], $error['code'], null, null, $error); + } + } + + return $pf; + } + + /** + * 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); + return $this->_postProcessChecks($pf); + } + + /** + * 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); + return $this->_postProcessChecks($pf); + } + + /** + * 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); + return $this->_postProcessChecks($pf); + } + + /** + * @param PEAR_PackageFile_v1|PEAR_PackageFile_v2 + * @return array + */ + function _postProcessValidPackagexml(&$pf) + { + if (!is_a($pf, 'PEAR_PackageFile_v2')) { + $this->pkginfo = $pf->toArray(); + return $this->pkginfo; + } + + // 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'], $arr['xsdversion'], $arr['contents'], $arr['compatible'], + $arr['channel'], $arr['uri'], $arr['dependencies'], $arr['phprelease'], + $arr['extsrcrelease'], $arr['zendextsrcrelease'], $arr['extbinrelease'], + $arr['zendextbinrelease'], $arr['bundle'], $arr['lead'], $arr['developer'], + $arr['helper'], $arr['contributor']); + $arr['filelist'] = $pf->getFilelist(); + $this->pkginfo = $arr; + return $arr; + } + + /** + * 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; + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * 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); + } + } + + /** + * Analyze the source code of the given PHP file + * + * @param string Filename of the PHP file + * @return mixed + * @access public + */ + function analyzeSourceCode($file) + { + if (!class_exists('PEAR_PackageFile_v2_Validator')) { + require_once 'PEAR/PackageFile/v2/Validator.php'; + } + + $a = new PEAR_PackageFile_v2_Validator; + return $a->analyzeSourceCode($file); + } + + 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, + ); + } + + /** + * 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); + } +} + +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..86a7db3 --- /dev/null +++ b/PEAR/Config.php @@ -0,0 +1,2097 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Config.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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'; + +/** + * 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')) { + define('PEAR_RUNTYPE', 'pear'); +} + +if (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 cfg_dir +if (getenv('PHP_PEAR_CFG_DIR')) { + define('PEAR_CONFIG_DEFAULT_CFG_DIR', getenv('PHP_PEAR_CFG_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_CFG_DIR', + $PEAR_INSTALL_DIR.DIRECTORY_SEPARATOR.'cfg'); +} + +// Default for www_dir +if (getenv('PHP_PEAR_WWW_DIR')) { + define('PEAR_CONFIG_DEFAULT_WWW_DIR', getenv('PHP_PEAR_WWW_DIR')); +} else { + define('PEAR_CONFIG_DEFAULT_WWW_DIR', + $PEAR_INSTALL_DIR.DIRECTORY_SEPARATOR.'www'); +} + +// 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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Config extends PEAR +{ + /** + * 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', 'cfg_dir', + 'test_dir', 'www_dir', 'php_bin', 'php_prefix', 'php_suffix', 'username', + 'password', 'verbose', 'preferred_state', 'umask', 'preferred_mirror', 'php_ini' + ); + + /** + * 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)', + ), + 'cfg_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_CFG_DIR, + 'doc' => 'directory where modifiable configuration files are installed', + 'prompt' => 'PEAR configuration file directory', + 'group' => 'File Locations (Advanced)', + ), + 'www_dir' => array( + 'type' => 'directory', + 'default' => PEAR_CONFIG_DEFAULT_WWW_DIR, + 'doc' => 'directory where www frontend files (html/js) are installed', + 'prompt' => 'PEAR www files 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 web service 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_DOWNLOAD_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_prefix' => array( + 'type' => 'string', + 'default' => '', + 'doc' => '--program-prefix for php_bin\'s ./configure, used for pecl installs', + 'prompt' => '--program-prefix passed to PHP\'s ./configure', + 'group' => 'File Locations (Advanced)', + ), + 'php_suffix' => array( + 'type' => 'string', + 'default' => '', + 'doc' => '--program-suffix for php_bin\'s ./configure, used for pecl installs', + 'prompt' => '--program-suffix passed to PHP\'s ./configure', + '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 + ); + + /** + * 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)) { + $system_file = PEAR_CONFIG_SYSCONFDIR . $sl; + if (OS_WINDOWS) { + $system_file .= 'pearsys.ini'; + } else { + $system_file .= '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, false); + $this->_regInitialized['default'] = false; + //$GLOBALS['_PEAR_Config_instance'] = &$this; + } + + /** + * Return the default locations of user and system configuration files + * @static + */ + function getDefaultConfigFiles() + { + $sl = DIRECTORY_SEPARATOR; + if (OS_WINDOWS) { + return array( + 'user' => PEAR_CONFIG_SYSCONFDIR . $sl . 'pear.ini', + 'system' => PEAR_CONFIG_SYSCONFDIR . $sl . 'pearsys.ini' + ); + } + + return array( + 'user' => getenv('HOME') . $sl . '.pearrc', + 'system' => PEAR_CONFIG_SYSCONFDIR . $sl . 'pear.conf' + ); + } + + /** + * 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']; + } + + /** + * 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; + } + + /** + * 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) { + return true; + } + + $this->_errorsFound++; + $this->lastError = $data; + + return $data; + } + + $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, false); + $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')) { + return PEAR::raiseError('PEAR_RemoteInstaller must be installed to use remote config'); + } + + $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)) { + return true; + } + + $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); + } while (false); // poor man's catch + unset($this->files['ftp']); + return PEAR::raiseError('no remote host specified'); + } + + /** + * 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); + } + + function deleteChannel($channel) + { + $ch = strtolower($channel); + foreach ($this->configuration as $layer => $data) { + if (isset($data['__channels']) && isset($data['__channels'][$ch])) { + unset($this->configuration[$layer]['__channels'][$ch]); + } + } + + $this->_channels = array_flip($this->_channels); + unset($this->_channels[$ch]); + $this->_channels = array_flip($this->_channels); + } + + /** + * 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) { + return true; + } + + $this->_errorsFound++; + $this->lastError = $data; + + return $data; + } + + $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, false); + $this->_regInitialized[$layer] = false; + } else { + unset($this->_registry[$layer]); + } + return true; + } + + /** + * @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); + } + + /** + * 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; + } + + /** + * 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; + } + + $data = array(); + } + // add parsing of newer formats here... + } else { + $err = $this->raiseError("$file: unknown version `$version'"); + return $err; + } + + return $data; + } + + /** + * 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 string Configuration class name, used for detecting duplicate calls + * @param array information on a role as parsed from its xml file + * @return true|PEAR_Error + * @access private + */ + function _addConfigVars($class, $vars) + { + static $called = array(); + if (isset($called[$class])) { + return; + } + + $called[$class] = 1; + 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'); + } elseif (!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)'); + } + + 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)) { + return $this->raiseError( + 'Unknown constant "' . $val . '" requested in ' . + 'default value for configuration variable "' . + $name . '"'); + } + + $real_default .= constant($val); + } 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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') { + return $ret; + } + + + 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 + } + + return $this->getDefaultChannel($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 (!isset($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; + } + + return false; + } + + 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, false); + } + } + + 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, false); + $this->_regInitialized[$layer] = false; + } else { + unset($this->_registry[$layer]); + return; + } + } + + $this->setChannels($this->_registry[$layer]->listChannels(), $merge); + $this->_regInitialized[$layer] = true; + $merge = true; + } + } + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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); + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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 the a config key from a specific config layer. + * + * @param string config key + * @param string (optional) config layer + * @param string (optional) channel (defaults to default channel) + * @return bool TRUE on success, FALSE on failure + * + * @access public + */ + function remove($key, $layer = 'user', $channel = null) + { + if ($channel === null) { + $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; + } + + /** + * 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; + } + + /** + * 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); + } + + /** + * 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 ''; + } + + /** + * 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; + } + + /** + * 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]); + } + + /** + * 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); + } + + function apiVersion() + { + return '1.1'; + } + + /** + * @return PEAR_Registry + */ + function &getRegistry($use = null) + { + $layer = $use === null ? 'user' : $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; + } + + // 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, false); + } + + return true; + } + + function noRegistry() + { + $this->_noRegistry = true; + } + + /** + * @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; + } + + $a = false; + return $a; + } + + 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, false); + $this->_regInitialized[$layer] = false; + } + } + } +} diff --git a/PEAR/Dependency2.php b/PEAR/Dependency2.php new file mode 100644 index 0000000..f3ddeb1 --- /dev/null +++ b/PEAR/Dependency2.php @@ -0,0 +1,1358 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Dependency2.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 ($dep['name'] == '*') { + return true; + } + + $not = isset($dep['conflicts']) ? true : false; + 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"); + } + + 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"); + } + + 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"); + } + + 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"); + } + + 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'); + } + + 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']); + } + + 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; + } + + $not = isset($dep['conflicts']) ? true : 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'] . '"'); + } + + return $this->warning('warning: %s Architecture dependency failed, does ' . + 'not match "' . $dep['pattern'] . '"'); + } + + return true; + } + + if ($not) { + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s Architecture dependency failed, required "' . + $dep['pattern'] . '"'); + } + + 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); + } + + 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); + } + + return $this->warning('warning: %s conflicts with PHP extension "' . + $dep['name'] . '"' . $extra); + } + + return true; + } + + 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); + } + + return $this->warning('warning: %s requires PHP extension "' . + $dep['name'] . '"' . $extra); + } + + 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); + } + + 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']) && !version_compare($version, $dep['min'], '>=')) { + $fail = true; + } + + if (isset($dep['max']) && !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); + } + + 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); + } + + 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); + } + + 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); + } + + 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; + } + + 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'); + } + + 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); + } + + 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); + } + + 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); + } + + 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.9.4'; + } + + 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); + } + + 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); + } + + 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); + } + + 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']) && !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'])) { + $rest = ''; + if ($version) { + $rest = ", $installed version is " . $version; + } + + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('%s conflicts with package "' . $depname . '"' . $extra . $rest); + } + + return $this->warning('warning: %s conflicts with package "' . $depname . '"' . $extra . $rest); + } + + return true; + } + + 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); + } + + return $this->warning('warning: %s requires package "' . $depname . '"' . $extra); + } + + 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); + } + + return $this->warning('warning: %s requires package "' . $depname . '"' . $extra); + } + + return $this->warning('%s can optionally use package "' . $depname . '"' . $extra); + } + + $fail = false; + if (isset($dep['min']) && version_compare($version, $dep['min'], '<')) { + $fail = true; + } + + if (isset($dep['max']) && 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); + } + + 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); + } + + 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); + } + + 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); + } + + 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; + } + + 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 && $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'); + } + + 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'); + } + + 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'); + } + + 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']) && !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) { + return $this->warning('"' . $depname . '" can be optionally used by ' . + 'installed package %s' . $extra); + } + + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError('"' . $depname . '" is required by ' . + 'installed package %s' . $extra); + } + + return $this->warning('warning: "' . $depname . '" is required by ' . + 'installed package %s' . $extra); + } + + $fail = false; + if (isset($dep['min']) && version_compare($version, $dep['min'], '>=')) { + $fail = true; + } + + if (isset($dep['max']) && version_compare($version, $dep['max'], '<=')) { + $fail = true; + } + + // we re-use this variable, preserve the original value + $saverequired = $required; + if (!$required) { + return $this->warning($depname . $extra . ' can be optionally used by installed package' . + ' "%s"'); + } + + if (!isset($this->_options['nodeps']) && !isset($this->_options['force'])) { + return $this->raiseError($depname . $extra . ' is required by installed package' . + ' "%s"'); + } + + return $this->raiseError('warning: ' . $depname . $extra . + ' is required by installed package "%s"'); + } + + /** + * 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']])) { + return array(false, false); + } + + $type = $types[$dep['type']]; + + $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.4.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..948f0c9 --- /dev/null +++ b/PEAR/DependencyDB.php @@ -0,0 +1,769 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: DependencyDB.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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) + { + $phpdir = $config->get('php_dir', null, 'pear.php.net'); + if (!isset($GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir])) { + $a = new PEAR_DependencyDB; + $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir] = &$a; + $a->setConfig($config, $depdb); + $e = $a->assertDepsDB(); + if (PEAR::isError($e)) { + return $e; + } + } + + return $GLOBALS['_PEAR_DEPENDENCYDB_INSTANCE'][$phpdir]; + } + + /** + * 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; + } + + 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(); + return; + } + + $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 ( + isset($dep['dep'], $dep['dep']['channel'], $dep['dep']['name']) && + strtolower($dep['dep']['channel']) == $channel && + strtolower($dep['dep']['name']) == $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']) == $depchannel && + strtolower($info['dep']['name']) == $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; + $depchannel = isset($dep['dep']['uri']) ? '__uri' : strtolower($dep['dep']['channel']); + $depname = strtolower($dep['dep']['name']); + if (isset($data['packages'][$depchannel][$depname])) { + foreach ($data['packages'][$depchannel][$depname] as $i => $info) { + if ($info['channel'] == $channel && $info['package'] == $package) { + $found = true; + break; + } + } + } + + if ($found) { + unset($data['packages'][$depchannel][$depname][$i]); + if (!count($data['packages'][$depchannel][$depname])) { + unset($data['packages'][$depchannel][$depname]); + if (!count($data['packages'][$depchannel])) { + unset($data['packages'][$depchannel]); + } + } else { + $data['packages'][$depchannel][$depname] = + array_values($data['packages'][$depchannel][$depname]); + } + } + } + + 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(); + if (PEAR::isError($packages)) { + return $packages; + } + + foreach ($packages as $channel => $ps) { + foreach ($ps as $package) { + $package = $this->_registry->getPackage($package, $channel); + if (PEAR::isError($package)) { + return $package; + } + $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 (stristr(php_uname(), 'Windows 9')) { + return true; + } + + 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(); + } + + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + + if (!isset($data['dependencies'][$channel])) { + $data['dependencies'][$channel] = array(); + } + + $data['dependencies'][$channel][$package] = 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'][$channel][$package] == array()) { + unset($data['dependencies'][$channel][$package]); + if (!count($data['dependencies'][$channel])) { + unset($data['dependencies'][$channel]); + } + } + } + + /** + * @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 + ); + + $dep = array_map('strtolower', $dep); + $depchannel = isset($dep['channel']) ? $dep['channel'] : '__uri'; + if (!isset($data['dependencies'])) { + $data['dependencies'] = array(); + } + + $channel = strtolower($pkg->getChannel()); + $package = strtolower($pkg->getPackage()); + + if (!isset($data['dependencies'][$channel])) { + $data['dependencies'][$channel] = array(); + } + + if (!isset($data['dependencies'][$channel][$package])) { + $data['dependencies'][$channel][$package] = array(); + } + + $data['dependencies'][$channel][$package][] = $info; + if (isset($data['packages'][$depchannel][$dep['name']])) { + $found = false; + foreach ($data['packages'][$depchannel][$dep['name']] as $i => $p) { + if ($p['channel'] == $channel && $p['package'] == $package) { + $found = true; + break; + } + } + } else { + if (!isset($data['packages'])) { + $data['packages'] = array(); + } + + if (!isset($data['packages'][$depchannel])) { + $data['packages'][$depchannel] = array(); + } + + if (!isset($data['packages'][$depchannel][$dep['name']])) { + $data['packages'][$depchannel][$dep['name']] = array(); + } + + $found = false; + } + + if (!$found) { + $data['packages'][$depchannel][$dep['name']][] = array( + 'channel' => $channel, + 'package' => $package + ); + } + } +} \ No newline at end of file diff --git a/PEAR/Downloader.php b/PEAR/Downloader.php new file mode 100644 index 0000000..730df0b --- /dev/null +++ b/PEAR/Downloader.php @@ -0,0 +1,1766 @@ + + * @author Stig Bakken + * @author Tomas V. V. Cox + * @author Martin Jansen + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Downloader.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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; + + /** + * 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; + + /** + * @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(); + + 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'; + } + + $tmpdir = $this->config->get('temp_dir'); + $tmp = System::mktemp('-d -t "' . $tmpdir . '"'); + $a = $this->downloadHttp('http://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false); + PEAR::popErrorHandling(); + if (PEAR::isError($a)) { + // Attempt to fallback to https automatically. + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $this->log(1, 'Attempting fallback to https instead of http on channel "' . $channel . '"...'); + $a = $this->downloadHttp('https://' . $channel . '/channel.xml', $this->ui, $tmp, $callback, false); + PEAR::popErrorHandling(); + if (PEAR::isError($a)) { + return false; + } + } + + list($a, $lastmodified) = $a; + if (!class_exists('PEAR_ChannelFile')) { + 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')) { + 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(); + } + + $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']) && $err->getMessage() !== '') { + $this->log(0, $err->getMessage()); + } + + $params[$i] = false; + if (is_object($param)) { + $param = $param->getChannel() . '/' . $param->getPackage(); + } + + if (!isset($this->_options['soft'])) { + $this->log(2, 'Package "' . $param . '" is not valid'); + } + + // Message logged above in a specific verbose mode, passing null to not show up on CLI + $this->pushError(null, 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()); + $url = 'http://' . $mirror . '/channel.xml'; + $a = $this->downloadHttp($url, $this->ui, $dir, null, $curchannel->lastModified()); + + PEAR::staticPopErrorHandling(); + if (PEAR::isError($a) || !$a) { + // Attempt fallback to https automatically + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $a = $this->downloadHttp('https://' . $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 "' . PEAR_RUNTYPE . ' 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) + { + if (isset($this->_options['downloadonly'])) { + return; + } + + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $redo = true; + $reset = $hasfailed = $failed = 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); + $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); + $send = $param->getPackageFile(); + if ($send === null) { + $send = $param->getDownloadURL(); + } + + $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']) && is_array($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']) && is_array($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) || (is_dir($downloaddir) && !is_writable($downloaddir))) { + if (is_dir($downloaddir) && !is_writable($downloaddir)) { + $this->log(0, 'WARNING: configuration download directory "' . $downloaddir . + '" is not writeable. Change download_dir config variable to ' . + 'a writeable dir to avoid this warning'); + } + + 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; + } + + 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; + } + } + + function setOptions($options) + { + $this->_options = $options; + } + + function getOptions() + { + return $this->_options; + } + + + /** + * @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') && $this->discover($parr['channel'])) { + break; + } + + $this->configSet('default_channel', $curchannel); + return PEAR::raiseError('Unknown remote channel: ' . $parr['channel']); + } while (false); + } + + $chan = &$this->_registry->getChannel($parr['channel']); + if (PEAR::isError($chan)) { + return $chan; + } + + PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN); + $version = $this->_registry->packageInfo($parr['package'], 'version', $parr['channel']); + $stability = $this->_registry->packageInfo($parr['package'], 'stability', $parr['channel']); + // package is installed - use the installed release stability level + if (!isset($parr['state']) && $stability !== null) { + $state = $stability['release']; + } + PEAR::staticPopErrorHandling(); + $base2 = false; + + $preferred_mirror = $this->config->get('preferred_mirror'); + if (!$chan->supportsREST($preferred_mirror) || + ( + !($base2 = $chan->getBaseURL('REST1.3', $preferred_mirror)) + && + !($base = $chan->getBaseURL('REST1.0', $preferred_mirror)) + ) + ) { + return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.'); + } + + if ($base2) { + $rest = &$this->config->getREST('1.3', $this->_options); + $base = $base2; + } else { + $rest = &$this->config->getREST('1.0', $this->_options); + } + + $downloadVersion = false; + if (!isset($parr['version']) && !isset($parr['state']) && $version + && !PEAR::isError($version) + && !isset($this->_options['downloadonly']) + ) { + $downloadVersion = $version; + } + + $url = $rest->getDownloadURL($base, $parr, $state, $downloadVersion, $chan->getName()); + 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'); + } + + if (!isset($this->_options['force']) && + !isset($this->_options['downloadonly']) && + $version && + !PEAR::isError($version) && + !isset($parr['group']) + ) { + if (version_compare($version, $url['version'], '=')) { + return PEAR::raiseError($this->_registry->parsedPackageNameToString( + $parr, true) . ' is already installed and is the same as the ' . + 'released version ' . $url['version'], -976); + } + + if (version_compare($version, $url['version'], '>')) { + return PEAR::raiseError($this->_registry->parsedPackageNameToString( + $parr, true) . ' is already installed and is newer than detected ' . + 'released 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) && isset($url['url'])) { + $url['url'] .= $ext; + } + + return $url; + } + + /** + * @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')) + && + ( + ($base2 = $chan->getBaseURL('REST1.3', $this->config->get('preferred_mirror'))) + || + ($base = $chan->getBaseURL('REST1.0', $this->config->get('preferred_mirror'))) + ) + ) { + if ($base2) { + $base = $base2; + $rest = &$this->config->getREST('1.3', $this->_options); + } else { + $rest = &$this->config->getREST('1.0', $this->_options); + } + + $url = $rest->getDepDownloadURL($base, $xsdversion, $dep, $parr, + $state, $version, $chan->getName()); + 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) && isset($url['url'])) { + $url['url'] .= $ext; + } + + return $url; + } + + return $this->raiseError($parr['channel'] . ' is using a unsupported protocol - This should never happen.'); + } + + /** + * @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; + } + + /** + * 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; + } + + 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); + } + + 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 + * @param integer + */ + function pushError($errmsg, $code = -1) + { + array_push($this->_errorStack, array($errmsg, $code)); + } + + function getErrorMsgs() + { + $msgs = array(); + $errs = $this->_errorStack; + foreach ($errs as $err) { + $msgs[] = $err[0]; + } + $this->_errorStack = array(); + return $msgs; + } + + /** + * for BC + * + * @deprecated + */ + 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, $count = count($installOrder); $i < $count; $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 + * @param false|string $channel Channel to use for retrieving authentication + * @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, $channel = false) + { + static $redirect = 0; + // always 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 . '"'); + } + + $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)) { + $port = (isset($info['scheme']) && $info['scheme'] == 'https') ? 443 : 80; + } + + $scheme = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http'; + + 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"; + $request .= "Host: $host\r\n"; + } else { + $request = "GET $url HTTP/1.0\r\n"; + $request .= "Host: $host\r\n"; + } + } else { + $network_host = $host; + if (isset($info['scheme']) && $info['scheme'] == 'https') { + $network_host = 'ssl://' . $host; + } + + $fp = @fsockopen($network_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\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.9.4/PHP/" . PHP_VERSION . "\r\n"; + + if (isset($this)) { // only pass in authentication for non-static calls + $username = $config->get('username', null, $channel); + $password = $config->get('password', null, $channel); + 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 $scheme://$host:$port$path not valid (received: $line)"); + } + } + } + + if ($reply != 200) { + if (!isset($headers['location'])) { + return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirected but no location)"); + } + + if ($wasredirect > 4) { + return PEAR::raiseError("File $scheme://$host:$port$path not valid (redirection looped more than 5 times)"); + } + + $redirect = $wasredirect + 1; + return $this->downloadHttp($headers['location'], + $ui, $save_dir, $callback, $lastmodified, $accept); + } + + 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 (is_link($dest_file)) { + return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $dest_file . ' as it is symlinked to ' . readlink($dest_file) . ' - Possible symlink attack'); + } + + 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"); + } + + $length = isset($headers['content-length']) ? $headers['content-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; + } +} \ No newline at end of file diff --git a/PEAR/Downloader/Package.php b/PEAR/Downloader/Package.php new file mode 100644 index 0000000..987c965 --- /dev/null +++ b/PEAR/Downloader/Package.php @@ -0,0 +1,1988 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Package.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 + * @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) { + return true; + } + + $options = $this->_downloader->getOptions(); + if (isset($options['offline'])) { + if (PEAR::isError($origErr) && !isset($options['soft'])) { + foreach ($origErr->getUserInfo() as $userInfo) { + if (isset($userInfo['message'])) { + $this->_downloader->log(0, $userInfo['message']); + } + } + + $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) && !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); + } + + if (!isset($options['soft'])) { + $this->_downloader->log(2, "Cannot initialize '$param', invalid or missing package file"); + } + + // Passing no message back - already logged above + return PEAR::raiseError(); + } + + if (PEAR::isError($err) && !isset($options['soft'])) { + $this->_downloader->log(0, $err->getMessage()); + } + + if (is_array($param)) { + $param = $this->_registry->parsedPackageNameToString($param, true); + } + + if (!isset($options['soft'])) { + $this->_downloader->log(2, "Cannot initialize '$param', invalid or missing package file"); + } + + // Passing no message back - already logged above + return PEAR::raiseError(); + } + } + + 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['channel'] == 'pecl.php.net' && $newinfo['channel'] == 'pear.php.net') { + $info['channel'] = 'pear.php.net'; + if ($info == $newinfo) { + // skip the channel check if a pecl package says it's a PEAR package + break; + } + } + if ($info['channel'] == 'pear.php.net' && $newinfo['channel'] == 'pecl.php.net') { + $info['channel'] = 'pecl.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); + } + + 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) { + $package = $param->getPackage(); + $channel = $param->getChannel(); + // remove self if already installed with this version + // this does not need any pecl magic - we only remove exact matches + if ($param->_installRegistry->packageExists($package, $channel)) { + $packageVersion = $param->_installRegistry->packageInfo($package, 'version', $channel); + if (version_compare($packageVersion, $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 ' . $packageVersion); + } + $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 ' . $packageVersion); + $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; + } elseif (PEAR::isError($ret) && !isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + } + } 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; + } elseif (PEAR::isError($ret) && !isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + } + } + } + + // 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']); + } + + $ret = $this->_detect2Dep($dep, $pname, 'optional', $params); + if (PEAR::isError($ret) && !isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + + if (!$ret) { + $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()) { + continue; + } + + $groupname = 'default'; // try the default dependency group + } + + 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) && 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; + } elseif (PEAR::isError($ret) && !isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + } + } else { + $ret = $this->_detect2Dep($group[$packagetype], $pname, + 'dependency group "' . + $group['attribs']['name'] . '"', $params); + if (is_array($ret)) { + $this->_downloadDeps[] = $ret; + } elseif (PEAR::isError($ret) && !isset($options['soft'])) { + $this->_downloader->log(0, $ret->getMessage()); + } + } + } + } + } + + 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; + } + + // 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 (isset ($dep['type']) && $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) && 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(); + } + + 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); + } + + return false; + } + + function getPackageType() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackageType(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getPackageType(); + } + + return false; + } + + function isBundle() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackageType() == 'bundle'; + } + + return false; + } + + function getPackageXmlVersion() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getPackagexmlVersion(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getPackagexmlVersion(); + } + + return '1.0'; + } + + function getChannel() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getChannel(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getChannel(); + } + + return false; + } + + function getURI() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getURI(); + } elseif (isset($this->_downloadURL['info'])) { + return $this->_downloadURL['info']->getURI(); + } + + return false; + } + + function getVersion() + { + if (isset($this->_packagefile)) { + return $this->_packagefile->getVersion(); + } elseif (isset($this->_downloadURL['version'])) { + return $this->_downloadURL['version']; + } + + 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); + } + + return true; + } + + function setGroup($group) + { + $this->_parsedname['group'] = $group; + } + + function getGroup() + { + if (isset($this->_parsedname['group'])) { + return $this->_parsedname['group']; + } + + 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; + } + + return false; + } + + return false; + } + + function getDeps() + { + if (isset($this->_packagefile)) { + $ver = $this->_packagefile->getPackagexmlVersion(); + if (version_compare($ver, '2.0', '>=')) { + return $this->_packagefile->getDeps(true); + } + + 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); + } + + return $this->_downloadURL['info']->getDeps(); + } + + 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()); + } + + 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'])) { + $packageVersion = $this->_installRegistry->packageInfo($package, 'version', $channel); + if (version_compare($packageVersion, $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) { + $package = $param->getPackage(); + $channel = $param->getChannel(); + $group = $param->getGroup(); + if (!isset($existing[$channel . '/' . $package])) { + $existing[$channel . '/' . $package] = array(); + } + + if (!isset($existing[$channel . '/' . $package][$group])) { + $existing[$channel . '/' . $package][$group] = array(); + } + + $existing[$channel . '/' . $package][$group][] = $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()) { + $group = $ignoreGroups ? '' : $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; + } + + $group = $ignoreGroups ? '' : $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) + { + $bundles = $newparams = 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; + } + + // FIXME do symlink check + + 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) { + $merge = array_merge($params, $newparams); + if (!PEAR_Downloader_Package::willDownload($dep, $merge) + && !$param->isInstalled($dep) + ) { + $newdeps[] = $dep; + } else { + //var_dump($dep); + // 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; + } + + 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) + { + $a = &new PEAR_PackageFile($c, $d); + 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(); + $pkg = &$this->getPackagefileObject($this->_config, $this->_downloader->_debug); + 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|https|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, null, false, $this->getChannel()); + $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') { + return PEAR::raiseError('CRITICAL ERROR: package.xml downloaded does ' . + 'not match value returned from xml-rpc'); + } + + // 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'); + } + } + + // whew, download worked! + $pkg = &$this->getPackagefileObject($this->_config, $this->_downloader->debug); + + 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()); + } + + ///FIXME need to pass back some error code that we can use to match with to cancel all further operations + /// At least stop all deps of this package from being installed + $out = $saveparam ? $saveparam : $param; + $err = PEAR::raiseError('Download of "' . $out . '" 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(); + $channel = $this->_config->get('default_channel'); + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pname = $this->_registry->parsePackageName($param, $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, $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 = 'rest'; + } + + $this->_parsedname = $pname; + $this->_explicitState = isset($pname['state']) ? $pname['state'] : false; + $this->_explicitGroup = isset($pname['group']) ? true : 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 === false) { + $saveparam = !is_string($param) ? ", cannot download \"$param\"" : ''; + + // 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() . '")'); + } + } + + $preferred_state = $this->_config->get('preferred_state'); + if (!isset($info['url'])) { + $package_version = $this->_registry->packageInfo($info['info']->getPackage(), + 'version', $info['info']->getChannel()); + if ($this->isInstalled($info)) { + if ($isdependency && version_compare($info['version'], $package_version, '<=')) { + // 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; + } + } + + if ($info['version'] === $package_version) { + if (!isset($options['soft'])) { + $this->_downloader->log(1, 'WARNING: failed to download ' . $pname['channel'] . + '/' . $pname['package'] . '-' . $package_version. ', additionally the suggested version' . + ' (' . $package_version . ') is the same as the locally installed one.'); + } + + return false; + } + + if (version_compare($info['version'], $package_version, '<=')) { + if (!isset($options['soft'])) { + $this->_downloader->log(1, 'WARNING: failed to download ' . $pname['channel'] . + '/' . $pname['package'] . '-' . $package_version . ', additionally the suggested version' . + ' (' . $info['version'] . ') is a lower version than the locally installed one (' . $package_version . ').'); + } + + 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($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 "' . $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 "' . $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($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 "' . $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; + } + + // Checks if the user has a package installed already and checks the release against + // the state against the installed package, this allows upgrades for packages + // with lower stability than the preferred_state + $stability = $this->_registry->packageInfo($pname['package'], 'stability', $pname['channel']); + if (!$this->isInstalled($info) + || !in_array($info['info']->getState(), PEAR_Common::betterStates($stability['release'], true)) + ) { + $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; + } +} \ No newline at end of file diff --git a/PEAR/ErrorStack.php b/PEAR/ErrorStack.php new file mode 100644 index 0000000..0303f52 --- /dev/null +++ b/PEAR/ErrorStack.php @@ -0,0 +1,985 @@ + + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: ErrorStack.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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.9.4 + * @package PEAR_ErrorStack + * @category Debugging + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: ErrorStack.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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..4a0e7b8 --- /dev/null +++ b/PEAR/Exception.php @@ -0,0 +1,389 @@ + + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Exception.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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/FixPHP5PEARWarnings.php b/PEAR/FixPHP5PEARWarnings.php new file mode 100644 index 0000000..be5dc3c --- /dev/null +++ b/PEAR/FixPHP5PEARWarnings.php @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/PEAR/Frontend.php b/PEAR/Frontend.php new file mode 100644 index 0000000..531e541 --- /dev/null +++ b/PEAR/Frontend.php @@ -0,0 +1,228 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Frontend.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * Include error handling + */ +//require_once 'PEAR.php'; + +/** + * 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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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']; + } + + $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; + } + + $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..340b99b --- /dev/null +++ b/PEAR/Frontend/CLI.php @@ -0,0 +1,751 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: CLI.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class PEAR_Frontend_CLI extends PEAR_Frontend +{ + /** + * 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' => '', + ); + + 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) { + 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 + } + } + + /** + * @param object PEAR_Error object + */ + function displayError($e) + { + return $this->_displayLine($e->getMessage()); + } + + /** + * @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(); + exit(1); + } + + $raised = false; + foreach (debug_backtrace() as $i => $frame) { + if (!$raised) { + if (isset($frame['class']) + && strtolower($frame['class']) == 'pear' + && strtolower($frame['function']) == 'raiseerror' + ) { + $raised = true; + } else { + continue; + } + } + + $frame['class'] = !isset($frame['class']) ? '' : $frame['class']; + $frame['type'] = !isset($frame['type']) ? '' : $frame['type']; + $frame['function'] = !isset($frame['function']) ? '' : $frame['function']; + $frame['line'] = !isset($frame['line']) ? '' : $frame['line']; + $this->_displayLine("#$i: $frame[class]$frame[type]$frame[function] $frame[line]"); + } + } + } + + exit(1); + } + + /** + * 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'); + return; + } + + $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)) { + return; + } + + 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; + } + } + } + + $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 = $prompts = $types = array(); + foreach ($params as $param) { + $prompts[$param['name']] = $param['prompt']; + $types[$param['name']] = $param['type']; + $answers[$param['name']] = isset($param['default']) ? $param['default'] : ''; + } + + $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; + } + + function userDialog($command, $prompts, $types = array(), $defaults = array(), $screensize = 20) + { + if (!is_array($prompts)) { + return array(); + } + + $testprompts = array_keys($prompts); + $result = $defaults; + + reset($prompts); + if (count($prompts) === 1) { + foreach ($prompts as $key => $prompt) { + $type = $types[$key]; + $default = @$defaults[$key]; + print "$prompt "; + if ($default) { + print "[$default] "; + } + print ": "; + + $line = fgets(STDIN, 2048); + $result[$key] = ($default && trim($line) == '') ? $default : trim($line); + } + + return $result; + } + + $first_run = true; + while (true) { + $descLength = max(array_map('strlen', $prompts)); + $descFormat = "%-{$descLength}s"; + $last = count($prompts); + + $i = 0; + foreach ($prompts as $n => $var) { + $res = isset($result[$n]) ? $result[$n] : null; + printf("%2d. $descFormat : %s\n", ++$i, $prompts[$n], $res); + } + print "\n1-$last, 'all', 'abort', or Enter to continue: "; + + $tmp = trim(fgets(STDIN, 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(STDIN, 1024)); + if ($tmp !== '') { + $result[$var] = $tmp; + } + } elseif ($tmp == 'all') { + foreach ($prompts as $var => $desc) { + $current = $result[$var]; + print "$desc [$current] : "; + $tmp = trim(fgets(STDIN, 1024)); + if (trim($tmp) !== '') { + $result[$var] = trim($tmp); + } + } + } + + $first_run = false; + } + + return $result; + } + + 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; + } + + 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 (is_array($data) && 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(is_array($data) ? $data['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))); + } + + $packages = array(); + foreach($data['data'] as $category) { + foreach($category as $name => $pkg) { + $packages[$pkg[0]] = $pkg; + } + } + + $p = array_keys($packages); + natcasesort($p); + foreach ($p as $name) { + $this->_tableRow($packages[$name], 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))); + } + + $packages = array(); + foreach($data['data'] as $category) { + foreach($category as $name => $pkg) { + $packages[$pkg[0]] = $pkg; + } + } + + $p = array_keys($packages); + natcasesort($p); + foreach ($p as $name) { + $pkg = $packages[$name]; + unset($pkg[4], $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); + } + + if (is_array($data['data'])) { + foreach($data['data'] as $row) { + $this->_tableRow($row, null, $opts); + } + } else { + $this->_tableRow(array($data['data']), null, $opts); + } + $this->_endTable(); + } else { + $this->_displayLine($data); + } + } + } + } + + function log($text, $append_crlf = true) + { + if ($append_crlf) { + return $this->_displayLine($text); + } + + return $this->_display($text); + } + + function bold($text) + { + if (empty($this->term['bold'])) { + return strtoupper($text); + } + + return $this->term['bold'] . $text . $this->term['normal']; + } + + function _displayHeading($title) + { + print $this->lp.$this->bold($title)."\n"; + print $this->lp.str_repeat("=", strlen($title))."\n"; + } + + 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; + } + + function _tableRow($columns, $rowparams = array(), $colparams = array()) + { + $highest = 1; + for ($i = 0; $i < count($columns); $i++) { + $col = &$columns[$i]; + if (isset($colparams[$i]) && !empty($colparams[$i]['wrap'])) { + $col = wordwrap($col, $colparams[$i]['wrap']); + } + + if (strpos($col, "\n") !== false) { + $multiline = explode("\n", $col); + $w = 0; + foreach ($multiline as $n => $line) { + $len = strlen($line); + if ($len > $w) { + $w = $len; + } + } + $lines = count($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 (count($columns) > $this->params['ncols']) { + $this->params['ncols'] = count($columns); + } + + $new_row = array( + 'data' => $columns, + 'height' => $highest, + 'rowparams' => $rowparams, + 'colparams' => $colparams, + ); + $this->params['table_data'][] = $new_row; + } + + 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 < count($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 < count($data); $c++) { + $rowlines[$c] = preg_split('/(\r?\n|\r)/', $data[$c]); + if (count($rowlines[$c]) < $height) { + $rowlines[$c] = array_pad($rowlines[$c], $height, ''); + } + } + } else { + for ($c = 0; $c < count($data); $c++) { + $rowlines[$c] = array($data[$c]); + } + } + + for ($r = 0; $r < $height; $r++) { + $rowtext = ''; + for ($c = 0; $c < count($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); + } + } + + function _displayLine($text) + { + print "$this->lp$text\n"; + } + + function _display($text) + { + print $text; + } +} \ No newline at end of file diff --git a/PEAR/Installer.php b/PEAR/Installer.php new file mode 100644 index 0000000..eb17ca7 --- /dev/null +++ b/PEAR/Installer.php @@ -0,0 +1,1823 @@ + + * @author Tomas V.V. Cox + * @author Martin Jansen + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Installer.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 'src': + case 'extsrc': + $this->source_files++; + return; + 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; + 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); + } + + 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')); + } + + if ($atts['role'] != 'src') { + $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"); + } + } + } + } + // }}} + + if ($atts['role'] == 'src') { + rename($dest_file, $final_dest_file); + $this->log(2, "renamed source file $dest_file to $final_dest_file"); + } else { + $this->addFileOperation("rename", array($dest_file, $final_dest_file, + $atts['role'] == 'ext')); + } + } + + // Store the full path where the file was installed for easy unistall + if ($atts['role'] != 'script') { + $loc = $this->config->get($atts['role'] . '_dir'); + } else { + $loc = $this->config->get('bin_dir'); + } + + if ($atts['role'] != 'src') { + $this->addFileOperation("installed_as", array($file, $installed_as, + $loc, + dirname(substr($installedas_dest_file, strlen($loc))))); + } + + //$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, &$real_atts, $tmp_path, $options) + { + $atts = $real_atts; + 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; + } + + 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)) { + // Make sure the original md5 sum matches with expected + if (strtolower($md5sum) === strtolower($attribs['md5sum'])) { + $this->log(2, "md5sum ok: $final_dest_file"); + + if (isset($contents)) { + // set md5 sum based on $content in case any tasks were run. + $real_atts['attribs']['md5sum'] = md5($contents); + } + } 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); + } + + 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"); + } + } + } + } else { + $real_atts['attribs']['md5sum'] = md5_file($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')); + } + + if ($attribs['role'] != 'src') { + $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"); + } + } + } + } + // }}} + + if ($attribs['role'] == 'src') { + rename($dest_file, $final_dest_file); + $this->log(2, "renamed source file $dest_file to $final_dest_file"); + } else { + $this->addFileOperation("rename", array($dest_file, $final_dest_file, $role->isExtension())); + } + } + + // Store the full path where the file was installed for easy uninstall + if ($attribs['role'] != 'src') { + $loc = $this->config->get($role->getLocationConfig(), null, $channel); + $this->addFileOperation('installed_as', array($file, $installed_as, + $loc, + dirname(substr($installed_as, strlen($loc))))); + } + + //$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() + { + // {{{ first, check permissions and such manually + $errors = array(); + foreach ($this->file_operations as $key => $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); + } + } + + /* Verify we are not deleting a file owned by another package + * This can happen when a file moves from package A to B in + * an upgrade ala http://pear.php.net/17986 + */ + $info = array( + 'package' => strtolower($this->pkginfo->getName()), + 'channel' => strtolower($this->pkginfo->getChannel()), + ); + $result = $this->_registry->checkFileMap($data[0], $info, '1.1'); + if (is_array($result)) { + $res = array_diff($result, $info); + if (!empty($res)) { + $new = $this->_registry->getPackage($result[1], $result[0]); + $this->file_operations[$key] = false; + $this->log(3, "file $data[0] was scheduled for removal from {$this->pkginfo->getName()} but is owned by {$new->getChannel()}/{$new->getName()}, removal has been cancelled."); + } + } + } + break; + } + + } + // }}} + + $n = count($this->file_operations); + $this->log(2, "about to commit $n file operations for " . $this->pkginfo->getName()); + + $m = count($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': + $test = file_exists($data[1]) ? @unlink($data[1]) : 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]) && dirname($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) + { + // Parse xml file ----------------------------------------------- + $pkg = new PEAR_PackageFile($this->config, $this->debug); + 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'); + } + + $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(); + } else { + $descfile = $pkgfile; + $pkg = $this->_parsePackageXml($descfile); + if (PEAR::isError($pkg)) { + return $pkg; + } + } + + $tmpdir = dirname($descfile); + if (realpath($descfile) != realpath($pkgfile)) { + // Use the temp_dir since $descfile can contain the download dir path + $tmpdir = $this->config->get('temp_dir', null, 'pear.php.net'); + $tmpdir = System::mktemp('-d -t "' . $tmpdir . '"'); + + $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()) + ) { + if (isset($parentreg['filelist'][$file])) { + unset($parentreg['filelist'][$file]); + } else{ + $pos = strpos($file, '/'); + $basedir = substr($file, 0, $pos); + $file2 = substr($file, $pos + 1); + if (isset($parentreg['filelist'][$file2]['baseinstalldir']) + && $parentreg['filelist'][$file2]['baseinstalldir'] === $basedir + ) { + unset($parentreg['filelist'][$file2]); + } + } + + unset($test[$file]); + } + } else { + if (strtolower($param->getChannel()) != 'pear.php.net') { + continue; + } + + if (strtolower($info) == strtolower($param->getPackage())) { + if (isset($parentreg['filelist'][$file])) { + unset($parentreg['filelist'][$file]); + } else{ + $pos = strpos($file, '/'); + $basedir = substr($file, 0, $pos); + $file2 = substr($file, $pos + 1); + if (isset($parentreg['filelist'][$file2]['baseinstalldir']) + && $parentreg['filelist'][$file2]['baseinstalldir'] === $basedir + ) { + unset($parentreg['filelist'][$file2]); + } + } + + unset($test[$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 (count($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); + } + + if (!isset($options['soft'])) { + $this->log(0, "WARNING: $msg"); + } + } + } + } + // }}} + + $this->startFileTransaction(); + + $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['upgrade']) && empty($options['soft'])) { + // checks to do only when installing new packages + if (empty($options['force']) && $test) { + return $this->raiseError("$channel/$pkgname is already installed"); + } + } else { + // Upgrade + 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)"); + } + } + } + + // Do cleanups for upgrade and install, remove old release's files first + if ($test && 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); + } + + 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"); + } + } + + if (substr($pkgfile, -4) != '.xml') { + $tmpdir .= 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; + } + + $p = &$installregistry->getPackage($pkgname, $channel); + $dirtree = (empty($options['register-only']) && $p) ? $p->getDirTree() : false; + + $pkg->resetFilelist(); + $pkg->setLastInstalledVersion($installregistry->packageInfo($pkg->getPackage(), + 'version', $pkg->getChannel())); + foreach ($filelist as $file => $atts) { + $this->expectError(PEAR_INSTALLER_FAILED); + if ($pkg->getPackagexmlVersion() == '1.0') { + $res = $this->_installFile($file, $atts, $tmpdir, $options); + } else { + $res = $this->_installFile2($pkg, $file, $atts, $tmpdir, $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); + } + + if (!isset($options['soft'])) { + $this->log(0, "Warning: " . $res->getMessage()); + } + } + + $real = isset($atts['attribs']) ? $atts['attribs'] : $atts; + if ($res == PEAR_INSTALLER_OK && $real['role'] != 'src') { + // 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 { + if ($dirtree) { + $this->startFileTransaction(); + // attempt to delete empty directories + uksort($dirtree, array($this, '_sortDirs')); + foreach($dirtree as $dir => $notused) { + $this->addFileOperation('rmdir', array($dir)); + } + $this->commitFileTransaction(); + } + + $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); + $extra = $copyto != $dest ? " as '$copyto'" : ''; + $this->log(1, "Installing '$dest'$extra"); + + $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)"); + } + } + } + + + $data = 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'], + ); + + if ($filelist->getPackageXmlVersion() == '1.0') { + $filelist->installedFile($bn, $data); + } else { + $filelist->installedFile($bn, array('attribs' => $data)); + } + } + } + + // }}} + 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()) + { + $installRoot = isset($options['installroot']) ? $options['installroot'] : ''; + $this->config->setInstallRoot($installRoot); + + $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); + } + + 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); + } + + 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"); + } + + if (!isset($options['soft'])) { + $this->log(0, 'WARNING: uninstall failed'); + } + } else { + $this->startFileTransaction(); + $dirtree = $pkg->getDirTree(); + if ($dirtree === false) { + $this->configSet('default_channel', $savechannel); + return $this->_registry->deletePackage($package, $channel); + } + + // attempt to delete empty directories + uksort($dirtree, array($this, '_sortDirs')); + foreach($dirtree as $dir => $notused) { + $this->addFileOperation('rmdir', array($dir)); + } + + if (!$this->commitFileTransaction()) { + $this->rollbackFileTransaction(); + if (!isset($options['ignore-errors'])) { + return $this->raiseError("uninstall failed"); + } + + if (!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'); + } + } + + // }}} +} \ No newline at end of file diff --git a/PEAR/Installer/Role.php b/PEAR/Installer/Role.php new file mode 100644 index 0000000..0c50fa7 --- /dev/null +++ b/PEAR/Installer/Role.php @@ -0,0 +1,276 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Role.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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($class, $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)) { + return $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)) { + return $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)) { + return $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: does not exist/is not directory"); + } + + $dp = @opendir($dir); + if (empty($dp)) { + return PEAR::raiseError("registerRoles: opendir($dir) failed: $php_errmsg"); + } + + 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; + } +} \ No newline at end of file diff --git a/PEAR/Installer/Role/Cfg.php b/PEAR/Installer/Role/Cfg.php new file mode 100644 index 0000000..7620122 --- /dev/null +++ b/PEAR/Installer/Role/Cfg.php @@ -0,0 +1,106 @@ + + * @copyright 2007-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Cfg.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.7.0 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 2007-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.7.0 + */ +class PEAR_Installer_Role_Cfg extends PEAR_Installer_Role_Common +{ + /** + * @var PEAR_Installer + */ + var $installer; + + /** + * the md5 of the original file + * + * @var unknown_type + */ + var $md5 = null; + + /** + * 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) + { + $this->installer = &$installer; + $reg = &$this->installer->config->getRegistry(); + $package = $reg->getPackage($pkg->getPackage(), $pkg->getChannel()); + if ($package) { + $filelist = $package->getFilelist(); + if (isset($filelist[$file]) && isset($filelist[$file]['md5sum'])) { + $this->md5 = $filelist[$file]['md5sum']; + } + } + } + + function processInstallation($pkg, $atts, $file, $tmp_path, $layer = null) + { + $test = parent::processInstallation($pkg, $atts, $file, $tmp_path, $layer); + if (@file_exists($test[2]) && @file_exists($test[3])) { + $md5 = md5_file($test[2]); + // configuration has already been installed, check for mods + if ($md5 !== $this->md5 && $md5 !== md5_file($test[3])) { + // configuration has been modified, so save our version as + // configfile-version + $old = $test[2]; + $test[2] .= '.new-' . $pkg->getVersion(); + // backup original and re-install it + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $tmpcfg = $this->config->get('temp_dir'); + $newloc = System::mkdir(array('-p', $tmpcfg)); + if (!$newloc) { + // try temp_dir + $newloc = System::mktemp(array('-d')); + if (!$newloc || PEAR::isError($newloc)) { + PEAR::popErrorHandling(); + return PEAR::raiseError('Could not save existing configuration file '. + $old . ', unable to install. Please set temp_dir ' . + 'configuration variable to a writeable location and try again'); + } + } else { + $newloc = $tmpcfg; + } + + $temp_file = $newloc . DIRECTORY_SEPARATOR . uniqid('savefile'); + if (!@copy($old, $temp_file)) { + PEAR::popErrorHandling(); + return PEAR::raiseError('Could not save existing configuration file '. + $old . ', unable to install. Please set temp_dir ' . + 'configuration variable to a writeable location and try again'); + } + + PEAR::popErrorHandling(); + $this->installer->log(0, "WARNING: configuration file $old is being installed as $test[2], you should manually merge in changes to the existing configuration file"); + $this->installer->addFileOperation('rename', array($temp_file, $old, false)); + $this->installer->addFileOperation('delete', array($temp_file)); + } + } + + return $test; + } +} \ No newline at end of file diff --git a/PEAR/Installer/Role/Cfg.xml b/PEAR/Installer/Role/Cfg.xml new file mode 100644 index 0000000..7a415dc --- /dev/null +++ b/PEAR/Installer/Role/Cfg.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + cfg_dir + + 1 + + + + + \ No newline at end of file diff --git a/PEAR/Installer/Role/Common.php b/PEAR/Installer/Role/Common.php new file mode 100644 index 0000000..23e7348 --- /dev/null +++ b/PEAR/Installer/Role/Common.php @@ -0,0 +1,174 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Common.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..e3b7fa2 --- /dev/null +++ b/PEAR/Installer/Role/Data.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Data.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..d592fff --- /dev/null +++ b/PEAR/Installer/Role/Doc.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Doc.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..eceb027 --- /dev/null +++ b/PEAR/Installer/Role/Ext.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Ext.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..e2abf44 --- /dev/null +++ b/PEAR/Installer/Role/Php.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Php.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..b31469e --- /dev/null +++ b/PEAR/Installer/Role/Script.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Script.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..5037053 --- /dev/null +++ b/PEAR/Installer/Role/Src.php @@ -0,0 +1,34 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Src.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..1034834 --- /dev/null +++ b/PEAR/Installer/Role/Src.xml @@ -0,0 +1,12 @@ + + extsrc + zendextsrc + 1 + temp_dir + + + + + + + \ 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..14c0e60 --- /dev/null +++ b/PEAR/Installer/Role/Test.php @@ -0,0 +1,28 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Test.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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/Installer/Role/Www.php b/PEAR/Installer/Role/Www.php new file mode 100644 index 0000000..11adeff --- /dev/null +++ b/PEAR/Installer/Role/Www.php @@ -0,0 +1,28 @@ + + * @copyright 2007-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Www.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.7.0 + */ + +/** + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 2007-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.7.0 + */ +class PEAR_Installer_Role_Www extends PEAR_Installer_Role_Common {} +?> \ No newline at end of file diff --git a/PEAR/Installer/Role/Www.xml b/PEAR/Installer/Role/Www.xml new file mode 100644 index 0000000..7598be3 --- /dev/null +++ b/PEAR/Installer/Role/Www.xml @@ -0,0 +1,15 @@ + + php + extsrc + extbin + zendextsrc + zendextbin + 1 + www_dir + 1 + + + + + + \ No newline at end of file diff --git a/PEAR/PackageFile.php b/PEAR/PackageFile.php new file mode 100644 index 0000000..7ae3362 --- /dev/null +++ b/PEAR/PackageFile.php @@ -0,0 +1,492 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: PackageFile.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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; + + var $_logger = false; + /** + * @var boolean + */ + var $_rawReturn = false; + + /** + * helper for extracting Archive_Tar errors + * @var array + * @access private + */ + var $_extractErrors = array(); + + /** + * + * @param PEAR_Config $config + * @param ? $debug + * @param string @tmpdir Optional temporary directory for uncompressing + * files + */ + function PEAR_PackageFile(&$config, $debug = false) + { + $this->_config = $config; + $this->_debug = $debug; + } + + /** + * 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; + } + + 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->_config->get('verbose') > 0 + && $this->_logger && $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; + } + + if ($this->_logger && $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; + } 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)) { + $a = PEAR::raiseError('Parsing of package.xml from file "' . $file . '" failed', + 2, null, null, $pf->getValidationWarnings()); + return $a; + } + + if ($this->_logger && $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; + } + } + + /** + * 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; + } + + 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 (preg_match('/package.xml$/', $name, $match)) { + $xml = $name; + break; + } + } + + $tmpdir = System::mktemp('-t "' . $this->_config->get('temp_dir') . '" -d pear'); + if ($tmpdir === false) { + $ret = PEAR::raiseError("there was a problem with getting the configured temp directory"); + return $ret; + } + + 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 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) + { + $fp = 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 == ' + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v1.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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.9.4'; + } + + /** + * @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('required' => 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) + { + $ret = $tests = array(); + foreach ($deps as $name => $dep) { + foreach ($dep as $d) { + $tests[$name][] = $this->_processDep($d); + } + } + + foreach ($tests as $name => $test) { + $max = $min = $php = 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..4d202df --- /dev/null +++ b/PEAR/PackageFile/Generator/v2.php @@ -0,0 +1,893 @@ + + * @author Stephan Schmidt (original XML_Serializer code) + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v2.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a1 + */ +/** + * file/dir manipulation routines + */ +require_once 'System.php'; +require_once 'XML/Util.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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 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; + if (isset($this->_packagefile->encoding)) { + $this->_defaultOptions['encoding'] = $this->_packagefile->encoding; + } + } + + /** + * @return string + */ + function getPackagerVersion() + { + return '1.9.4'; + } + + /** + * @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'); + } + + $file = $where . DIRECTORY_SEPARATOR . 'package.xml'; + if (file_exists($file) && !is_file($file)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: unable to save package.xml as' . + ' "' . $file .'"'); + } + + 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($dest_package) && !is_file($dest_package)) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: cannot create tgz file "' . + $dest_package . '"'); + } + + $pkgfile = $this->_packagefile->getPackageFile(); + if (!$pkgfile) { + return PEAR::raiseError('PEAR_Packagefile_v2::toTgz: package file object must ' . + 'be created from a real file'); + } + + $pkgdir = dirname(realpath($pkgfile)); + $pkgfile = basename($pkgfile); + + // {{{ 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"); + } + + $origperms = fileperms($file); + $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); + } + + chmod($tfile, $origperms); + $filelist[$i++] = $tfile; + $this->_packagefile->setFileAttribute($fname, 'md5sum', md5_file($tfile), $i - 1); + $packager->log(2, "Adding file $fname"); + } + } + // }}} + + $name = $pf1 !== null ? 'package2.xml' : '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']); + } + + // Fix the notes a little bit + if (isset($arr['notes'])) { + // This trims out the indenting, needs fixing + $arr['notes'] = "\n" . trim($arr['notes']) . "\n"; + } + + if (isset($arr['changelog']) && !empty($arr['changelog'])) { + // Fix for inconsistency how the array is filled depending on the changelog release amount + if (!isset($arr['changelog']['release'][0])) { + $release = $arr['changelog']['release']; + unset($arr['changelog']['release']); + + $arr['changelog']['release'] = array(); + $arr['changelog']['release'][0] = $release; + } + + foreach (array_keys($arr['changelog']['release']) as $key) { + $c =& $arr['changelog']['release'][$key]; + if (isset($c['notes'])) { + // This trims out the indenting, needs fixing + $c['notes'] = "\n" . trim($c['notes']) . "\n"; + } + } + } + + 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.9.4'; + 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)) { + $tagName = isset($this->options['rootName']) ? $this->options['rootName'] : 'array'; + $this->_serializedData .= $this->_serializeArray($data, $tagName, $this->options['rootAttributes']); + } + + // add doctype declaration + if ($this->options['addDoctype'] === true) { + $this->_serializedData = XML_Util::getDoctypeDeclaration($tagName, $this->options['doctype']) + . $this->options['linebreak'] + . $this->_serializedData; + } + + // build xml declaration + if ($this->options['addDecl']) { + $atts = array(); + $encoding = isset($this->options['encoding']) ? $this->options['encoding'] : null; + $this->_serializedData = 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 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) && (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 = 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 { + $indent = $multiline = 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') + ) { + $tag['content'] = utf8_encode($tag['content']); + } + + if ($replaceEntities === true) { + $replaceEntities = XML_UTIL_ENTITIES_XML; + } + + $tag = XML_Util::createTagFromArray($tag, $replaceEntities, $multiline, $indent, $this->options['linebreak']); + } 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 = XML_Util::createTagFromArray($tag, $replaceEntities); + } + return $tag; + } +} \ 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..23395dc --- /dev/null +++ b/PEAR/PackageFile/Parser/v1.php @@ -0,0 +1,459 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v1.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @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) { + $a = &PEAR::raiseError('Cannot create xml parser for parsing package.xml'); + return $a; + } + 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); + $a = &PEAR::raiseError(sprintf("XML error: %s at line %d", + $str = xml_error_string($code), $line), 2); + return $a; + } + + 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"; + } elseif (trim(substr($line, 0, $indent_len))) { + $data .= ltrim($line); + } + } + 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': + 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 ;-) + // Trim only on the right side + $data = rtrim($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..a3ba706 --- /dev/null +++ b/PEAR/PackageFile/Parser/v2.php @@ -0,0 +1,113 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v2.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @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->encoding = $this->encoding; + $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..43e346b --- /dev/null +++ b/PEAR/PackageFile/v1.php @@ -0,0 +1,1612 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v1.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..1ca412d --- /dev/null +++ b/PEAR/PackageFile/v2.php @@ -0,0 +1,2049 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: v2.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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']); + // If the changelog isn't an array then it was passed in as an empty tag + if (isset($pinfo['changelog']) && !is_array($pinfo['changelog'])) { + unset($pinfo['changelog']); + } + $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'])) { + return false; + } + 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]) + || empty($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..33c8eee --- /dev/null +++ b/PEAR/PackageFile/v2/Validator.php @@ -0,0 +1,2154 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Validator.php 313023 2011-07-06 19:17:11Z dufuz $ + * @link http://pear.php.net/package/PEAR + * @since File available since Release 1.4.0a8 + */ +/** + * 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 + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a8 + * @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.9.4', + $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'); + } + $test = isset($this->_packageInfo['uri']) ? 'uri' :'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('channel' => $chan->getName(), + 'package' => $this->_pf->getPackage(), + 'name' => $valpack['_content'], + 'version' => $valpack['attribs']['version']), + '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.9.4'); + } + + 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); + } + + // Silence this function so we can catch PHP Warnings and show our own custom message + $tokens = @token_get_all($contents); + if (isset($php_errormsg)) { + if (isset($this->_stack)) { + $pn = $this->_pf->getPackage(); + $this->_stack->push(__FUNCTION__, 'warning', + array('file' => $file, 'package' => $pn), + 'in %file%: Could not process file for unkown reasons,' . + ' possibly a PHP parse error in %file% from %package%'); + } + } +/* + 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)) { + if (isset($this->_stack)) { + $this->_stack->push(__FUNCTION__, 'error', array('file' => $file), + 'Parser error: invalid PHP found in file "%file%"'); + } else { + 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') + ) + ) { + if (isset($this->_stack)) { + $this->_stack->push(__FUNCTION__, 'warning', array( + 'file' => $file), + 'Error, PHP5 token encountered in %file%,' . + ' analysis should be in PHP5'); + } else { + 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: + $token = $tokens[$i - 1][0]; + if (!($token == T_WHITESPACE || $token == T_STRING || $token == T_STATIC)) { + if (isset($this->_stack)) { + $this->_stack->push(__FUNCTION__, 'warning', array('file' => $file), + 'Parser error: invalid PHP found in file "%file%"'); + } else { + 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, + ); + } + + /** + * 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 = isset($this->_pf) ? $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..58f76c5 --- /dev/null +++ b/PEAR/PackageFile/v2/rw.php @@ -0,0 +1,1604 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: rw.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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['attribs']['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..8995a16 --- /dev/null +++ b/PEAR/Packager.php @@ -0,0 +1,201 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Packager.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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; + + 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"); + } + + 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"); + } + + 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"); + } + + 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"); + } + + 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)"); + } elseif (file_exists("$pkgdir/.svn")) { + $svnversion = preg_replace('/[^a-z0-9]/i', '.', $pf->getVersion()); + $svntag = $pf->getName() . "-$svnversion"; + $this->log(1, 'Tag the released code with "pear svntag ' . + $main->getPackageFile() . '"'); + $this->log(1, "(or set the SVN tag $svntag 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)"); + } elseif (file_exists("$pkgdir/.svn")) { + $svnversion = preg_replace('/[^a-z0-9]/i', '.', $pf->getVersion()); + $svntag = $pf->getName() . "-$svnversion"; + $this->log(1, "Tag the released code with `pear svntag $pkgfile'"); + $this->log(1, "(or set the SVN tag $svntag by hand)"); + } + } + + return $dest_package; + } +} \ No newline at end of file diff --git a/PEAR/REST.php b/PEAR/REST.php new file mode 100644 index 0000000..34a804f --- /dev/null +++ b/PEAR/REST.php @@ -0,0 +1,483 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: REST.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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, $channel = 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, $channel); + } + + /** + * 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, $channel = false) + { + $cacheId = $this->getCacheId($url); + if ($ret = $this->useLocalCache($url, $cacheId)) { + return $ret; + } + + $file = $trieddownload = false; + if (!isset($this->_options['offline'])) { + $trieddownload = true; + $file = $this->downloadHttp($url, $cacheId ? $cacheId['lastChange'] : false, $accept, $channel); + } + + if (PEAR::isError($file)) { + if ($file->getCode() !== -9276) { + return $file; + } + + $trieddownload = false; + $file = false; // use local copy if available on socket connect error + } + + if (!$file) { + $ret = $this->getCache($url); + if (!PEAR::isError($ret) && $trieddownload) { + // reset the age of the cache if the server says it was unmodified + $result = $this->saveCache($url, $ret, null, true, $cacheId); + if (PEAR::isError($result)) { + return PEAR::raiseError($result->getMessage()); + } + } + + return $ret; + } + + if (is_array($file)) { + $headers = $file[2]; + $lastmodified = $file[1]; + $content = $file[0]; + } else { + $headers = array(); + $lastmodified = false; + $content = $file; + } + + if ($forcestring) { + $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId); + if (PEAR::isError($result)) { + return PEAR::raiseError($result->getMessage()); + } + + return $content; + } + + if (isset($headers['content-type'])) { + switch ($headers['content-type']) { + case 'text/xml' : + case 'application/xml' : + case 'text/plain' : + if ($headers['content-type'] === 'text/plain') { + $check = substr($content, 0, 5); + if ($check !== '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(); + } + + $result = $this->saveCache($url, $content, $lastmodified, false, $cacheId); + if (PEAR::isError($result)) { + return PEAR::raiseError($result->getMessage()); + } + + 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)) { + return false; + } + + $cacheid = unserialize(implode('', file($cacheidfile))); + } + + $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)) { + return false; + } + + $ret = unserialize(implode('', file($cacheidfile))); + return $ret; + } + + function getCache($url) + { + $cachefile = $this->config->get('cache_dir') . DIRECTORY_SEPARATOR . + md5($url) . 'rest.cachefile'; + + if (!file_exists($cachefile)) { + return PEAR::raiseError('No cached content available for "' . $url . '"'); + } + + return unserialize(implode('', file($cachefile))); + } + + /** + * @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) + { + $cache_dir = $this->config->get('cache_dir'); + $d = $cache_dir . DIRECTORY_SEPARATOR . md5($url); + $cacheidfile = $d . 'rest.cacheid'; + $cachefile = $d . 'rest.cachefile'; + + if (!is_dir($cache_dir)) { + if (System::mkdir(array('-p', $cache_dir)) === false) { + return PEAR::raiseError("The value of config option cache_dir ($cache_dir) is not a directory and attempts to create the directory failed."); + } + } + + if ($cacheid === null && $nochange) { + $cacheid = unserialize(implode('', file($cacheidfile))); + } + + $idData = serialize(array( + 'age' => time(), + 'lastChange' => ($nochange ? $cacheid['lastChange'] : $lastmodified), + )); + + $result = $this->saveCacheFile($cacheidfile, $idData); + if (PEAR::isError($result)) { + return $result; + } elseif ($nochange) { + return true; + } + + $result = $this->saveCacheFile($cachefile, serialize($contents)); + if (PEAR::isError($result)) { + if (file_exists($cacheidfile)) { + @unlink($cacheidfile); + } + + return $result; + } + + return true; + } + + function saveCacheFile($file, $contents) + { + $len = strlen($contents); + + $cachefile_fp = @fopen($file, 'xb'); // x is the O_CREAT|O_EXCL mode + if ($cachefile_fp !== false) { // create file + if (fwrite($cachefile_fp, $contents, $len) < $len) { + fclose($cachefile_fp); + return PEAR::raiseError("Could not write $file."); + } + } else { // update file + $cachefile_lstat = lstat($file); + $cachefile_fp = @fopen($file, 'wb'); + if (!$cachefile_fp) { + return PEAR::raiseError("Could not open $file for writing."); + } + + $cachefile_fstat = fstat($cachefile_fp); + if ( + $cachefile_lstat['mode'] == $cachefile_fstat['mode'] && + $cachefile_lstat['ino'] == $cachefile_fstat['ino'] && + $cachefile_lstat['dev'] == $cachefile_fstat['dev'] && + $cachefile_fstat['nlink'] === 1 + ) { + if (fwrite($cachefile_fp, $contents, $len) < $len) { + fclose($cachefile_fp); + return PEAR::raiseError("Could not write $file."); + } + } else { + fclose($cachefile_fp); + $link = function_exists('readlink') ? readlink($file) : $file; + return PEAR::raiseError('SECURITY ERROR: Will not write to ' . $file . ' as it is symlinked to ' . $link . ' - Possible symlink attack'); + } + } + + fclose($cachefile_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, $channel = false) + { + static $redirect = 0; + // always reset , so we are clean case of error + $wasredirect = $redirect; + $redirect = 0; + + $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 . '"'); + } + + $host = isset($info['host']) ? $info['host'] : null; + $port = isset($info['port']) ? $info['port'] : null; + $path = isset($info['path']) ? $info['path'] : null; + $schema = (isset($info['scheme']) && $info['scheme'] == 'https') ? 'https' : 'http'; + + $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 ($schema === '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; + $proxy_schema = (isset($proxy['scheme']) && $proxy['scheme'] == 'https') ? 'https' : 'http'; + } + + if (empty($port)) { + $port = (isset($info['scheme']) && $info['scheme'] == 'https') ? 443 : 80; + } + + if (isset($proxy['host'])) { + $request = "GET $url HTTP/1.1\r\n"; + } else { + $request = "GET $path HTTP/1.1\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.9.4/PHP/" . PHP_VERSION . "\r\n"; + + $username = $this->config->get('username', null, $channel); + $password = $this->config->get('password', null, $channel); + + 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 .= "Accept-Encoding:\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 ($schema === '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(); + $reply = 0; + while ($line = trim(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 $schema://$host:$port$path not valid (received: $line)"); + } + } + } + + if ($reply != 200) { + if (!isset($headers['location'])) { + return PEAR::raiseError("File $schema://$host:$port$path not valid (redirected but no location)"); + } + + if ($wasredirect > 4) { + return PEAR::raiseError("File $schema://$host:$port$path not valid (redirection looped more than 5 times)"); + } + + $redirect = $wasredirect + 1; + return $this->downloadHttp($headers['location'], $lastmodified, $accept, $channel); + } + + $length = isset($headers['content-length']) ? $headers['content-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; + } +} \ No newline at end of file diff --git a/PEAR/REST/10.php b/PEAR/REST/10.php new file mode 100644 index 0000000..6ded7ae --- /dev/null +++ b/PEAR/REST/10.php @@ -0,0 +1,871 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: 10.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 = false) + { + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + + $channel = $packageinfo['channel']; + $package = $packageinfo['package']; + $state = isset($packageinfo['state']) ? $packageinfo['state'] : null; + $version = isset($packageinfo['version']) ? $packageinfo['version'] : null; + $restFile = $base . 'r/' . strtolower($package) . '/allreleases.xml'; + + $info = $this->_rest->retrieveData($restFile, false, false, $channel); + if (PEAR::isError($info)) { + return PEAR::raiseError('No releases available for package "' . + $channel . '/' . $package . '"'); + } + + if (!isset($info['r'])) { + return false; + } + + $release = $found = 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, false, $channel); + } + + function getDepDownloadURL($base, $xsdversion, $dependency, $deppackage, + $prefstate = 'stable', $installed = false, $channel = false) + { + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + + $channel = $dependency['channel']; + $package = $dependency['name']; + $state = isset($dependency['state']) ? $dependency['state'] : null; + $version = isset($dependency['version']) ? $dependency['version'] : null; + $restFile = $base . 'r/' . strtolower($package) . '/allreleases.xml'; + + $info = $this->_rest->retrieveData($restFile, false, false, $channel); + 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') { + 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 { + $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']); + } + } + } + $release = $found = 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, false, $channel); + } + } + } + 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, false, $channel); + } + + /** + * 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, $channel = false) + { + if (!$found) { + $release = $info['r'][0]; + } + + $packageLower = strtolower($package); + $pinfo = $this->_rest->retrieveCacheFirst($base . 'p/' . $packageLower . '/' . + 'info.xml', false, false, $channel); + if (PEAR::isError($pinfo)) { + return PEAR::raiseError('Package "' . $package . + '" does not have REST info xml available'); + } + + $releaseinfo = $this->_rest->retrieveCacheFirst($base . 'r/' . $packageLower . '/' . + $release['v'] . '.xml', false, false, $channel); + if (PEAR::isError($releaseinfo)) { + return PEAR::raiseError('Package "' . $package . '" Version "' . $release['v'] . + '" does not have REST xml available'); + } + + $packagexml = $this->_rest->retrieveCacheFirst($base . 'r/' . $packageLower . '/' . + 'deps.' . $release['v'] . '.txt', false, true, $channel); + 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/' . $packageLower . + '/allreleases.xml', false, false, $channel); + if (PEAR::isError($allinfo)) { + return $allinfo; + } + + 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; + } + + $deprecated = false; + 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'])); + } + } + + $return = array( + 'version' => $releaseinfo['v'], + 'info' => $packagexml, + 'package' => $releaseinfo['p']['_content'], + 'stability' => $releaseinfo['st'], + 'compatible' => $compatible, + 'deprecated' => $deprecated, + ); + + if ($found) { + $return['url'] = $releaseinfo['g']; + return $return; + } + + $return['php'] = $phpversion; + return $return; + } + + function listPackages($base, $channel = false) + { + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml', false, false, $channel); + 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, $channel = false) + { + $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', false, false, $channel); + 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', false, false, $channel); + 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, $channel = false) + { + // gives '404 Not Found' error when category doesn't exist + $packagelist = $this->_rest->retrieveData($base.'c/'.urlencode($category).'/packages.xml', false, false, $channel); + 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, false, false, $channel); + if (PEAR::isError($version)) { + break; // skipit + } + $url = sprintf('%s'.'r/%s/%s.xml', + $base, + strtolower($packageitem['_content']), + $version); + $info = $this->_rest->retrieveData($url, false, false, $channel); + if (PEAR::isError($info)) { + break; // skipit + } + $packagelist[$i]['info'] = $info; + } + PEAR::popErrorHandling(); + } + + return $packagelist; + } + + + function listAll($base, $dostable, $basic = true, $searchpackage = false, $searchsummary = false, $channel = false) + { + $packagelist = $this->_rest->retrieveData($base . 'p/packages.xml', false, false, $channel); + if (PEAR::isError($packagelist)) { + return $packagelist; + } + if ($this->_rest->config->get('verbose') > 0) { + $ui = &PEAR_Frontend::singleton(); + $ui->log('Retrieving data...0%', true); + } + $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', false, false, $channel); + } else { + $latest = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/latest.txt', false, false, $channel); + } + if (PEAR::isError($latest)) { + $latest = false; + } + $info = array('stable' => $latest); + } else { // list-all command + $inf = $this->_rest->retrieveData($base . 'p/' . strtolower($package) . '/info.xml', false, false, $channel); + 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', false, false, $channel); + 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', false, false, $channel); + 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', false, false, $channel); + 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', false, false, $channel); + PEAR::popErrorHandling(); + if (PEAR::isError($info)) { + continue; // no remote releases + } + + if (!isset($info['r'])) { + continue; + } + + $release = $found = false; + if (!is_array($info['r']) || !isset($info['r'][0])) { + $info['r'] = array($info['r']); + } + + // $info['r'] is sorted by version number + usort($info['r'], array($this, '_sortReleasesByVersionNumber')); + 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', false, false, $channel); + if (PEAR::isError($relinfo)) { + return $relinfo; + } + + $ret[$package] = array( + 'version' => $release['v'], + 'state' => $release['s'], + 'filesize' => $relinfo['f'], + ); + } + + return $ret; + } + + function packageInfo($base, $package, $channel = false) + { + PEAR::pushErrorHandling(PEAR_ERROR_RETURN); + $pinfo = $this->_rest->retrieveData($base . 'p/' . strtolower($package) . '/info.xml', false, false, $channel); + if (PEAR::isError($pinfo)) { + PEAR::popErrorHandling(); + return PEAR::raiseError('Unknown package: "' . $package . '" in channel "' . $channel . '"' . "\n". 'Debug: ' . + $pinfo->getMessage()); + } + + $releases = array(); + $allreleases = $this->_rest->retrieveData($base . 'r/' . strtolower($package) . + '/allreleases.xml', false, false, $channel); + 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', false, false, $channel); + 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', false, false, $channel); + + 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; + } + + if (!isset($latest)) { + $latest = ''; + } + + 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); + } + + /** + * Sort releases by version number + * + * @access private + */ + function _sortReleasesByVersionNumber($a, $b) + { + if (version_compare($a['v'], $b['v'], '=')) { + return 0; + } + + if (version_compare($a['v'], $b['v'], '>')) { + return -1; + } + + if (version_compare($a['v'], $b['v'], '<')) { + return 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..831cfcc --- /dev/null +++ b/PEAR/REST/11.php @@ -0,0 +1,341 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: 11.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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, $searchpackage = false, $searchsummary = false, $channel = false) + { + $categorylist = $this->_rest->retrieveData($base . 'c/categories.xml', false, false, $channel); + 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', false, false, $channel); + + 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) { + if (empty($packageinfo)) { + continue; + } + + $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 + if (!isset($unstable)) { + $unstable = false; + $state = 'stable'; + if (isset($stable)) { + $latest = $unstable = $stable; + } + } else { + $latest = $unstable; + } + + if (!isset($latest)) { + $latest = false; + } + + $deps = array(); + 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, $channel = false) + { + $categorylist = $this->_rest->retrieveData($base . 'c/categories.xml', false, false, $channel); + 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, $channel = 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, false, false, $channel); + 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; + } + + // 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..722ae0d --- /dev/null +++ b/PEAR/REST/13.php @@ -0,0 +1,299 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: 13.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 = false) + { + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + + $channel = $packageinfo['channel']; + $package = $packageinfo['package']; + $state = isset($packageinfo['state']) ? $packageinfo['state'] : null; + $version = isset($packageinfo['version']) ? $packageinfo['version'] : null; + $restFile = $base . 'r/' . strtolower($package) . '/allreleases2.xml'; + + $info = $this->_rest->retrieveData($restFile, false, false, $channel); + if (PEAR::isError($info)) { + return PEAR::raiseError('No releases available for package "' . + $channel . '/' . $package . '"'); + } + + if (!isset($info['r'])) { + return false; + } + + $release = $found = 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, $channel); + } + + function getDepDownloadURL($base, $xsdversion, $dependency, $deppackage, + $prefstate = 'stable', $installed = false, $channel = false) + { + $states = $this->betterStates($prefstate, true); + if (!$states) { + return PEAR::raiseError('"' . $prefstate . '" is not a valid state'); + } + + $channel = $dependency['channel']; + $package = $dependency['name']; + $state = isset($dependency['state']) ? $dependency['state'] : null; + $version = isset($dependency['version']) ? $dependency['version'] : null; + $restFile = $base . 'r/' . strtolower($package) .'/allreleases2.xml'; + + $info = $this->_rest->retrieveData($restFile, false, false, $channel); + 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']); + } + } + } + + $skippedphp = $found = $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'])) { + 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; + } + + 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, false, $channel); + } + } + + 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, $channel); + } +} \ No newline at end of file diff --git a/PEAR/Registry.php b/PEAR/Registry.php new file mode 100644 index 0000000..35e17db --- /dev/null +++ b/PEAR/Registry.php @@ -0,0 +1,2395 @@ + + * @author Tomas V. V. Cox + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Registry.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 1.4.0a1 + */ +class PEAR_Registry extends PEAR +{ + /** + * 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 false|PEAR_ChannelFile + */ + var $_docChannel; + + /** + * @var PEAR_DependencyDB + */ + var $_dependencyDB; + + /** + * @var PEAR_Config + */ + var $_config; + + /** + * 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(); + $this->setInstallDir($pear_install_dir); + $this->_pearChannel = $pear_channel; + $this->_peclChannel = $pecl_channel; + $this->_config = false; + } + + function setInstallDir($pear_install_dir = PEAR_INSTALL_DIR) + { + $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'; + } + + function hasWriteAccess() + { + if (!file_exists($this->install_dir)) { + $dir = $this->install_dir; + while ($dir && $dir != '.') { + $olddir = $dir; + $dir = dirname($dir); + if ($dir != '.' && file_exists($dir)) { + if (is_writeable($dir)) { + return true; + } + + 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, $resetInstallDir = true) + { + $this->_config = &$config; + if ($resetInstallDir) { + $this->setInstallDir($config->get('php_dir')); + } + } + + 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 . 'doc.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->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/'); + $pear_channel->setBaseURL('REST1.3', 'http://pear.php.net/rest/'); + //$pear_channel->setBaseURL('REST1.4', 'http://pear.php.net/rest/'); + } else { + $pear_channel->setServer('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->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->setServer('pecl.php.net'); + $pecl_channel->setAlias('pecl'); + } + + $pecl_channel->validate(); + $this->_addChannel($pecl_channel); + } + + if (!file_exists($this->channelsdir . $ds . 'doc.php.net.reg')) { + $doc_channel = $this->_docChannel; + if (!is_a($doc_channel, 'PEAR_ChannelFile') || !$doc_channel->validate()) { + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + + $doc_channel = new PEAR_ChannelFile; + $doc_channel->setAlias('phpdocs'); + $doc_channel->setServer('doc.php.net'); + $doc_channel->setSummary('PHP Documentation Team'); + $doc_channel->setDefaultPEARProtocols(); + $doc_channel->setBaseURL('REST1.0', 'http://doc.php.net/rest/'); + $doc_channel->setBaseURL('REST1.1', 'http://doc.php.net/rest/'); + $doc_channel->setBaseURL('REST1.3', 'http://doc.php.net/rest/'); + } else { + $doc_channel->setServer('doc.php.net'); + $doc_channel->setAlias('doc'); + } + + $doc_channel->validate(); + $this->_addChannel($doc_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->setDefaultPEARProtocols(); + $private->setBaseURL('REST1.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? + $file = OS_WINDOWS ? 'pear.ini' : '.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; + } + } + } + + /** + * PEAR_Registry destructor. Makes sure no locks are forgotten. + * + * @access private + */ + function _PEAR_Registry() + { + parent::_PEAR(); + if (is_resource($this->lock_fp)) { + $this->_unlock(); + } + } + + /** + * 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 . 'doc.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; + } + + /** + * 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; + } + + /** + * 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; + } + + /** + * 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'; + } + + /** + * 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'; + } + + /** + * @param string + * @return string + */ + function _getChannelAliasFileName($alias) + { + return $this->channelsdir . DIRECTORY_SEPARATOR . '.alias' . + DIRECTORY_SEPARATOR . str_replace('/', '_', strtolower($alias)) . '.txt'; + } + + /** + * 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 == 'doc.php.net') { + return 'doc.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))); + } + + return $channel; + } + + /** + * 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'; + } + + if ($channel == 'doc.php.net') { + return 'phpdocs'; + } + + return false; + } + + $channel = $this->_getChannel($channel); + if (PEAR::isError($channel)) { + return $channel; + } + + return $channel->getAlias(); + } + + /** + * 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; + } + + $ch = $this->_getChannelFromAlias($channel); + if (!$ch) { + $ch = $channel; + } + + return $this->statedir . DIRECTORY_SEPARATOR . strtolower('.channel.' . + str_replace('/', '_', $ch)); + } + + 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; + } + + function _closePackageFile($fp) + { + fclose($fp); + } + + 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; + } + + function _closeChannelFile($fp) + { + fclose($fp); + } + + 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; + } + + 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 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 (stristr(php_uname(), 'Windows 9')) { + return true; + } + + 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'); + } + + 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)) { + $this->lock_fp = null; + 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; + } + + //is resource at this point, close it on error. + fclose($this->lock_fp); + $this->lock_fp = null; + return $this->raiseError("could not acquire $str lock ($this->lockfile)", + PEAR_REGISTRY_ERROR_LOCK); + } + + return true; + } + + function _unlock() + { + $ret = $this->_lock(LOCK_UN); + if (is_resource($this->lock_fp)) { + fclose($this->lock_fp); + } + + $this->lock_fp = null; + return $ret; + } + + function _packageExists($package, $channel = false) + { + return file_exists($this->_packageFileName($package, $channel)); + } + + /** + * 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; + } + + if (!$a && $channel == 'doc.php.net') { + return true; + } + + return $a; + } + + /** + * Determine whether a mirror exists within the deafult channel in the registry + * + * @param string Channel name + * @param string Mirror name + * + * @return boolean + */ + function _mirrorExists($channel, $mirror) + { + $data = $this->_channelInfo($channel); + if (!isset($data['servers']['mirror'])) { + return false; + } + + foreach ($data['servers']['mirror'] as $m) { + if ($m['attribs']['host'] == $mirror) { + return true; + } + } + + return false; + } + + /** + * @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', 'doc.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; + } + + /** + * 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')) { + return false; + } + + if (!$channel->validate()) { + return false; + } + $channel = $channel->getName(); + } + + if ($this->_getChannelFromAlias($channel) == '__uri') { + return false; + } + + if ($this->_getChannelFromAlias($channel) == 'pecl.php.net') { + return false; + } + + if ($this->_getChannelFromAlias($channel) == 'doc.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; + } + + /** + * Determine whether a channel exists in the registry + * @param string Channel Alias + * @return boolean + */ + function _isChannelAlias($alias) + { + return file_exists($this->_getChannelAliasFileName($alias)); + } + + /** + * @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; + } + + /** + * @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; + } + + function _listChannels() + { + $channellist = array(); + if (!file_exists($this->channelsdir) || !is_dir($this->channelsdir)) { + return array('pear.php.net', 'pecl.php.net', 'doc.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('doc.php.net', $channellist)) { + $channellist[] = 'doc.php.net'; + } + + + if (!in_array('__uri', $channellist)) { + $channellist[] = '__uri'; + } + + natsort($channellist); + return $channellist; + } + + 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; + } + + 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->setServer('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/'); + $pear_channel->setBaseURL('REST1.3', '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->setServer('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) == 'doc.php.net') { + // the registry is not properly set up, so use defaults + if (!class_exists('PEAR_ChannelFile')) { + require_once 'PEAR/ChannelFile.php'; + } + + $doc_channel = new PEAR_ChannelFile; + $doc_channel->setServer('doc.php.net'); + $doc_channel->setAlias('phpdocs'); + $doc_channel->setSummary('PHP Documentation Team'); + $doc_channel->setDefaultPEARProtocols(); + $doc_channel->setBaseURL('REST1.0', 'http://doc.php.net/rest/'); + $doc_channel->setBaseURL('REST1.1', 'http://doc.php.net/rest/'); + $doc_channel->setBaseURL('REST1.3', 'http://doc.php.net/rest/'); + return $doc_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->setDefaultPEARProtocols(); + $private->setBaseURL('REST1.0', '****'); + $private->setSummary('Pseudo-channel for static packages'); + return $private; + } + + return $ch; + } + + /** + * @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; + } + + // }}} + + /** + * @param string channel name mirror is in + * @param string mirror name + * + * @return bool + */ + function mirrorExists($channel, $mirror) + { + if (PEAR::isError($e = $this->_lock(LOCK_SH))) { + return $e; + } + + $ret = $this->_mirrorExists($channel, $mirror); + $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') || !$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); + $ret = file_exists($file) ? @unlink($file) : 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); + $this->_unlock(); + if (!$ret) { + return PEAR::raiseError('Unknown channel: ' . $channel); + } + 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; + } +} \ No newline at end of file diff --git a/PEAR/RunTest.php b/PEAR/RunTest.php new file mode 100644 index 0000000..5182490 --- /dev/null +++ b/PEAR/RunTest.php @@ -0,0 +1,968 @@ + + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: RunTest.php 313024 2011-07-06 19:51:24Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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 $tests_count; + var $xdebug_loaded; + /** + * Saved value of php executable, used to reset $_php when we + * have a test that uses cgi + * + * @var unknown_type + */ + var $_savephp; + 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()) + { + if (!defined('E_DEPRECATED')) { + define('E_DEPRECATED', 0); + } + if (!defined('E_STRICT')) { + define('E_STRICT', 0); + } + $this->ini_overwrites[] = 'error_reporting=' . (E_ALL & ~(E_DEPRECATED | E_STRICT)); + 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); + } + + /** + * Turns a PHP INI string into an array + * + * Turns -d "include_path=/foo/bar" into this: + * array( + * 'include_path' => array( + * 'operator' => '-d', + * 'value' => '/foo/bar', + * ) + * ) + * Works both with quotes and without + * + * @param string an PHP INI string, -d "include_path=/foo/bar" + * @return array + */ + function iniString2array($ini_string) + { + if (!$ini_string) { + return array(); + } + $split = preg_split('/[\s]|=/', $ini_string, -1, PREG_SPLIT_NO_EMPTY); + $key = $split[1][0] == '"' ? substr($split[1], 1) : $split[1]; + $value = $split[2][strlen($split[2]) - 1] == '"' ? substr($split[2], 0, -1) : $split[2]; + // FIXME review if this is really the struct to go with + $array = array($key => array('operator' => $split[0], 'value' => $value)); + return $array; + } + + 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) { + if (is_array($value)) { + $operator = $value['operator']; + $value = $value['value']; + } else { + $operator = '-d'; + } + $value = addslashes($value); + $settings .= " $operator \"$name=$value\""; + } + return $settings; + } + + function _preparePhpBin($php, $file, $ini_settings) + { + $file = escapeshellarg($file); + // This was fixed in php 5.3 and is not needed after that + if (OS_WINDOWS && version_compare(PHP_VERSION, '5.3', '<')) { + $cmd = '"'.escapeshellarg($php).' '.$ini_settings.' -f ' . $file .'"'; + } else { + $cmd = $php . $ini_settings . ' -f ' . $file; + } + + return $cmd; + } + + function runPHPUnit($file, $ini_settings = '') + { + if (!file_exists($file) && file_exists(getcwd() . DIRECTORY_SEPARATOR . $file)) { + $file = realpath(getcwd() . DIRECTORY_SEPARATOR . $file); + } elseif (file_exists($file)) { + $file = realpath($file); + } + + $cmd = $this->_preparePhpBin($this->_php, $file, $ini_settings); + 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 + } + + /** + * Runs an individual test case. + * + * @param string The filename of the test + * @param array|string INI settings to be applied to the test run + * @param integer Number what the current running test is of the + * whole test suite being runned. + * + * @return string|object Returns PASSED, WARNED, FAILED depending on how the + * test came out. + * PEAR Error when the tester it self fails + */ + function run($file, $ini_settings = array(), $test_number = 1) + { + if (isset($this->_savephp)) { + $this->_php = $this->_savephp; + unset($this->_savephp); + } + if (empty($this->_options['cgi'])) { + // try to see if php-cgi is in the path + $res = $this->system_with_timeout('php-cgi -v'); + if (false !== $res && !(is_array($res) && in_array($res[0], array(-1, 127)))) { + $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']; + } + + if (is_string($ini_settings)) { + $ini_settings = $this->iniString2array($ini_settings); + } + + $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->_savephp = $this->_php; + $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) { + $xdebug_file = $temp_dir . DIRECTORY_SEPARATOR . $main_file_name . 'xdebug'; + $text = "\n" . 'function coverage_shutdown() {' . + "\n" . ' $xdebug = var_export(xdebug_get_code_coverage(), true);'; + if (!function_exists('file_put_contents')) { + $text .= "\n" . ' $fh = fopen(\'' . $xdebug_file . '\', "wb");' . + "\n" . ' if ($fh !== false) {' . + "\n" . ' fwrite($fh, $xdebug);' . + "\n" . ' fclose($fh);' . + "\n" . ' }'; + } else { + $text .= "\n" . ' file_put_contents(\'' . $xdebug_file . '\', $xdebug);'; + } + + // Workaround for http://pear.php.net/bugs/bug.php?id=17292 + $lines = explode("\n", $section_text['FILE']); + $numLines = count($lines); + $namespace = ''; + $coverage_shutdown = 'coverage_shutdown'; + + if ( + substr($lines[0], 0, 2) == 'save_text($temp_file, "save_text($temp_file, $section_text['FILE']); + } + + $args = $section_text['ARGS'] ? ' -- '.$section_text['ARGS'] : ''; + $cmd = $this->_preparePhpBin($this->_php, $temp_file, $ini_settings); + $cmd.= "$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); + } + + if (isset($section_text['EXPECT'])) { + $wanted = preg_replace('/\r\n/', "\n", trim($section_text['EXPECT'])); + } else { + $wanted = ''; + } + + // 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'])) { + if (strpos($section_text['ENV'], '{PWD}') !== false) { + $section_text['ENV'] = str_replace('{PWD}', dirname($temp_file), $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']); + $output = $this->system_with_timeout("$this->_php $temp_clean 2>&1"); + if (strlen($output[1])) { + echo "BORKED --CLEAN-- section! output:\n", $output[1]; + } + 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..5b99c2e --- /dev/null +++ b/PEAR/Task/Common.php @@ -0,0 +1,202 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Common.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..e43ecca --- /dev/null +++ b/PEAR/Task/Postinstallscript.php @@ -0,0 +1,323 @@ + + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Postinstallscript.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..8f358bf --- /dev/null +++ b/PEAR/Task/Postinstallscript/rw.php @@ -0,0 +1,169 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: rw.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..376df64 --- /dev/null +++ b/PEAR/Task/Replace.php @@ -0,0 +1,176 @@ + + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Replace.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..32dad58 --- /dev/null +++ b/PEAR/Task/Replace/rw.php @@ -0,0 +1,61 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: rw.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..89ca81b --- /dev/null +++ b/PEAR/Task/Unixeol.php @@ -0,0 +1,77 @@ + + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Unixeol.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..b2ae5fa --- /dev/null +++ b/PEAR/Task/Unixeol/rw.php @@ -0,0 +1,56 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: rw.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..8ba4171 --- /dev/null +++ b/PEAR/Task/Windowseol.php @@ -0,0 +1,77 @@ + + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Windowseol.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..f0f1149 --- /dev/null +++ b/PEAR/Task/Windowseol/rw.php @@ -0,0 +1,56 @@ + - read/write version + * + * PHP versions 4 and 5 + * + * @category pear + * @package PEAR + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: rw.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..176560b --- /dev/null +++ b/PEAR/Validate.php @@ -0,0 +1,629 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: Validate.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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; + } + + $result = preg_match('|\d{2}\:\d{2}\:\d{2}|', $this->_packagexml->getTime(), $matches); + if ($result === false || empty($matches)) { + $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; + } +} \ No newline at end of file diff --git a/PEAR/Validator/PECL.php b/PEAR/Validator/PECL.php new file mode 100644 index 0000000..89b951f --- /dev/null +++ b/PEAR/Validator/PECL.php @@ -0,0 +1,63 @@ + + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id: PECL.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: 1.9.4 + * @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..7f091c1 --- /dev/null +++ b/PEAR/XMLParser.php @@ -0,0 +1,253 @@ + + * @author Stephan Schmidt (original XML_Unserializer code) + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license New BSD License + * @version CVS: $Id: XMLParser.php 313023 2011-07-06 19:17:11Z dufuz $ + * @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-2009 The Authors + * @license http://opensource.org/licenses/bsd-license New BSD License + * @version Release: 1.9.4 + * @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; + + /** + * The XML encoding to use + * @var string $encoding + */ + var $encoding = 'ISO-8859-1'; + + /** + * @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->_dataStack = $this->_valStack = array(); + $this->_depth = 0; + + if ( + strpos($data, 'encoding="UTF-8"') + || strpos($data, 'encoding="utf-8"') + || strpos($data, "encoding='UTF-8'") + || strpos($data, "encoding='utf-8'") + ) { + $this->encoding = 'UTF-8'; + } + + if (version_compare(phpversion(), '5.0.0', 'lt') && $this->encoding == 'UTF-8') { + $data = utf8_decode($data); + $this->encoding = 'ISO-8859-1'; + } + + $xp = xml_parser_create($this->encoding); + 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) + { + $this->_depth++; + $this->_dataStack[$this->_depth] = null; + + $val = array( + 'name' => $element, + 'value' => null, + 'type' => 'string', + '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; + } + + $value['value'] = isset($value['children']) ? $value['children'] : 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; + } + + // 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/Starting with PHP and Telldus LIVE on server with cPanel (oAuth and API).pdf b/Starting with PHP and Telldus LIVE on server with cPanel (oAuth and API).pdf new file mode 100644 index 0000000000000000000000000000000000000000..42d8140c507f82f93a03f4cd8411ada7e48ff246 GIT binary patch literal 960051 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
nGMnO|8CpyB;cDCPSzx`zW_Zb7}dmto_;kD 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%0Dbimpn& z0B36tNw+QfMF9N!z*T+>J2Gf6`_Y`jf}nd+x^M6WqaoksM6dwFmi@XZ0>oA*5k4s| zVi-(Ix`RHUE$-Hu;0kyzUjQe=mMwKnOhkDhv71sn57jw5wFgh3Z9zgKvTYs= z2Z}9BZ?r^t-Vq0~EuoGAxvm6RWnscR;&1rgKZI&3LuF9v$Xa04E@3pF)wf|^0~V;l zM!>37kZ8cF3lLKvj3xwM;;-ln6H*l8BinJ}zUNaXbcUBpui0T{ys!?dQ1U2d&nig6 z=ho#tD6!}AMqEJ+_fQ@yy+vJ7Bz;%J7xnnG>znkfcqY~ixoex`LTRORHa#ZQ0k^A` zq^)Qx-VALDHylmLk*gEyh&Non#5FXZs}o&=y-S^BsI^&ak4Y|6V-L;+nXq^z_{9&8 z0Y>z?us*Dc(O^%;3S?79n8lEP6Bn=;>=9fG?cf= zM~K)rZ^=&-izvs4A{$I8OxlgvC1q*;IWRky!~z-#T&`=dNb8gj?2%0xa#$#-Qn5qy z5wz$UEa_fpO~eCZ7ca?&(r&x1E#!)P*fps$*IHyT;tG5?ixbT)x2_g#=dcijMgK}> zkSh{;A7z}R4iUIg1`+;isbD+e68%8zsTM;OBifMbuxq94B2UVq%!e`=fd~#~Jw_R< z`QNNMWoRmsw!}DDd&HAq(!l6ANl8Kt%hXHo$a_~4ieaTB63XvNzWvZ|ki+XrzQT`4 zyO2re4r-kukLbHBNeW8*LR|h=Ov4hC1Y0L(;vPt*7{joXej<-F!`GA(dAyM|D7#om zu1Nz9{RAS5Q8mz}B*UlPcOmjum&|~W;ARjOy^sZ5K-$ia#b9y(EkG9VMVKTnfH8|P znlYI%#Qw7(b6&Qh1ZNgw_y)-=GHmMcl=%+_>mMI;- zr_=N6Q1Iht$yEQdRc6CAc_k7F%;&evTxO~?5>4l&l-FEosxmT;4*W{egR`p4+T{73cF$IsbB@E|_x7@TJwNW}*zxW0#cB^0hmqWU-zqwJUp4FW zdBghiaxh}5qAOIMe{n|VS18aRyWm`o-f>~b7Z5xqj(gel%vq#p^9V~MScYpTh9_0tF5*@94I&SMb!kgJTppUJ*w;czb`tyBd zZR_oBp1!RYuTZZK-hS=4{Aw*~TUaTKn;Jy{#d*=VSPgI(Bs#=78FS5B>{$05q-UqV zlSclrtf9Jf)oX>^N9hf9BPl80L~dvU2nGL|gp33?Hbs*kVs;Hv8)tr(exG`uLB?3g z0;@DHS`od3NEQ>Bj(;nsN-4dB*vBr)kM-#Ilat=jbLT~pi8`T3ERlnmqmy|!)%qsp z0<(_@-?+pfQ>m^0{{b~X%D>3G#@vERuQ4sXveYonFx?=oH88Kiomu&fE~FE@I?VV+ z8`26PEd!A`83SfQh!K2_SX^=?uE&iQreb)tXsex=l~vuY2~DW>M~pwi&sO9ulBFNh?hq~7y)~m z1lA^PSjf?$6k6Cay#?FA z3{)>v)I2p!DRQbd{#U~(NAkZB*nhM0|G$HE`ZNH|T>!xwdi}lm#rSgvtcT6;1Uv;V zz-#b5(?J7V22aAr@Fjc)NANY(usD{)GJngr6nUaC49jN%SZ^Zx-SbU5@y@K2#H z0928^G&f)>UUK(nLeZg)<8{Fsg6+YV)Os)}bxp!9)E;6Tp~FHcPlpEbv~W3(D>V*j z?h0-Qu06(_W>^3%uo#xWQn&z?!9}nfE`^nFIjn*!&=<=wekEK5tKn+62Cjwc;Ci?L zZiF>(6Wk29z^!l_+>U;{6Yhew;XIzc3x94>a(T`@@BsV?9!A~^_rd+}AUuTWKcn{_ zhDT9H8cIKcnssnDY9Bx?FU55m@W&5r5P*%a3FnN`U8#210gu7s$SpXVTVWgg1-9cn z?!cLSTG4S$SNhZ?>G=%oglFM7cpm-=f5Vlr3toiX@DjZIFXcSjt>LLX@Cv+&Yi%#Q z4sXDl@HV^y@52YMA3nlWckqP#x3I)MEc0Ht)Q4E=6Zis4c3?>=M`hksaz9sE*o$@d z!vPinKQaN1KnP=e_HI&U=vHMO`Rwu8xli#kpN|cg<}>tQ_uM>!K7It}lc#w6w(x8` zie=l-gI)eRxz}C_&kp%}8$%~)7`E1c$7x?dN1$1 z+slWaz^965$a|FA<2_#D0G8oi=4~A7+mEm=d7szi`rdVT-abr!fxE*&^fc#J%1nF( zpLNGS3+H#h*YFMes5}qBx455t4?ke~C)6Cm)QQ^THGjvSf56Xh7-#S(boNSh9+%Sz zL7Zb?j0sGHU{9<^tB8bG3^nd)5lqW;%)pE+Qu#iR=C~YvcaHgl9K%WSw2CI0#jsf1 zFXNe&B{CcCn|9`4$t;DX_R34>&T(Rn%+i=EoM%yLCUnP}=$a zd(YuxcXY95H}kyL@kWQK^Eq>|r(^px{>PtFndXhceszch7keyGfA`gG3iac_*aUsYr0t%W z%WfBbr1LtwdN8nP(O^ior^&KIXS9HvYSl#BCq(;HKCqUSmPW@H7nuvq=-KJhrWPiS z=_p_co@rAPt-A{fmtDSdC$sLHHkG3Jd3Z^+9c#vB9{G2A%g@Wos&`o|G;ir*iY8K& zQ{C=?gBVR#yhavNq&9=vQj}j<>@Y~D2NNeqM*D!Q-2Sm@1G`3Tl7|)!u1q(ppJtD< z`7_e`#i>NC$;hP6$QZq(j_)T+7nf&9HOd!t{}^1zFwsrQy=~L;oz>6oSYOk`jKz%pAhx@BPIh|D+Ar^&a@V3^N3Sh9_aXeWxH@#0t-%!&2mQL%3kWxE_UcR%$ob5JCZMew zyA)`OtLsfLhhK#_w#MivNOu=HjK<^wcY1-tn5Hx7)M|W4&vnK4cPY__VlDQU2-RKv z#R)g!8qh{fh*Jh1$N&7otL`2=^X%sRdrw)Q%OLVi*dR0a5cZ^t(sgb~$uT+#6|30I zs8F5b$g~;IA2)YdcH|pC@dpTzEWX4|_e0`W~?wL`(1LFI`3i+F)_)>kY z-k#@5%S$o{!7HTbyyR#xc$1)acu;TG56pQuw=BGq)s{38)8FWh5wyvz2FLBjqgWLF; z;VU}0O&6(Csqi6QtHsqL-DA&8FdXUV9z={GA=B>Ym!Q`t^yAiNg$`?Cag7gw<>6JN z_2>*RBrnfU&@PPe>V_ENtwxtD8&cbaHQs2i)i7v6zX^G=UOaBs!`CmtW8I5pM;9k} zii@L*t){(7JR0N0{I!TWWmP`c;RzOHv1_Vvj~u8>a7hHMAkMJ4LvVveSWZ^c-RD#>V#J(?lDYl=6jRF*6k?yWjJ zxrR#FkOmnrezP?`-QYI13pHMyH{R_;rQVHi-94MZ?XvevZ)Ya2)?kRXH$~4;%>h1O zJk{}hP`uS)=8j4%Bd zRh!d>7G@64Of=(GCS1o{)8!02Dl}2X?d_4crqz~ysp74s7@fo(qI?>rv_EhEXg=P!wEc!7|erdjtpWV*RYSYZcyV~BV z9m<2Bjd*8p@v-$>wxqc5yUMR+aj_18k4U+cU8%JfR-G}RBB0urd`B{Ija@H^BXFbacM9#vKI=@XtvlKqn zfAO3ysaf;-wZZq587R4W_tm3q0Lh715mkcIndoO{W2^7WL1mSn?eZHeja!t%$p&KPG5XcOC6 zp=7hx;!d)q#p@1W#Fpr?XoEp*yo>LMD{(*9q4hEpwyDhR!kv#N>Gd{9vZ-(>Y&4tW zrFJ%8qjOd~f0?<_XtQT0F7@y_S08I(@53skz#{5Q=HT6|98FWHxM|xAjsmy4z+ph+ zlguXZsR6k`yeiwcsliKhR7@vsbEoBS>*_*ZN)BlV9tTzV`OF zFB`pN@*upTUYwjB&lhmsFCu4SF)vD@v!Zz z6>X1$7=B~DbCb^0q*#e_(A%``3u>z&nYuRGdDqE39g1IQjeJEIwV@DR$Pcp{R1XPm zcobiCwcxgPt>1UCw<7Rv0aJ*sWT@j zN({a!pw{j7*mSw>4LRQ2WW(Fi2gc;AlJrN?vwIdz{?QlAQBs5cDDGDxM`dSry=TMFtAio8EhU$+FbXZ=;TC;_665) zr@#cpvD@GzOd}dWu@H^Ykrjes(m)dMYXN??nrZ-7QS^3)aqlaAj$_*Fqxo*tR{~_*6 zpxij?JnPU|Rl23peM_bLzFK{%yKSjjE%oUuw%hHieLJ?}GmaAy$PFPOAqfevWEj}x z7$COWPJ=_p5DzRf3A+#S!oYiEV9C5}_R%vCm|;lV?ft$gU47Y(!|X;ct)taa)$jK| zf8YNrf9%y~NABJf&rP0Lz4qQsvE1a{FmD5YeF0dP6;xk|vyA8x@KF^!xO0dKq(|9# zp^);zd)-c|CFQ){sHhx={HzyyRjL*N+imFIVrib^f%MF2IdH#Ya+Jn@N`E78_DUgTyz6}D( z*QOK7kSYy_kC_e?49}L33KWOuJ391vaPSwP+3-%m9X}`!XOply$iLAVXM9n7$;mWI zMZKP;(UuzVZOYLe38fu!yehYPW8QN8^gz9`X6n&B?WW97+8Fe@lxqpe{NJjW&g_X> zQ$0bQMob8C0imRRPitF>^}7DL}vR)ULz4_x$ z_pRBmrq|!G`x7J4b;YDoEW~j!DJ6qlBXMWSqYm`1y=84*0PPsMbA3!{w5k<4J8kzH z2#d?Cw1+LOgwN?um3H)%kBtQ7I-^pqcNtAitwe9qD=j|S6=z(&WN8~f#0+dMlvr_4>a?pf^C&5TfU z=$5$T?>_mh+YWx?WEW*kdVL^&+495DXfK~=Wy(bN<#{S+8|e3 zlfYJ9;X3Eow-G&d>p2-^f|KVS186bt27R)9${B+;(HV9zQr>iGrFx0SYdK z1Q%)pZV=l_SKq-;;6H=04Gr*`$T_JVgtkr7h|h<#&tR(yN)@h0|DZ?pGgKPAl19^T zE=Eep&`LTT?G4VLhJyK5E)>7h^@s~AxJF!CUAV$!cVU#kwD317u36@;7Qk81*$4Q46(YJ8se4YiHXSxs2apVu)972jQKLxaqzmz|L3FOlsZ_( zFia*>Tb_Wvl*-g{X6+RL=Urkxe$lYaz)xsH!AL-rdt_bdz^0 zZ}(W;Hcepd{hNQG_N1I`g=`w;th=thBS3_Qks#8AJXPbBaAq%ITR~wS!tO0d5gn1u zI0eFZU66~VpofTN~ zmf4m`tcqyS-=2Kwepbb_2ey$4LLeX%q|i+$Ns)+D67w2Y+%6x!|NQa%zOjVdmG#g6 z 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®>^BYTh_z)suKBOlpSK25m&?<= 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

5wu3*HvrT5?qd9Z4Y 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

hOEz3feF+BYM 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 z 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 z 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

P1kcfgAxhOkW zWmK$;OpJv*UsEw(lVerGel?lH0M_GlBqO( zO(8)IBc|SWay=s4kH0WJ{vz!dUMQNB1MAw3pmBJLv!bg(d*-L*^k&U>{9SuthRuuPH1 zcD~Oif8&<#6y=vDlm>|4@eyThlztZQ{K|DKD?~Ha6?+V9#jft6a$#5wi#=iR$Ia`u zK9Tk(UHR&DjQST-)UuuBPaoaZ-6_3L&gZ0yV&W5~rPrb-%;LqOPo%o#`JPBvrCeD6 z^i^DK?HiLESX$(W=2R1!W3rM{HgTK9suHO-AzP&Ae6Xpi(XB}fC%#XZ@}D*Y zMqWFkoUf4Vc0BnVH5`IMCWfq-FGid%rkNj1s+Rvmt&@^-g^!StLakS`h(&$wzsbdsIZ&ac$$V6XIiF}cW{PW9x5Px39G5UpObd1XV z8=d=7()lTpFI7lqv`A+(y{VMRFSAFeY}d$a*QjjhNNf_pB&Z=IVZkH?VliD}F}(EB z&_qTQIU=m$XcP@8RJNDFB(-8OQ2Jg%`d&49X`w_$WVs~FVmTBICRDaaWHyaplAaKf z_hKj%@&l4F@L zqq67S(!}OylVafc7}`W0IZe`b*87_2w3jWGw zXHBvUpRdw5SS=^q5fnZj+B$4_n>tTHd2MbPzkF}rZW-!-Y?2kl-f;CD+vlkXNpz}x zTqP}ZxqCUq6mS@esuZI&Qgg9x5 z5@PzkZmeUS6niUh5~;>of306{?#LK>tKLB`m$Ds2Gr~r&PT#rogGHNE`UIw%N*lvvRyNpk8D5b-FKoX~2@k z^W6aPhM7+UF!i$AFuWB!nRMP;-)jxDTGG!^kCuqKxC#uMR#LdQzDQ#W^1KUTuslc` zPZe!@R`kW_M~U$K4PP(&Uz(5X0h(9YRTuUXk?+;^*7D3+Q!OcmD&{QwtaaHf*o~at znu$_QMx_j7Ka!7QX{1aCG09g!Oc`)PEnZH3(MDJ_^=u=~rok+zjg@a! z)%I(Ts;N}L#l9-A+FGd!)U)gFp(FQwlyT`$c;@6lGLy9wJT~&fBeiZ*v6wSEBUa!^ zpKVkAyU*QBW%}aZ`wXjGZaJ8wm!<|W+p2IsD=o^7+PejyWPd7&bdc3HY;&GHo`wC> ztI1+17G?P`v=25Fm$K5&n3!drXUNHLO1&^_IxNH3y*CI+DX|z`uWdBDz3gYL5B{7k zb+HxLF-pTs91xLGKPlt-%G0%A;m*A&VzSUlcQE_7!_^U6-t;82vDKPK^qy|uWJR9a za)5L9XT}@)Y-amlo6pbRF{cy+*thiB2iEJ+HW)a+TBtW$GttcY`SUog3Kz$T&r!`p z+Q|E5h0zLo>>G^@%}6s)N1(|>&3og4HB-SdTTSe2<dip!AEJg zL3O7iQrTE2=*>uP`p-E0yGEXM55bj-4i_HL_yy0}uH4~T(v@88(G%|6Es-KA%iQmM zs80twWYfVWQ`_c!V!A1vatSYp`J1>eqpece+P4oJOl$pmN<*ox{Yt#DOER`+3T`P= zx2p{Vpl-Am>YO;vT6(Q6_t|%Q6ZI2sd-MMIlr$_%#rAx%`Z$oxW)oj7KhMV#mo+FY zpQBr{tQ?K~Eg66_nYcILarmcw;WA9#>xcNzul_sp2z5`l{+9!((uRY0Ol>F6t*Jx= z-7Mc3*X+C7N0IsZ`USlGkfk8b8S=jM-K6Gj(gN2EM~l6Kon2j4k-3yYt7LbAhFi3p zf{PL@WuF9%eNg$!&ri9ucD*ML%KIV|sxG*)_j8z8211@CU*CV0$xu4vxRP56+E+C4 zyqVX0LoIM>T^6ly71gMDuQ9@VMv~Q#=8R8QPYGsN%f~<3tgWmU9-j2x{$l_kF}jL_ZIJ4pBCjUtBYx2-&`dJ z#nhd5C{RK_bXEsljLTQFo++u&1%k@628(}eMwsIt;^7f&RaU7uYr^7m)XeFwl2&sO zw}~rDO$}H+@}_9C==_IClc$Ft-5nPsHa6hAZq|H0epjePXDuvc0T+Mf((^|r*!E_T ziZE5X{?>9Vpm(GpFWh@f>B7NM`{0Lb>`>3(LX(6s=T)z*s_H~L zPv4?mel(dfU-tRJ!V9`QuQki9b%DjDrv=(o8=_55_<2v2_7WT0FmSU;e7nB>${Ok# z@0$)tMtw)#Mj3YPjbCHzK(l#vd@Ir*g9B=FEKVLIvpP}Th$l4`-1buk+6Fk*-Zv<} zec?qG?!G^5uj{?vO2?{MBp|V?Zl5C_8;p;G{?lXHPw;HBk)!cO2i@eFxPkM9$o=+F z#>cz)4a4~+iG_nc12RY=O4?-Ma58JXYm1fFT~m3<8>7+&p`l))!Ucy!l zH#6zynnUkJC7X7~cp(>y7vB?T2t@Ga172O`jNH9DpU+1_%a$mN z(z7J^kW20V$KtEf%n9j-Tyux0qjBiutFt-S{#WgI%Uw6pLp;-Aq}E(h(z?^~In+Ma za_al&ffM)hHHW+NIJ1dG@s}(ukz9*)&$QYg+(O=Fqw@mFQ3p%ObpczAr?IU5cBiF< zhk>AFsDV_~!e#N#aWqevC)JRTuf7QT9q%{1o;rg{W3)VrNs;^r^i%y+r7y%-IJ6C& zO1ZS0lV0;kT&QSbqnhl+cAqM=$&yxBo7fma+gm)brWJ0TduHIf``-EZxBc`?V0ZZ~ z73q`=*xUE(Xs$uFCC^Q_Jn_QYEg#{Qat_O+TM~2oGzv`udoxSS->%mjZ4$^hyLN7s zrK2~dkBcM%!ofQy2Uv!Sl4CH8=F|(|(1>xl)LJQ;b)wq2EQnz88H_ zUyY?Pds9WGh#uhOVDCsz%bS)ApPw5c1eMdd_&dH{hV6duJ@y%U@b)w!G1_MGOpK>o z0~HSAKB=W7y9GF+V8LpcnOtZVQ z+0>Nb*&O8#d$vJoL+|_P_QU4U!}_tG&8I06{$X_=9iz{3Y5D!6h1y=y-L+TTOjLgO zm14a-ID~4YhbBbT(0qe27F)T{T(t-WTIAdfX4m6cqcsTDX>*a7+=gU|^T}Q-;^|FI zydALHlrW%B~RLJm(6!KRL*foh#)&hzWh$j#HCuQ4bliF`n}$-5{`R&$Hgl@hNR{ zig%XZ;HcLio5fYsv$~x7^`bK_-Hi;)@a?DW7@O zAX#JP>K(zRBc3xiS1P}ayEO8dujOS`_UH4<>K&W%3ig#T#WV*m>)J1mhHVSTEein$ zx!bq>F~vB!GgZ5yV^HoDkMpc0)CpUTrVl|APTNT)cUxcf zFf;hZu!J{V8%S!S+l)sQ`)nh|Du4cK^ zSuVp)q}e$v&pF<7{NQ`@rIjF=BW<4(LE6QWTzimJxnxlk#vpBu-j9VJJonwgs4LI# zmW`HO=2%3vf%cagTfVC(OHOB0UYUk9P^r2vagica0jpD5Y3ON`r@cp|xgg<%!huav zI`)@u_S+KzpZ8w&YOa8NWI0hsH~kNM)enB;JHBi5O0K@E6A3gPR^DsLMP8`q5pKM2 zw`l~o=+Xt%QZEEh0sR_Np;zXjw!Jg8{A8peAQQfeGtz^E4}K_*|9W}kv<*^u{E2az z{pj=ioAj3M)$n0OhtlHsxS}kV{nXc)fFBi>igCAEEDXv6XXm*$Z*zP54p4O+LGnkx z_)Zt=FdSKihj2DLsW?3StBo2pJ-Z5JO-IOG_scVm!UmRB1Z$E7oS{qU0eO7FO5I*NoPzBlV^7*=Nvq7Vd(0d zDyzS;?%1UmM@uVCoh=zWsl6Z z?OY7%-BD*iwYc7WPO0m($9dPbwESyT!mqE)C?ZLm5%;Z0fW#_bEaC} zXH6CFz+P*zl#zL!{PMgbzAV{w9PP8s$0zS}JN5%Vd79e|F|H8za5jAW5$<^hiah_b z|FOQK#>HD z9Z%?TRY!+K?~|rt)Ql00t4QY`!vtJI&wp?p&(KINmTi|r94HJ=7zf$Z&j?l)f3O`r z5v-v5&7EQ9^ZdFKdt<;LDZAyiF|a;naD?t?Iq=y_U0?d<>F-_6dw&o1el(G6b5omL zE-VhrKjoy(;s5Y#8$bQ~Z?(gP*Ve<%t$aHkzB^e?C24i##qYa$m|rogJ~naCbMJcZ z6Zh>kGDDLuWVGdc>Xju%l!&jSs(tl)*TLiocFvW~0gK0#PWR1sr(>KVXB6Gf8PYnDF8VKL$AGa_={|&<*!{<*uvNs z`J{7vqMXp;PNS?7(VJJ+7qHjZA3tpa9D~{hSjj9m_2ZmRy*TXcWL!0r6!csjBIisf zNqyepa`~;g^z3Z)+Y7rga?9!VJsDoQ5?Zv9>tiH-l`}G-tIU4~*?84~B`9+9S(SHP zgimPCBd|%=Gh4m;=kVUi?UIq;(Gr<3>>UPa*#UkU8%?BAEhDPY!*MU36dwwL_U_&+ zJpCK04EkcQ^{ZIW0-Hx==aapqDL~?O_Y+aVGWot+z*+2}HmF+LDWUCxC+^cxd(g{V zw)s`_0vDd!JmGkrhMc?lx$jno6{Vf0`^wuZUR1mt0?u#0L)ShSUH@!$6`1|ufU!{&Sf<(kVn-kSQ^KSzx_HJ9W z+8w)h!*={<{d?Zkx-VjnmIRnvYF^9tq|wNH z15OlENnVV$C6vkEa(SX788~i}Sx^vyR+DFEWkVa0#E8evth!YD7*jY^jgt9n|9)a# zse;R93(Q)%34Gz~oF?ElSfy=xKOc8iy<|dhaom|7_b|FQQN9N)TzVG4B))+tSIx&E3_~%<(^8onPAFVFRH6dS3ef zd=(Pn(eiPz<;^ZmtKQM-pLB~e9Hg+ zE@dbL0m?}6i}M3P($WB4Nj|VRKc4`Z$GRv`-%6XJFXfGQdqB31(_K% zdsXv(~}m~Y!c-=a8Ts+msl%s#@xS2@<=wZMvhB5{PV zHpbIMQDAD7!DBPhMU#uA<9qp3PZtM*7CQP^Xe}b2KUn<|!1@=L^o{3ualX#L-Y&HX zn%j*_SAm9$YdGaEqHN84WfxnL82boSv+kc71)_!&$B)rrFHGuw8SZjL9?L4qz@}_N zK(gFU+|Gn$vKP?>MT=AYM7YAn*#-i61KYjvyQx7Ev@jdd10#u2>a{5Km0&?2p9|k)Bp3U|Na2@>HqVM-syia01ym^ z0r}uDFp%$|9RS1!cRc_Y0D-gP2g74fUI92eCc zfKa%8`S>8vhu7odhZ`#}1PsR)0_FugYzu)6`a`}Det-a+9X|*J+t2>_{O^1~_(2eO z3=DiYE)agO0DK=%xUqup3&8ga1;ULL1PTJf`3D6-;W026&Og|+J&YF!6bgAT9{`{L zFYg087zP4A;06HkLP5O$9v2uU0DZt000IE`9{2}<0KgAp0|@K&;rPKYDENUdKwbbp z--AA27!>*-{$QBE!`J}w0(l?$2g3l62RvXH-@~{Ef&d`kgO~(@!Qh8+0K=dUV;2bi zXQ2N*RzNU6FXX{mg<&ATLkzY8AI2_Be*YuS|2HOKn+o5xnSsDN<(fk7~*|2O4Lz=ODlVNl+OzQC5{Kk3c?9X}911PFY<k7y$-B zfbk>1U=J90r~&)}@EDXA0R}*TA)`V0$rqzX*81_F{NDcpdD3J_J1Y5b)qbz=IC~4_FQi-!B3lu$&Oy4gn87 z1U$e9c);>2xa+}kBzOz~4=@5AusjF8Edm~31Uz6lA$(f|JYabaoEZdJaIK=Kv@I9tiXt0L!1@`$fP5mfOPFA<%PJ@{Pa_0S^Rv z4nUyi00eptK%nOU0R%h{=s7?Do(Jr>0p~O97=ZwT=K(taBd~*yDF-v2=pAt z3(o^~ctOw>0S^Rv4n&~mKmYe^P&w(HW zJP_zP5QJbp5a>DVPz>)g0zC&J&~qRHJqIGt^M7huaAOZU@55sV<^xt|fVV?19|-gu zh(OPQ2=p8XMi5sB^c+?OfcFJfZ-Vm=ft~{q=s6IKfCmCS2O`jOAgo^ZuwU58_TLzY zUjPWpC;vI}{%@TNhCyK3_%Do401P{gKg1v)7>@^b5a`320<828cvzEx@Pc7Qo`1K6 zVSF&1{@3;RA+VGD1ONCTe1L~F3K+)s&}SG1hMmD4T#pY{2zh|tN}MhYh>S^56GXfd8N0Q2DQ0 zCT6bg|5cI}0Q2!epm^BK%+jhdc-a3_r{=}O{{O{09yM1>Px}9I>OVJ~G~FFxKmIdr NaE`<3jsN5L{{uVD7DWI6 literal 0 HcmV?d00001 diff --git a/ajax_light_control.php b/ajax_light_control.php index 765c870..7463fa4 100644 --- a/ajax_light_control.php +++ b/ajax_light_control.php @@ -12,24 +12,60 @@ if (isset($_GET['btnID'])) $btnID = clean($_GET['btnID']); - + /* exists an device extension file? + --------------------------------------------------------------------------- */ + $extensionPath = "fuPlugins/device_extensions/".$getID."_action.php"; + $callExtension = 0; + if (file_exists($extensionPath)) { + $callExtension=1; + include "".$extensionPath; + } /* Connect and send to telldus live --------------------------------------------------------------------------- */ require_once 'HTTP/OAuth/Consumer.php'; - $consumer = new HTTP_OAuth_Consumer(constant('PUBLIC_KEY'), constant('PRIVATE_KEY'), constant('TOKEN'), constant('TOKEN_SECRET')); if ($state == "on") { $params = array('id'=> $getID); + + // call preOnAction + if ($callExtension == 1) { + if (preOnAction($getID) == 1) { + exit(); + } + } + $response = $consumer->sendRequest(constant('REQUEST_URI').'/device/turnOn', $params, 'GET'); + + // call postOnAction + if ($callExtension == 1) { + if (postOnAction($getID) == 1) { + exit(); + } + } //echo "check"; } if ($state == "off") { $params = array('id'=> $getID); + + // call preOffAction + if ($callExtension == 1) { + if (preOffAction($getID) == 1) { + exit(); + } + } $response = $consumer->sendRequest(constant('REQUEST_URI').'/device/turnOff', $params, 'GET'); + + // call postOffAction + if ($callExtension == 1) { + if (postOffAction($getID) == 1) { + exit(); + } + } + //echo "check"; } diff --git a/cron_device_log.php b/cron_device_log.php new file mode 100644 index 0000000..e23a667 --- /dev/null +++ b/cron_device_log.php @@ -0,0 +1,99 @@ +connect_errno) { + die('Connect Error: ' . $mysqli->connect_errno); + } + + // Set DB charset + mysqli_set_charset($mysqli, "utf8"); + + /* Get oAuth class + --------------------------------------------------------------------------- */ + require_once 'HTTP/OAuth/Consumer.php'; + + + + /* ##################################################################################################################### */ + /* ######################################## SCRIPT RUNS BELOW THIS LINE ################################################ */ + /* ##################################################################################################################### */ + + + /* Find users + --------------------------------------------------------------------------- */ + $query = "SELECT * FROM ".$db_prefix."users"; + $result = $mysqli->query($query); + + while ($row = $result->fetch_array()) { + + + /* Connect to telldus + --------------------------------------------------------------------------- */ + $query2 = "SELECT * FROM ".$db_prefix."users_telldus_config WHERE user_id='{$row['user_id']}'"; + $result2 = $mysqli->query($query2); + $telldusConf = $result2->fetch_array(); + + // Define variables for oAuth + define('URL', 'http://api.telldus.com'); //https should be used in production! + define('REQUEST_TOKEN', constant('URL').'/oauth/requestToken'); + define('AUTHORIZE_TOKEN', constant('URL').'/oauth/authorize'); + define('ACCESS_TOKEN', constant('URL').'/oauth/accessToken'); + define('REQUEST_URI', constant('URL').'/xml'); + + define('BASE_URL', 'http://'.$_SERVER["SERVER_NAME"].dirname($_SERVER['REQUEST_URI'])); + + + /* Get devices for user + --------------------------------------------------------------------------- */ + $consumer = new HTTP_OAuth_Consumer($telldusConf['public_key'], $telldusConf['private_key'], $telldusConf['token'], $telldusConf['token_secret']); + $params = array('supportedMethods'=> 1023); + $response = $consumer->sendRequest(constant('REQUEST_URI').'/devices/list', $params, 'GET'); + + $xmlString = $response->getBody(); + $xmldata = new SimpleXMLElement($xmlString); + + $lastUpdated = time(); + + foreach($xmldata->device as $deviceData) { + $deviceID = trim($deviceData['id']); + $name = trim($deviceData['name']); + $state = trim($deviceData['state']); + $statevalue = trim($deviceData['statevalue']); + $methods = trim($deviceData['methods']); + $type = trim($deviceData['type']); + $client = trim($deviceData['client']); + $clientName = trim($deviceData['clientName']); + $online = trim($deviceData['online']); + $editable = trim($deviceData['editable']); + + $queryDevice = "select status from ".$db_prefix."devices_log where device_id = '".$deviceID."' order by time_updated desc limit 1"; + $result3 = $mysqli->query($queryDevice); + $row = mysqli_fetch_row($result3); + $oldstatus = $row[0]; + + // only update id value changed + if ($result3->num_rows<=0 or $oldstatus != $state) { + // Add values to DB + $queryInsert = "REPLACE INTO ".$db_prefix."devices_log SET + device_id='". $deviceID ."', + time_updated='". $lastUpdated ."', + status='". $state ."'"; + $resultInsert = $mysqli->query($queryInsert); + } + } + + } //end-while-users + +?> \ No newline at end of file diff --git a/cron_schedule.php b/cron_schedule.php index 6676005..2b9aab4 100644 --- a/cron_schedule.php +++ b/cron_schedule.php @@ -1,8 +1,11 @@ connect_errno) { die('Connect Error: ' . $mysqli->connect_errno); @@ -22,30 +22,15 @@ // Set DB charset mysqli_set_charset($mysqli, "utf8"); - - - /* Get oAuth class --------------------------------------------------------------------------- */ require_once 'HTTP/OAuth/Consumer.php'; - - - - - - - - - /* ##################################################################################################################### */ /* ######################################## SCRIPT RUNS BELOW THIS LINE ################################################ */ /* ##################################################################################################################### */ - - - /* Find users --------------------------------------------------------------------------- */ $query = "SELECT * FROM ".$db_prefix."users"; @@ -53,9 +38,6 @@ while ($row = $result->fetch_array()) { - - - /* Connect to telldus --------------------------------------------------------------------------- */ $query2 = "SELECT * FROM ".$db_prefix."users_telldus_config WHERE user_id='{$row['user_id']}'"; @@ -71,8 +53,6 @@ define('BASE_URL', 'http://'.$_SERVER["SERVER_NAME"].dirname($_SERVER['REQUEST_URI'])); - - /* Get sensors for user that should me monitored --------------------------------------------------------------------------- */ $query2 = "SELECT * FROM ".$db_prefix."sensors WHERE user_id='{$row['user_id']}' AND monitoring='1'"; @@ -103,10 +83,6 @@ temp_value='". $tempValue ."', humidity_value='". $humidityValue ."'"; $resultInsert = $mysqli->query($queryInsert); - - - - } //end-sensorlist } //end-while-users diff --git a/cron_virtual_sensor.php b/cron_virtual_sensor.php new file mode 100644 index 0000000..9eedadc --- /dev/null +++ b/cron_virtual_sensor.php @@ -0,0 +1,111 @@ +connect_errno) { + die('Connect Error: ' . $mysqli->connect_errno); + } + + // Set DB charset + mysqli_set_charset($mysqli, "utf8"); + + /* Get oAuth class + --------------------------------------------------------------------------- */ + require_once 'HTTP/OAuth/Consumer.php'; + + + /* ##################################################################################################################### */ + /* ######################################## SCRIPT RUNS BELOW THIS LINE ################################################ */ + /* ##################################################################################################################### */ + + + /* Find users + --------------------------------------------------------------------------- */ + $query = "SELECT * FROM ".$db_prefix."users"; + $result = $mysqli->query($query); + + while ($row = $result->fetch_array()) { + $query2 = "SELECT * FROM ".$db_prefix."virtual_sensors WHERE user_id='".$row['user_id']."' and monitoring=1"; + $result2 = $mysqli->query($query2); + + while ($row2 = $result2->fetch_array()) { + $lastUpdated = time(); + + $returnValues = getCurrentVirtualSensorState($row2['id']); + + // get all return types + $queryReturnType = "select vstc.value_key from futelldus_virtual_sensors_types_config vstc, futelldus_virtual_sensors vs where vs.id=".$row2['id']." and vstc.type_int = vs.sensor_type and vstc.value_type='return'"; + $resultReturnType = $mysqli->query($queryReturnType); + + //print_r(array_keys($returnValues)); + + $valueChanged = 0; + $valueChangedTo = ""; + $logArray = array(); + + // insert every log value + foreach (array_keys($returnValues) as $key) { + $value = $returnValues[$key]; + + // check if the status changed + $queryOldVal = "select slv.value from ".$db_prefix."virtual_sensors_log sl, ".$db_prefix."virtual_sensors_log_values slv + where sl.sensor_id='".$row2['id']."' + and sl.id = slv.log_id + and slv.value_key='".$key."' + order by sl.time_updated desc LIMIT 1"; + $resultOldVal = $mysqli->query($queryOldVal); + $oldVal = $resultOldVal->fetch_assoc(); + if (!isset($oldVal['value']) or $oldVal['value'] != $value) { + $valueChanged = 1; + $valueChangedTo=$valueChangedTo."".$key."=".$value."; "; + //echo "value changed for ".$key." from ".$oldVal['value']." to ".$value."\n"; + } + + // generate the sql and store to array + $queryInsertValue = "REPLACE INTO ".$db_prefix."virtual_sensors_log_values SET + log_id='%%LOGID%%', + value_key='". $key ."', + value='".$value."'"; + array_push($logArray, $queryInsertValue); + } + + if ($valueChanged == 1) { + // update timestamp + $update1 = "update ".$db_prefix."virtual_sensors set last_update='".$lastUpdated."'WHERE id='".$row2['id']."'"; + $resultUpdate1 = $mysqli->query($update1); + + // execute the sqls + // insert the log + $queryInsert = "REPLACE INTO ".$db_prefix."virtual_sensors_log SET + sensor_id='". $row2['id'] ."', + time_updated='".$lastUpdated."'"; + $mysqli->query($queryInsert); + $log_id = $mysqli->insert_id; + + foreach($logArray as $sqlInsert) { + $sqlInsert = str_replace("%%LOGID%%", $log_id, $sqlInsert); + $mysqli->query($sqlInsert); + } + + + $pushover_key = $row['pushover_key']; + $subject = "Status changed"; + $message = "The status of jens@home changed to ".$valueChangedTo; + + // sendNotification($pushover_key, $subject, $message); + } else { + //echo "no values changed\n"; + } + } + } //end-while-users +?> diff --git a/dev/test_device_available.php b/dev/test_device_available.php new file mode 100644 index 0000000..d94cecd --- /dev/null +++ b/dev/test_device_available.php @@ -0,0 +1,46 @@ + '58:1F:AA:B0:31:2A', + 'host' => '192.168.1.1' + ); + $data = getVirtualSensorVal($paras); + echo $data['available']."\n"; + + function getVirtualSensorVal($parameter) { + $device = $parameter['device']; //"58:1F:AA:B0:31:2A"; + $host = $parameter['host']; //"192.168.1.1"; + $community = "public"; + $snmpObj = "1.3.6.1.2.1.3.1.1.2"; + $snmpReturnVals = snmp2_walk($host, $community, $snmpObj); + + $contains = containsDevice($device, $snmpReturnVals); + return array('available' => $contains); + } + + function containsDevice($device, $snmpReturnVals) { + foreach ($snmpReturnVals as $returnVal) { + $hex = getHexCode($returnVal); +// echo $hex."\n"; + if (strcasecmp($device, $hex)==0) { + return 1; + } + } + return 0; + } + + function getHexCode($snmpReutnVal) { + if (startsWith($snmpReutnVal, 'Hex-STRING: ') == TRUE){ + $hex = str_ireplace('Hex-STRING: ', '', $snmpReutnVal); + $hex = trim($hex); + $hex = str_ireplace(' ', ':', $hex); + return $hex; + } else { + return null; + } + } + + function startsWith($haystack, $needle) { + return !strncmp($haystack, $needle, strlen($needle)); + } +?> \ No newline at end of file diff --git a/dev/test_nmap.php b/dev/test_nmap.php new file mode 100644 index 0000000..8073de8 --- /dev/null +++ b/dev/test_nmap.php @@ -0,0 +1,34 @@ +&1"); + +if (strlen(stristr($output,$mac))>0) { + preg_match("/\(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\)/", $output, $matches); + $deviceIP = $matches[0]; + + // remove first and last character because we added them to our regExp + $deviceIP = substr($deviceIP, 1 , strlen($deviceIP)-2); +echo $deviceIP; + // contact the iphone on port 62078 to keep wifi on + //echo shell_exec("nmap -P0 -sT -p62078 ".$deviceIP." 2>&1"); + +} else { + echo "no device found\n"; +} + + + +?> diff --git a/dev/test_ssh2.php b/dev/test_ssh2.php new file mode 100644 index 0000000..1aed619 --- /dev/null +++ b/dev/test_ssh2.php @@ -0,0 +1,55 @@ + diff --git a/dev/wartung_delete_double_devices_log.php b/dev/wartung_delete_double_devices_log.php new file mode 100644 index 0000000..ba3d267 --- /dev/null +++ b/dev/wartung_delete_double_devices_log.php @@ -0,0 +1,61 @@ +connect_errno) { + die('Connect Error: ' . $mysqli->connect_errno); + } + + // Set DB charset + mysqli_set_charset($mysqli, "utf8"); + + /* Get oAuth class + --------------------------------------------------------------------------- */ + require_once 'HTTP/OAuth/Consumer.php'; + + + + /* ##################################################################################################################### */ + /* ######################################## SCRIPT RUNS BELOW THIS LINE ################################################ */ + /* ##################################################################################################################### */ + + + $query = "SELECT * FROM ".$db_prefix."devices"; + $result = $mysqli->query($query); + $countDelete=0; + + while ($row = $result->fetch_array()) { + $deviceID = $row['device_id']; + + $query2 = "SELECT * FROM ".$db_prefix."devices_log where device_id = '".$deviceID."' order by time_updated asc"; + $result2 = $mysqli->query($query2); + + $first = 1; + $oldState = -1; + + while ($row2 = $result2->fetch_array()) { + if ($first==0) { + if ($oldState == $row2['status']){ + $countDelete = $countDelete + 1; + $query3 = "DELETE FROM ".$db_prefix."devices_log where device_id = '".$deviceID."' and time_updated='".$row2['time_updated']."'"; + $result3 = $mysqli->query($query3); + } + $oldState = $row2['status']; + } else { + $oldState = $row2['status']; + $first=0; + } + } + + + } + echo "Would delete ".$countDelete. " "; +?> \ No newline at end of file diff --git a/fuPlugins/virtutal_devices/device_as_sensor/index.php b/fuPlugins/virtutal_devices/device_as_sensor/index.php new file mode 100644 index 0000000..ecd58e2 --- /dev/null +++ b/fuPlugins/virtutal_devices/device_as_sensor/index.php @@ -0,0 +1,86 @@ + '123456', // telldus device ID + 'onDesc' => '', //return + 'offDesc' => '', //return + ); + status=1 --> device on, status=2 --> device off +*/ + + function activateHook() { + // return an array with the description, and all fields needed for determining to correct sensor state + return getConfigArray(); + } + + function disableHook() { + // nothing todo for this plugin + } + + function updateHook() { + return getConfigArray(); + } + + function getConfigArray() { + return $configs = array( + 'device_id' => array('Telldus device id' => 'text'), + 'on_desc' => array('Description when on' => 'text'), + 'off_desc' => array('Description when off' => 'text'), + 'status' => array('Status' => 'return') + ); + } + + function getDashBoardWidget($lastLogValues, $virtualSensorID) { + $myPath = getPluginPathToVSensorId($virtualSensorID); + + $widget = ""; + $widget.="

"; + + return $widget; + } + + function getVirtualSensorVal($parameter, $virtualSensorID) { + $device = $parameter['device_id']; + + $actState = trim(getDeviceState($device)); + $returnValArr = array( + 'status'=>$actState + ); + return $returnValArr; + } + +?> diff --git a/fuPlugins/virtutal_devices/device_as_sensor/light_off.png b/fuPlugins/virtutal_devices/device_as_sensor/light_off.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c1033d59df44d0d13f2b0c9926c1d84e033f8a GIT binary patch literal 505 zcmVVGd000McNliru-31O5A1*-XRqp@*03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00CV|L_t(Y$JLj;Y63wN zg}<{3k`y+PSSloquaGtmAovEplywThQhWsktALeVe1VWOfv8QqDKdhXtj^DBaNrg* z%$#q}?wvb3GBf_8m17o3ssi_}`(kDv!6Ihn6+j8-0?)t(Fc%v))&)x5dY;faFxi3S z#RRAq0tTwU+ylCeIZ!PE+zWtiqn88*YQQ=IyasBAw6i0*_Xt!X62Ics=C)(tM#5HY zq<~im;8hBEmH?hb7ZCSgi-_b}09opG>O_D$MOChT0JL9Z$!zV_=e*bet-`<#KJoyM zeD$(`EPX0y&d<^6`y(Lf8h8V)4(!{SnJs5OhmzW7aV+T$cyQgRnJrEZmUIi;>=cp}@NQvLRT%I-i&(usN10`$A0h6#NG`{BX({Cm=J00000NkvXXu0mjfSNP4b literal 0 HcmV?d00001 diff --git a/fuPlugins/virtutal_devices/device_as_sensor/light_on.png b/fuPlugins/virtutal_devices/device_as_sensor/light_on.png new file mode 100644 index 0000000000000000000000000000000000000000..b8b1b7ba9c1f99507ad18b769d75569cf4ba3595 GIT binary patch literal 623 zcmV-#0+9WQP)VGd000McNliru-31p22Ng@zW|IH_03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00GlUL_t(Y$JLiTOjJP> zg}=LOqb3xysIggo)WWUkh{nQ#LMjp(V`5`LMMG^tLPsZ!p`;{KB-=n?X(S|D7z!x- z%f5yv@DfNCEQCYz3Zl64vkQhvrkI&~&&fG6@6H>O#Q$jJlq56j1s)W`FOm+UK_mrr zpl7|#BG6FgbiM&qAy=v%A%MtTi{>aJ#tvk^ED(!*fYLyqG@X!q#ie9r*RqZ)S7G@hWo|3b=9;8!UtB4u8cEx83%U0O znp?;ZLr8;(0b0kr=|)VH)rH+RmARJ3=3A1Ey80s^>FCvjHK$(Cf6@B8WtyR+QHs0W zN>PQuY{1O)k3JBEbuAPZs+TTMyA+LcofMCo*~&S!D`qx+C5p}LKJcU%ew6g}(uvLN z4p0GZ6jR5*zNDX*{gP+_Cx6WSx{4=E8BnJVGd000McNliru-31O59|i1Gtgiq703B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00N^)L_t(Y$HkUUNL4`; z#((GeEJ;%lF(o5|!j-fsBq%VF7Ewl*A%&c1WQ(zx(IdR9pE5LzEvJU_S;H2yU zpgU!k-N1m}w`ks=>^<^34>YM)1D)Guf;1#uT8+5|ih z?@my$i-FH5dW$iib8+GK$^i-uz)SVbz-^#Ybuk4D0n@-$cmFJ2r+Cqv33s0|vnpUy z@OiB&u6tVLS=sGc%u((93ec63-!6?C&;YCeCp5N8e%}DOqT&)^hr)Y5z)r>N8ud-U zaiA$-5&gi!nEj`)RtbxAS`}#lb`EHW@%n&UKwpB73()~2`!VpSh?5ewyFgD2tBKje z%GYoVuL@5npdNS*)RiM_z>zh+S5v;0)Y}@|{qbm}3YT#fcL2-E+cMBmsgK-W-gZ5@ zJfcgaXI<{&GFop5yY%&0=^u!h<$({t)&TyZaIa*!<1U~rhP4ULG}H$Z=H-NED%@Kk z{_i<9vpSt>jS0guz&3ZE&tk`Wbpa%`290MD>O;ZL!m5C1ysVb5PE(A z*)_($W+gle)Rq`ue_?lDGPCKF+9s3abjW6aB~0v002ov JPDHLkV1giOb~FG0 literal 0 HcmV?d00001 diff --git a/fuPlugins/virtutal_devices/network_scan/index.php b/fuPlugins/virtutal_devices/network_scan/index.php new file mode 100644 index 0000000..ff50f39 --- /dev/null +++ b/fuPlugins/virtutal_devices/network_scan/index.php @@ -0,0 +1,231 @@ + '58:1F:AA:B0:31:2A', + 'host' => '192.168.1.1', + 'timeout' => '10' + ); +*/ + + function activateHook() { + // return an array with the description, and all fields needed for determining to correct sensor state + return getConfigArray(); + } + + function disableHook() { + // nothing todo for this plugin + } + + function updateHook() { + return getConfigArray(); + } + + function getConfigArray() { + return $configs = array( + 'mac_client' => array('Mac Address' => 'text'), + 'snmp_host' => array('Router IP' => 'text'), + 'timeout' => array('Timeout in minutes' => 'text'), + 'addSNMPCheck' => array('Perform SNMP check' => 'boolean'), + 'wakeupWithLastIP' => array('Send wakeup signal' => 'boolean'), + 'available' => array('Available' => 'return'), + 'ip' => array('IP address' => 'return') + ); + } + + function getDashBoardWidget($lastLogValues, $virtualSensorID) { + $myPath = getPluginPathToVSensorId($virtualSensorID); + + $widget = ""; + $widget.="
"; + + $widget.= "
"; + $widget.= getVirtualSensorDescriptionToSensorId($virtualSensorID); + $widget.= "
"; + + $widget.= "
"; + $widget.= getVirtualSensorTypeDescriptionToSensorId($virtualSensorID); + $widget.= "
"; + + $widget.= "
"; + $widget.= "icon"; + if (isset($lastLogValues['available']) and $lastLogValues['available']==1) { + $widget.= " yes ".$lastLogValues['ip'].""; + } else { + $widget.= " no "; + } + $widget.= "
"; + + $widget.= "
"; + $timeUpdatedByInsertedLog = getLastVirtualSensorLogTimestamp($virtualSensorID); + if ($timeUpdatedByInsertedLog==0){ + $timeUpdatedByInsertedLog = time(); + } + + $lastLogsTimestamp = getVirtualSensorTmpVal($virtualSensorID, "lastOfflineState"); + $timeStyle = ""; + if ($lastLogsTimestamp>0){ + $timeStyle = "style='color:#580000;'"; + } + + $widget.= "".date("d-m-Y H:i", $timeUpdatedByInsertedLog).""; + $widget.= "
"; + $widget.= "
"; + + return $widget; + } + + function getVirtualSensorVal($parameter, $virtualSensorID) { + $device = $parameter['mac_client']; //"58:1F:AA:B0:31:2A"; + $host = $parameter['snmp_host']; //"192.168.1.1"; + $timeout = $parameter['timeout']; //"timeout in minutes"; + $addSNMPCheck = $parameter['addSNMPCheck']; //"check via SNMP?"; + $wakeupWithLastIP = $parameter['wakeupWithLastIP']; //"send NMAP package on port 62078 before testing? --> wakeup iPhone's wifi"; + + if ($wakeupWithLastIP=='true') { + tryWakeUpWithOldIP($virtualSensorID); + } + + $snmpResult = 0; + if ($addSNMPCheck=='true') { + $snmpResult = trySNMPCheck($parameter, $virtualSensorID); + } + + $ip = getDeviceIP($host, $device); + $available=0; + if (isset($ip) and strlen(trim($ip))>0) { + $available=1; + $returnValArr = array( + 'available'=>$available, + 'ip'=>$ip + ); + } else { + // if snmp is enabled, we might get true back, but the IP is blank. But this + // don't effect the availability ... maybe we have no IP for minute or so. + $available=$snmpResult; + if($available==1) { + $returnValArr = getLastVirtualSensorLog($virtualSensorID); + } else { + $returnValArr = array( + 'available'=>$available, + 'ip'=>$ip + ); + } + } + + $tmpValKey="lastOfflineState"; + if ($available == 0) { + // handle timeout only when go offline + $returnValArr = checkTimeOut($virtualSensorID, $returnValArr, $timeout, $tmpValKey); + } else { + deleteVirtualSensorTmpVal($virtualSensorID, $tmpValKey); + } + return $returnValArr; + } + + function checkTimeOut($virtualSensorID, $currentState, $timeout, $tmpValKey) { + $lastLogsTimestamp = getVirtualSensorTmpVal($virtualSensorID, $tmpValKey); + $actualTimeStamp = time(); + if (!isset($lastLogsTimestamp)) { + $lastLogsTimestamp = $actualTimeStamp; + storeVirtualSensorTmpVal($virtualSensorID, $tmpValKey, $actualTimeStamp); + } + + $timeoutMilliseconds = 60*$timeout; + $differenceMillis = time() - $lastLogsTimestamp; + + //echo "diff = ".$differenceMillis."; act: ".$actualTimeStamp." last: ".$lastLogsTimestamp."\n"; + //echo "timeout: ".$timeoutMilliseconds.", currentDiff: ".$differenceMillis.".\n"; + + if ($differenceMillis > $timeoutMilliseconds){ + //echo "returning new state\n"; + deleteVirtualSensorTmpVal($virtualSensorID, $tmpValKey); + return $currentState; + } + //echo "returning old state\n"; + + return $lastLogs = getLastVirtualSensorLog($virtualSensorID); + } + + + function getDeviceIP($host, $device) { + $shellCommand = "nmap -sP ".$host."/24 | grep -B2 -i ".$device." 2>&1"; + $output = shell_exec($shellCommand); + + if (strlen(stristr($output,$device))>0) { + preg_match("/\(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\)/", $output, $matches); + $deviceIP = $matches[0]; + + // remove first and last character because we added them to our regExp + $deviceIP = substr($deviceIP, 1 , strlen($deviceIP)-2); + + return $deviceIP; + } else { + return null; + } + } + + function wakeup($ip) { + // contact the iphone on port 62078 to keep wifi on + $command = "nmap -P0 -sT -p62078 ".$ip." 2>&1"; + shell_exec($command); + } + + function tryWakeUpWithOldIP($virtualSensorID) { + global $mysqli; + global $db_prefix; + + $query = "select lv.value from ".$db_prefix."virtual_sensors_log l, ".$db_prefix."virtual_sensors_log_values lv where l.id = lv.log_id and lv.value_key='ip' and l.sensor_id=".$virtualSensorID." and lv.value is not null and LENGTH(lv.value)>0 order by l.time_updated desc limit 1"; + $result = $mysqli->query($query); + + $ip = $result->fetch_assoc(); + wakeup($ip['value']); + + //wait a second + sleep(3); + } + + function trySNMPCheck($parameter, $virtualSensorID) { + $device = $parameter['mac_client']; //"58:1F:AA:B0:31:2A"; + $host = $parameter['snmp_host']; //"192.168.1.1"; + $community = "public"; + $snmpObj = "1.3.6.1.2.1.3.1.1.2"; + + $snmpReturnVals = snmp2_walk($host, $community, $snmpObj); + + $contains = containsDevice($device, $snmpReturnVals); + + return $contains; + } + + function containsDevice($device, $snmpReturnVals) { + foreach ($snmpReturnVals as $returnVal) { + $hex = getHexCode($returnVal); + if (strcasecmp($device, $hex)==0) { + return 1; + } + } + return 0; + } + + function getHexCode($snmpReutnVal) { + if (startsWith($snmpReutnVal, 'Hex-STRING: ') == TRUE){ + $hex = str_ireplace('Hex-STRING: ', '', $snmpReutnVal); + $hex = trim($hex); + $hex = str_ireplace(' ', ':', $hex); + return $hex; + } else { + return null; + } + } + + function startsWith($haystack, $needle) { + return !strncmp($haystack, $needle, strlen($needle)); + } + +?> diff --git a/fuPlugins/virtutal_devices/network_scan/plugin_info.txt b/fuPlugins/virtutal_devices/network_scan/plugin_info.txt new file mode 100644 index 0000000..e8260f9 --- /dev/null +++ b/fuPlugins/virtutal_devices/network_scan/plugin_info.txt @@ -0,0 +1,4 @@ +name = Network scan +author = Jens Leisenberg +version = 1 +description = Detects hosts in a network using NMAP and/or SNMP. It is also possible to detect smartphones. \ No newline at end of file diff --git a/fuPlugins/virtutal_devices/owl_energy_monitor/fetchSonsorData.sh b/fuPlugins/virtutal_devices/owl_energy_monitor/fetchSonsorData.sh new file mode 100644 index 0000000..32a6f71 --- /dev/null +++ b/fuPlugins/virtutal_devices/owl_energy_monitor/fetchSonsorData.sh @@ -0,0 +1,8 @@ +cm160server > /var/log/owl & +# Get its PID +PID=$! +echo $PID +# Wait for 2 seconds +sleep 15 +# Kill it +kill -9 $PID diff --git a/fuPlugins/virtutal_devices/owl_energy_monitor/icon.png b/fuPlugins/virtutal_devices/owl_energy_monitor/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..780fd61e5d8cc3557964b673822b96f9ddd4fb54 GIT binary patch literal 376 zcmeAS@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3Y)RhkE)4%caKYZ?lYt_f1s;*b z3=DjSL74G){)!Z!AbW|YuPggqRv7_RxqXd2pMXNLnIRD+&iT2ysd*(pE(3#eQEFmI zYKlU6W=V#EyQgnJie4%^Q2dUki(`n#@wd|qxehr9xZZd3)r^SjY%SmLP2gLT>j##m zsnb8C$Q@+v@KtEtW!CI6@xYmNdIx!YCeJ^6JMmwRSd@fw0&`Ct?>#2D8 server have to be started with parameters: /usr/local/bin/cm160server -t /cm160_data.db.txt -q > /var/log/owl160 (init-script) + this plugin will read the new entries in the generated TXT file and not directly to the sensor!!! Take care that the TXT file is written + periodically + after a complete read of the file, the file will be deleted +*/ + +/* $paras = array( + 'datapath' => '/cm160_data.db.txt', // path to SQLlite db + 'energy_consumption' => '', //return + 'voltage' => '', // text + ); +*/ + + function activateHook() { + // return an array with the description, and all fields needed for determining to correct sensor state + return getConfigArray(); + } + + function disableHook() { + // nothing todo for this plugin + } + + function updateHook() { + return getConfigArray(); + } + + function getConfigArray() { + return $configs = array( + 'datafilepath' => array('Path to OWL-export file' => 'text'), + 'voltage' => array('Voltage' => 'text'), + 'energy_consumption' => array('Energy consumption' => 'return') + ); + } + + // marks that the plugin is supporting charts + // maybe it will transform the data before it will be shown in + // UI + function editChartData($virtualSensorID, $chartDataArray) { + // round each value and convert it into Watt + $newChartDataArray = array(); + while (list($returnKey, $returnValues) = each($chartDataArray)) { + + //if returnKey == 'energy_consumption' --> convert and round + if ($returnKey=='energy_consumption') { + while (list($timestamp, $value) = each($returnValues)) { + $consumptionAmpere = round($value,2); + $voltageConfig = getVirtualSensorConfigToKey($virtualSensorID, "voltage"); + $consumptionWatt = $consumptionAmpere * $voltageConfig; + $newChartDataArray[$returnKey][$timestamp] = $consumptionWatt; + } + } else { + $newChartDataArray[$returnKey][$timestamp] = $value; + } + } + + return $newChartDataArray; + } + + // chance to redefine the description and the suffix for the axis + // like they will shown on the UI + // --> iterate over the array, included is another array, keys: + // 0 --> position, don't change + // 1 --> description + // 2 --> suffix + function overwriteChartAxisDefinition($axisDefinition) { + $newAxisDefinition = array(); + while (list($value_key, $configArray) = each($axisDefinition)) { + if ($value_key == 'energy_consumption') { + $configArray[2] = " W"; + } + $newAxisDefinition[$value_key] = $configArray; + } + return $newAxisDefinition; + } + + function getDashBoardWidget($lastLogValues, $virtualSensorID) { + $myPath = getPluginPathToVSensorId($virtualSensorID); + + $consumptionAmpere = round($lastLogValues['energy_consumption'],2); + $voltageConfig = getVirtualSensorConfigToKey($virtualSensorID, "voltage"); + $consumptionWatt = $consumptionAmpere * $voltageConfig; + + $widget = ""; + $widget.="
"; + + $widget.= "
"; + $widget.= getVirtualSensorDescriptionToSensorId($virtualSensorID); + $widget.= "
"; + + $widget.= "
"; + $widget.= getVirtualSensorTypeDescriptionToSensorId($virtualSensorID); + $widget.= "
"; + + $widget.= "
"; + $widget.= "icon"; + $widget.= " ".$consumptionWatt." W ".$consumptionAmpere." A"; + $widget.= "
"; + + $widget.= "
"; + $timeUpdatedByInsertedLog = getLastVirtualSensorLogTimestamp($virtualSensorID); + if ($timeUpdatedByInsertedLog==0){ + $timeUpdatedByInsertedLog = time(); + } + + $widget.= "".date("d-m-Y H:i", $timeUpdatedByInsertedLog).""; + $widget.= "
"; + $widget.= "
"; + + return $widget; + } + + function getVirtualSensorVal($parameter, $virtualSensorID) { + + $datafilepath=$parameter['datafilepath']; //"/cm160_data.db.txt"; + + $row = 1; + $avgConsumption = 0; + if (($handle = fopen($datafilepath, "r")) !== FALSE) { + $count=0; + + while (($data = fgetcsv($handle, 1000, " ")) !== FALSE) { + $num = count($data); + + $time = $data[0]; + $date = $data[1]; + $consumption = $data[2]; + $footprint = $data[3]; + + $avgConsumption = $avgConsumption + $consumption; + $count++; + } + + if ($count>0) { + $avgConsumption = $avgConsumption / $count; + } else { + $avgConsumption = -1; + } + fclose($handle); + unlink($datafilepath); + touch($datafilepath); + } + + + $returnValArr = array(); + if ($avgConsumption >= 0) { + $returnValArr = array('energy_consumption'=>$avgConsumption); + } else { + $returnValArr = getLastVirtualSensorLog($virtualSensorID); + } + + return $returnValArr; + } + + +?> diff --git a/fuPlugins/virtutal_devices/owl_energy_monitor/plugin_info.txt b/fuPlugins/virtutal_devices/owl_energy_monitor/plugin_info.txt new file mode 100644 index 0000000..16fb155 --- /dev/null +++ b/fuPlugins/virtutal_devices/owl_energy_monitor/plugin_info.txt @@ -0,0 +1,4 @@ +name = OWL Energy Monitor +author = Jens Leisenberg +version = 2 +description = 'Reads the current energy consumption, generated by electicowl (sourceforge) from an SQLLite database.' diff --git a/fuTelldus.sql b/fuTelldus.sql index e2b7679..8a92949 100644 --- a/fuTelldus.sql +++ b/fuTelldus.sql @@ -42,8 +42,8 @@ INSERT INTO `futelldus_config` (`config_id`, `config_name`, `config_value`, `com (1, 'pagetitle', 'fuTelldus', ''), (10, 'default_language', 'en', ''), (11, 'mail_from', 'mail@mydomain.com', ''), -(13, 'chart_max_days', '365', ''), -(14, 'public_page_language', 'en', ''); +(14, 'public_page_language', 'en', ''), +(15, 'pushover_api_token', '', ''); -- -------------------------------------------------------- @@ -84,12 +84,13 @@ CREATE TABLE IF NOT EXISTS `futelldus_schedule` ( `device` int(11) NOT NULL, `device_set_state` tinyint(4) NOT NULL, `send_to_mail` tinyint(4) NOT NULL, + `send_to_pushover` tinyint(4) NOT NULL, `last_warning` int(11) NOT NULL, `notification_mail_primary` varchar(256) NOT NULL, `notification_mail_secondary` varchar(256) NOT NULL, PRIMARY KEY (`notification_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; - +-- ALTER TABLE futelldus_schedule ADD send_to_pushover tinyint(4) NOT NULL; -- -------------------------------------------------------- @@ -163,9 +164,12 @@ CREATE TABLE IF NOT EXISTS `futelldus_users` ( `password` varchar(128) NOT NULL, `language` varchar(64) NOT NULL, `admin` tinyint(4) NOT NULL, - `chart_type` varchar(64) NOT NULL, + `pushover_key` varchar(256), PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=2 ; +-- ALTER TABLE futelldus_users ADD pushover_key varchar(256); +-- alter table futelldus_users drop column chart_type; + -- -- Dataark for tabell `futelldus_users` @@ -187,9 +191,92 @@ CREATE TABLE IF NOT EXISTS `futelldus_users_telldus_config` ( `private_key` varchar(256) NOT NULL, `token` varchar(256) NOT NULL, `token_secret` varchar(256) NOT NULL, + `pushover_key` varchar(256), PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtueal_sensors` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors` ( + `id` MEDIUMINT NOT NULL AUTO_INCREMENT, + `user_id` int(11) NOT NULL, + `description` varchar(256) NOT NULL, + `sensor_type` tinyint(4) NOT NULL, + `last_update` int(11), + `last_check` int(11), + `online` tinyint(4) NOT NULL, + `monitoring` tinyint(4) NOT NULL, + `show_in_main` tinyint(4) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtual_sensors_config` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors_config` ( + `sensor_id` int(11) NOT NULL, + `config_id` int(11) NOT NULL, + `value` varchar(256) NOT NULL, + PRIMARY KEY (`sensor_id`, `config_id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtual_sensors_tmpvals` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors_tmpvals` ( + `sensor_id` int(11) NOT NULL, + `value_key` varchar(256) NOT NULL, + `value` varchar(256), + PRIMARY KEY (`sensor_id`, `value_key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtueal_sensors_types` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors_types` ( + `type_int` MEDIUMINT NOT NULL AUTO_INCREMENT, + `type_description` varchar(256), + `plugin_path` varchar(256), + `activated_version` int(11) NOT NULL, + `hidden` tinyint(4) NOT NULL COMMENT '0=off, 1=on', + PRIMARY KEY (`type_int`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtual_sensors_types_config` +-- +CREATE TABLE `futelldus_virtual_sensors_types_config` ( + `id` MEDIUMINT auto_increment not null, + `type_int` int(11), + `value_key` varchar(256), + `value_type` varchar(256) NOT NULL, + `description` varchar(256), + key(id), + PRIMARY KEY (`type_int`, `value_key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtual_sensors_log` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors_log` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `sensor_id` int(11) NOT NULL, + `time_updated` int(11) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + +-- +-- Tabellstruktur for tabell `futelldus_virtual_sensors_log_values` +-- +CREATE TABLE IF NOT EXISTS `futelldus_virtual_sensors_log_values` ( + `log_id` int(11) NOT NULL, + `value_key` varchar(256) NOT NULL, + `value` varchar(256) NOT NULL, + PRIMARY KEY (`log_id`, `value_key`) +) ENGINE=InnoDB DEFAULT CHARSET=latin1; + -- -- Dataark for tabell `futelldus_users_telldus_config` -- diff --git a/images/Thumbs.db b/images/Thumbs.db index 494629d6a4841e5b079dfa86ea45237ea4204977..761b628c6206578c432cf439291eb2db72985630 100644 GIT binary patch delta 106 zcmZoT!`N_!aYGIZTf;K@+?t5ZMJ(T#CZAy9*<8Zz@5{@`$iTqx9||U4WL25`C8TGv mx~nrUBTN)TgJf9#0U4Wrg!*vsumUx)K{bM;HkZWpumb?5I5d&~ delta 137 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 literal 0 HcmV?d00001 diff --git a/images/host.png b/images/host.png new file mode 100644 index 0000000000000000000000000000000000000000..8258e598335e4289ac24a28d65d7e14af48f7f92 GIT binary patch literal 831 zcmV-F1Hk-=P)VGd000McNliru-31O59|i1Gtgiq703B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00N^)L_t(Y$HkUUNL4`; z#((GeEJ;%lF(o5|!j-fsBq%VF7Ewl*A%&c1WQ(zx(IdR9pE5LzEvJU_S;H2yU zpgU!k-N1m}w`ks=>^<^34>YM)1D)Guf;1#uT8+5|ih z?@my$i-FH5dW$iib8+GK$^i-uz)SVbz-^#Ybuk4D0n@-$cmFJ2r+Cqv33s0|vnpUy z@OiB&u6tVLS=sGc%u((93ec63-!6?C&;YCeCp5N8e%}DOqT&)^hr)Y5z)r>N8ud-U zaiA$-5&gi!nEj`)RtbxAS`}#lb`EHW@%n&UKwpB73()~2`!VpSh?5ewyFgD2tBKje z%GYoVuL@5npdNS*)RiM_z>zh+S5v;0)Y}@|{qbm}3YT#fcL2-E+cMBmsgK-W-gZ5@ zJfcgaXI<{&GFop5yY%&0=^u!h<$({t)&TyZaIa*!<1U~rhP4ULG}H$Z=H-NED%@Kk z{_i<9vpSt>jS0guz&3ZE&tk`Wbpa%`290MD>O;ZL!m5C1ysVb5PE(A z*)_($W+gle)Rq`ue_?lDGPCKF+9s3abjW6aB~0v002ov JPDHLkV1giOb~FG0 literal 0 HcmV?d00001 diff --git a/images/light.png b/images/light.png new file mode 100644 index 0000000000000000000000000000000000000000..d5c1033d59df44d0d13f2b0c9926c1d84e033f8a GIT binary patch literal 505 zcmVVGd000McNliru-31O5A1*-XRqp@*03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00CV|L_t(Y$JLj;Y63wN zg}<{3k`y+PSSloquaGtmAovEplywThQhWsktALeVe1VWOfv8QqDKdhXtj^DBaNrg* z%$#q}?wvb3GBf_8m17o3ssi_}`(kDv!6Ihn6+j8-0?)t(Fc%v))&)x5dY;faFxi3S z#RRAq0tTwU+ylCeIZ!PE+zWtiqn88*YQQ=IyasBAw6i0*_Xt!X62Ics=C)(tM#5HY zq<~im;8hBEmH?hb7ZCSgi-_b}09opG>O_D$MOChT0JL9Z$!zV_=e*bet-`<#KJoyM zeD$(`EPX0y&d<^6`y(Lf8h8V)4(!{SnJs5OhmzW7aV+T$cyQgRnJrEZmUIi;>=cp}@NQvLRT%I-i&(usN10`$A0h6#NG`{BX({Cm=J00000NkvXXu0mjfSNP4b literal 0 HcmV?d00001 diff --git a/images/moon.png b/images/moon.png new file mode 100644 index 0000000000000000000000000000000000000000..11860cc414caa0a1b402382d4e1dd6599d103425 GIT binary patch literal 704 zcmV;x0zdtUP)VGd000McNliru-31O5ATkU$Bb)#L03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00JaQL_t(Y$L-h8i_Kva z$MM%0!x+X87Bh*3U$Ici!a^i0Oj*evp!^H6v9Xh~la!>S1`8`2vXF&^vXZ1x7Jf^T z7{-qrn`i2FyZ3v)GvAnvQ>S~s=bq=hpL4s<=R7wu(_C1eIkNxY8Cygwj);p9vGbpT zGjkI6aW^8yBVt+Ig^e8x@Dho~Slw$)U(K&`Fd}wTZ2kw%m&eVx84<&cI>sKH!vpNY zV3)jg_+7eAoUg1~13ZSG)5S2a!~@&fav!&Ve_%rs@KU_S6O5JECLF~>Y{kM7zuW>& zoM{4Hj3La&Xo>B|R4IN3OG^BB8+f8y-Ts=#W!m19*IW3AU$~T+pG*8}+w%1hF_M{Y z8=d2bSXtgK#CB}Jy_S@r_9a*`YXOOfp@=wy>k+XlGrwi#^UR#e%%?4>8#^pt)I_?D3{Anwji0Zjg4gLe9QFOl$4BJZp0000iRvw%XfnIRD+&iT2ysd*(pE(3#eQEFmI zYKlU6W=V#EyQgnJie4%^0|O(Er;B5V$ML(<&Uzhj5NO%&wMg1Uz~+a zo^KET>-h(iFD$#0zFXo0L&*WTjA?A|8ucGYu2}Xi$BW}=*)|ViRK)siAiUdXzFOR7GHhzXj5!|#IO5RrC#s#wDdgv zB-zr#kKgQh-gi*t!AhIn#-eZYSRY%is$+QPet-SZBArb3EA?}BvflczWG_S60q-Z_ z<(56`8&7*2$lrEfb?X7y0^ystr=BX5miT{b``iAb%O)?mRc61vubyG{FP($@Qg&{j O0P}S9b6Mw<&;$U;c&s-7 literal 0 HcmV?d00001 diff --git a/inc/chart.php b/inc/chart.php index e21f13d..d9e672c 100644 --- a/inc/chart.php +++ b/inc/chart.php @@ -1,92 +1,196 @@ -your userprofile
."; - exit(); - } - - - - /* Get chart type - --------------------------------------------------------------------------- */ - if (empty($user['chart_type'])) $chartType = "rgraph"; - else $chartType = $user['chart_type']; - - if (isset($_GET['charttype'])) { - $chartType = clean($_GET['charttype']); - } - - if (isset($_GET['view'])) { - $view = clean($_GET['view']); - } - - + + +{$lang['Chart']}"; - + echo "

{$lang['Charts']}

"; - - - /* Toggle + /* Scenes - todo --------------------------------------------------------------------------- */ - echo "
"; - - echo ""; - echo "{$lang['Action']}"; - echo ""; - echo ""; - - echo ""; - echo "
"; - - - - - - - - - /* Include chart - --------------------------------------------------------------------------- */ - if (!isset($_GET['view'])) { - if ($chartType == "rgraph") { - include("inc/chart_rgraph.php"); - } - - elseif ($chartType == "highcharts") { - include("inc/chart_highchart.php"); - } - - elseif ($charttype == "mergeCharts") { - include("inc/chart_rgraph_mergeSensors.php"); - } - - else { - echo "Something went wrong.. Could'n determine chart to display. Try selecting chart in your userprofile."; - } - } - - - else { - if ($view == "mergeCharts") { - include("inc/chart_rgraph_mergeSensors.php"); - } - } - -?> \ No newline at end of file +/* echo "
"; + echo "{$lang['Scenes']}"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo "
{$lang['Name']}Included dataActions
Schlafzimmer mit FensterSchlafzimmer; Fenster"; + echo " "; + echo " "; + echo ""; + echo "
Jens@Home mit Pathfinder onlinepathfinder online; Jens@Home"; + echo " "; + echo " "; + echo ""; + echo "
"; + + echo "
"; + echo "Create scene"; + echo "
"; + echo "
"; */ + + /* Sensors + --------------------------------------------------------------------------- */ + echo "
"; + echo "{$lang['Sensors']}"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + + + $query = "select s.name, s.sensor_id as id, 'sensor' as type, count(sl.time_updated)-1 as rows from ".$db_prefix."sensors s, ".$db_prefix."sensors_log sl + where s.monitoring=1 and s.sensor_id=sl.sensor_id group by name, id, type + union + select vs.description as name, vs.id as id, 'virtual' as type, count(vsl.time_updated)-1 as rows from ".$db_prefix."virtual_sensors vs, ".$db_prefix."virtual_sensors_log vsl + where vs.monitoring=1 and vs.id=vsl.sensor_id group by name, id, type"; + $result = $mysqli->query($query); + + while($row = $result->fetch_array()) { + echo ""; + echo ""; + $activateChartButton = ""; + if ($row['rows']>0) { + echo ""; + } else { + echo ""; + $activateChartButton = "disabled"; + } + echo ""; + echo ""; + } + + echo ""; + echo "
{$lang['Name']}
".$row['name']."about ".$row['rows']." logsno data"; + + $activateChartButton = ""; + if ($row['type']=='virtual' and !isPluginProvidingCharts($row['id'])) { + $activateChartButton = "disabled"; + } + echo ""; + echo "
"; + echo "
"; + + /* Devices + --------------------------------------------------------------------------- */ + echo "
"; + echo "{$lang['Devices']}"; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + + $query = "SELECT d.name, d.type as type, d.device_id as id, count(dl.time_updated)-1 as rows FROM ".$db_prefix."devices d, ".$db_prefix."devices_log dl where d.device_id = dl.device_id group by name, id, type"; + $result = $mysqli->query($query); + + while($row = $result->fetch_array()) { + echo ""; + echo ""; + $activateChartButton = ""; + if ($row['rows']>0) { + echo ""; + } else { + echo ""; + $activateChartButton = "disabled"; + } + + echo ""; + echo ""; + } + echo ""; + + echo "
{$lang['Name']}
".$row['name']."about ".$row['rows']." logsno data"; + echo ""; + echo "
"; + echo "
"; +?> + + + + + \ No newline at end of file diff --git a/inc/charts/getChart.php b/inc/charts/getChart.php new file mode 100644 index 0000000..dff86a9 --- /dev/null +++ b/inc/charts/getChart.php @@ -0,0 +1,527 @@ +"; + + + function generateVirtualSensorChart($id, $name) { + + // determine, how the hichart object should be configured: + // ask the plugin abaout returnData and how it should be displayed + // the array-position now have to be the same like later --> the plugin + // is responsible for that + + $axisArray = getVirtualSensorChartAxis($id); + $yaxis = ""; + $series = ""; + $callback = ""; + $axisInfo = array(); + foreach ($axisArray as $axisConfig) { + $position = $axisConfig[0]; + $description = $axisConfig[1]; + $suffix = $axisConfig[2]; + $yaxis .= "{ + title: { + text: '$description', + }, + labels: { + formatter: function() { + return this.value +'$suffix'; + }, + }, + }, "; + $series .= "{ + name: '$description', + data : data[$position], + dataGrouping: { + enabled: false + }, + tooltip: { + valueSuffix: '$suffix' + }, + }"; + $callback .= " + chart.series[$position].setData(data[$position]);"; + + $axisInfo['yaxis'] = $yaxis; + $axisInfo['series'] = $series; + $axisInfo['callback'] = $callback; + } + + //print_r($axisInfo); + + return getVirtualSensorJavaScript($id, $axisInfo); + } + + function getVirtualSensorJavaScript($id, $axisInfo) { + $script = ""; + $script .= " + "; + return $script; + } + + function generateDeviceChart($id, $name) { + global $tempColor; + global $humiColor; + + return " + + + "; + } + + + + function generateSensorChart($id, $name) { + global $tempColor; + global $humiColor; + + return " + + + "; + } + + +?> diff --git a/inc/charts/getChartDataJSON.php b/inc/charts/getChartDataJSON.php new file mode 100644 index 0000000..9b56695 --- /dev/null +++ b/inc/charts/getChartDataJSON.php @@ -0,0 +1,234 @@ +connect_errno) { + die('Connect Error: ' . $mysqli->connect_errno); + } + + // Set DB charset + mysqli_set_charset($mysqli, "utf8"); + + // get parameter + $id = clean($_GET['id']); + $type = clean($_GET['type']); + + $callback = $_GET['callback']; + if (!preg_match('/^[a-zA-Z0-9_]+$/', $callback)) { + die('Invalid callback name'); + } + + $start = $_GET['start']; + if ($start && !preg_match('/^[0-9]+$/', $start)) { + die("Invalid start parameter: $start"); + } + + $end = $_GET['end']; + if ($end && !preg_match('/^[0-9]+$/', $end)) { + die("Invalid end parameter: $end"); + } + + + echo "/*para_start=$start, para_end=$end*/"; + + if (!$end){ + $end = time(); + } else { + $end = $end/1000; + } + if (!$start) { + $start = 1; + } else { + $start = $start/1000; + } + + + // Set how long back you want to pull data + //$showFromDate = time() - 86400 * $config['chart_max_days']; // 86400 => 24 hours * 10 days + + + /* Generate the chart + -------------------------------------------------------*/ + if ($type == "sensor") { + $rows = getSensorChartJSON($id, $name, $start, $end); + header('Content-Type: text/javascript'); + echo $callback ."([\n" . $rows ."\n]);"; + } + + if ($type == "virtual") { + $rows = generateVirtualSensorChart($id, $name, $start, $end); + header('Content-Type: text/javascript'); + echo $callback ."([\n" . $rows ."\n]);"; + } + + if ($type == "device") { + $rows = generateDeviceChart($id, $name, $start, $end); + header('Content-Type: text/javascript'); + echo $callback ."([\n" . $rows ."\n]);"; + } + + // returns an array in array with the return-type definied by the plugin + // and the data found in the DB belonging to this return-type + function generateVirtualSensorChart($id, $name, $start, $end) { + global $mysqli; + global $db_prefix; + + $offset = date('Z'); + echo "/* timezone offset: ".$offset."\n*/"; + // calculate the correct offset to get ever 0am for the actual day + $end = $end - $offset + 86400; + + $chartDataArray = getVirtualSensorChartData($id, $start, $end); + // convert the chartDataArray into a highchart compatible array + // make sure, that all the values are at the correct position (given by parameter) + + // every axis + $returnArray = ""; + $first = true; + while (list($returnKey, $returnValues) = each($chartDataArray)) { + //echo $returnKey; + + //all timestamps with values + $axis = array(); + while (list($timestamp, $value) = each($returnValues)) { + $axis[] = "[" . $timestamp*1000 . "," . $value . "]"; + } + if ($first) { + $returnArray .= "[\n" . join(",\n", $axis) ."\n]"; + $first = false; + } else { + $returnArray .= ",[\n" . join(",\n", $axis) ."\n]"; + } + } + return $returnArray; + } + + function generateDeviceChart($id, $name, $start, $end) { + global $mysqli; + global $db_prefix; + + $offset = date('Z'); + echo "/* timezone offset: ".$offset."\n*/"; + // calculate the correct offset to get ever 0am for the actual day + $end = $end - $offset + 86400; + + $queryS = " + SELECT * FROM futelldus_devices_log + WHERE device_id={$id} + and time_updated between $start and $end + ORDER BY time_updated ASC + "; + echo "/* query: $queryS */"; + $resultS = $mysqli->query($queryS); + + $temp_values = array(); + $first=true; + while ($sensorData = $resultS->fetch_array()) { + + // add extra values to get the on and off-range correctly (last -1 millisecond with old state) + $timeJS = $sensorData["time_updated"] * 1000; + if (!$first) { + $timeEndLast = $timeJS - 1; + $temp_values[] = "[" . $timeEndLast . "," . $status . "]"; + } else { + $first=false; + } + + // convert status to 0 and 1 status + $status = convertStatusToOnOff(trim($sensorData["status"])); + + $temp_values[] = "[" . $timeJS . "," . $status . "]"; + } + + // add the current timestamp with the last state + $temp_values[] = "[" . time()*1000 . "," . $status . "]"; + + $return = join(",\n", $temp_values); + + return $return; + } + + function convertStatusToOnOff($status) { + $newstatus = 0; + if ($status > 1) { + $newstatus = $status - floor($status); + } else { + $newstatus = 1; + } + return $newstatus; + } + + + function getSensorChartJSON($id, $name, $start, $end) { + global $mysqli; + global $db_prefix; + + $range = $end - $start; + + $rangeValue=86400; // bigger than half a year, dayily + if ($range <= 15778458) { // half year + $rangeValue=43200; // halfdays + } + + if ($range <= 2629744) { // one month + $rangeValue=3600; // hour + } + + if ($range <= 604800) { // one week + $rangeValue=60; //minute + } + + if ($range <= 86400) { // one day + $rangeValue=1; // all + } + + if ($range <= 604800) { // one week + $rangeValue=1; //all + } + + echo "/* range: $range, so grouping: $group_by \n*/"; + + $offset = date('Z'); + echo "/* timezone offset: ".$offset."\n*/"; + // calculate the correct offset to get ever 0am for the actual day + $end = $end - $offset + 86400; + + $queryS = " + SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(time_updated)) as time_updated, ROUND(avg(temp_value),1) as temp_value, ROUND(avg(humidity_value),1) as humidity_value FROM futelldus_sensors_log + WHERE sensor_id='{$id}' + and time_updated between $start and $end + GROUP BY FLOOR(time_updated / $rangeValue) + ORDER BY time_updated ASC + "; + echo "/* query: $queryS */"; + $resultS = $mysqli->query($queryS); + + $temp_values = array(); + while ($sensorData = $resultS->fetch_array()) { + $db_tempValue = trim($sensorData["temp_value"]); + $db_humidity_value = trim($sensorData["humidity_value"]); + + $timeJS = $sensorData["time_updated"] * 1000; + $temp_values[] = "[" . $timeJS . "," . $db_tempValue . "]"; + $humi_values[] = "[" . $timeJS . "," . $db_humidity_value . "]"; + + } + $return = "[\n" . join(",\n", $temp_values) ."\n]"; + $return .= ",[\n" . join(",\n", $humi_values) ."\n]"; + + //$joinValues = join(",\n", $temp_values); + //print_r ($temp_values); + return $return; + } + + + +?> diff --git a/inc/mainpage.php b/inc/mainpage.php index 4b65bf0..997f027 100644 --- a/inc/mainpage.php +++ b/inc/mainpage.php @@ -80,7 +80,7 @@ $result = $mysqli->query($query); while ($row = $result->fetch_array()) { - + $showFromDate = 0; $sensorID = trim($row['sensor_id']); @@ -168,8 +168,27 @@ } } echo ""; + + + + /* Virtual sensors + --------------------------------------------------------------------------- */ + echo "
"; + $query = "SELECT * FROM ".$db_prefix."virtual_sensors WHERE user_id='{$user['user_id']}' AND show_in_main='1' ORDER BY description ASC"; + $result = $mysqli->query($query); - + while ($row = $result->fetch_array()) { + + $sensorType = trim($row['sensor_type']); + + $queryVHT = "SELECT * FROM ".$db_prefix."virtual_sensors_types WHERE type_int='$sensorType'"; + $resultVHT = $mysqli->query($queryVHT); + $virtualHostType = $resultVHT->fetch_array(); + + $actualState = getCurrentVirtualSensorStateWidet($row['id']); + echo $actualState; + } + echo "
"; echo ""; diff --git a/inc/sensors_exec.php b/inc/sensors_exec.php index 7f35376..20f05e2 100644 --- a/inc/sensors_exec.php +++ b/inc/sensors_exec.php @@ -5,9 +5,7 @@ if (isset($_GET['id'])) $getID = clean($_GET['id']); if (isset($_GET['action'])) $action = clean($_GET['action']); - - - + /* add sensor --------------------------------------------------------------------------- */ @@ -60,4 +58,5 @@ exit(); } + ?> \ No newline at end of file diff --git a/inc/settings.php b/inc/settings.php index ab2f986..62a3e84 100644 --- a/inc/settings.php +++ b/inc/settings.php @@ -1,5 +1,5 @@ {$lang['Userprofile']}"; echo "
  • {$lang['Shared sensors']}
  • "; + echo "
  • {$lang['Virtual sensors']}
  • "; echo "
  • {$lang['Schedule']}
  • "; //echo "
  • {$lang['Telldus connection test']}
  • "; @@ -53,6 +67,9 @@ echo "
  • ".$lang['Page settings']."
  • "; echo "
  • ".$lang['Users']."
  • "; echo "
  • ".$lang['Test cron-files']."
  • "; + + echo ""; + echo "
  • ".$lang['Virtual sensor plugins']."
  • "; } ?> diff --git a/inc/settings_cron.php b/inc/settings_cron.php index bfc4aed..1377987 100644 --- a/inc/settings_cron.php +++ b/inc/settings_cron.php @@ -3,4 +3,6 @@

    Run CRON files manually to get/check data. The link will open in a new window, and would most likely don't return any data/text in your browser

    > Get temperature values
    +> Get device status
    +> Get virtual sensor status
    > Run schedule \ No newline at end of file diff --git a/inc/settings_exec.php b/inc/settings_exec.php index 92aa6f2..81fc30e 100644 --- a/inc/settings_exec.php +++ b/inc/settings_exec.php @@ -30,12 +30,14 @@ $token_key = clean($_POST['token_key']); $token_secret_key = clean($_POST['token_secret_key']); - + $pushover_key = clean($_POST['pushover_key']); + // Insert user $query = "INSERT INTO ".$db_prefix."users SET mail='".$inputEmail."', password='".$cryptPW."', admin='".$admin."', + pushover_key='".$pushover_key."' chart_type='".$selectChart."'"; $result = $mysqli->query($query); @@ -81,13 +83,17 @@ $private_key = clean($_POST['private_key']); $token_key = clean($_POST['token_key']); $token_secret_key = clean($_POST['token_secret_key']); + + $pushover_key = clean($_POST['pushover_key']); + // Update userdata $query = "UPDATE ".$db_prefix."users SET mail='".$inputEmail."', language='".$language."', - chart_type='".$selectChart."' + chart_type='".$selectChart."', + pushover_key='".$pushover_key."' WHERE user_id='".$getID."'"; $result = $mysqli->query($query); @@ -199,6 +205,7 @@ $mail_from = clean($_POST['mail_from']); $chart_max_days = clean($_POST['chart_max_days']); $language = clean($_POST['language']); + $pushover_api_token = clean($_POST['pushover_api_token']); $query = "UPDATE ".$db_prefix."config SET config_value='".$pageTitle."' WHERE config_name LIKE 'pagetitle'"; @@ -212,6 +219,9 @@ $query = "UPDATE ".$db_prefix."config SET config_value='".$language."' WHERE config_name LIKE 'public_page_language'"; $result = $mysqli->query($query); + + $query = "UPDATE ".$db_prefix."config SET config_value='".$pushover_api_token."' WHERE config_name LIKE 'pushover_api_token'"; + $result = $mysqli->query($query); // Redirect @@ -222,14 +232,6 @@ - - - - - - - - /* Add XML-shared sensor --------------------------------------------------------------------------- */ if ($action == "addSensorFromXML") { @@ -304,14 +306,6 @@ } - - - - - - - - /* Add schedule --------------------------------------------------------------------------- */ if ($action == "addSchedule") { @@ -323,6 +317,7 @@ $type = clean($_POST['type']); $repeat = clean($_POST['repeat']); $sendTo_mail = clean($_POST['sendTo_mail']); + $send_to_pushover = clean($_POST['sendTo_pushover']); $mail_primary = clean($_POST['mail_primary']); $mail_secondary = clean($_POST['mail_secondary']); @@ -341,6 +336,7 @@ device='".$deviceID."', device_set_state='".$device_action."', send_to_mail='". $sendTo_mail ."', + send_to_pushover='".$send_to_pushover."', notification_mail_primary='". $mail_primary ."', notification_mail_secondary='". $mail_secondary ."'"; $result = $mysqli->query($query); @@ -364,6 +360,7 @@ $type = clean($_POST['type']); $repeat = clean($_POST['repeat']); $sendTo_mail = clean($_POST['sendTo_mail']); + $send_to_pushover = clean($_POST['sendTo_pushover']); $mail_primary = clean($_POST['mail_primary']); $mail_secondary = clean($_POST['mail_secondary']); @@ -381,6 +378,7 @@ device='".$deviceID."', device_set_state='".$device_action."', send_to_mail='".$sendTo_mail."', + send_to_pushover='".$send_to_pushover."', notification_mail_primary='".$mail_primary."', notification_mail_secondary='".$mail_secondary."' WHERE notification_id='".$getID."'"; @@ -407,5 +405,186 @@ } + /* add virtual sensor + --------------------------------------------------------------------------- */ + if ($action == "addVirtualSensor") { + // Get POST data + $virtualsensor_description = clean($_POST['virtualsensor_description']); + $virtualsensor_type = clean($_POST['virtualsensor_type']); + + $query = "INSERT INTO ".$db_prefix."virtual_sensors SET + user_id='".$user['user_id']."', + description='".$virtualsensor_description."', + sensor_type='".$virtualsensor_type."', + last_update = '".time()."', + online = '1', + monitoring = '0', + show_in_main = '0'"; + $result = $mysqli->query($query); + $vSensorId = $mysqli->insert_id; + + // for every para starts with virtualsensor_value_ + foreach (array_keys($_POST) as $postkey) { + $para_prefix = "virtualsensor_value_"; + if(strpos($postkey, $para_prefix) === 0){ + $configId = substr($postkey, strlen($para_prefix)); + $configValue = clean($_POST[$postkey]);; + $queryInsert2 = "INSERT INTO ".$db_prefix."virtual_sensors_config SET + sensor_id='".$vSensorId."', + value='".$configValue."', + config_id='".$configId."'"; + $resultInsert2 = $mysqli->query($queryInsert2); + } + } + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=01"); + exit(); + } + + /* update virtual sensor + --------------------------------------------------------------------------- */ + if ($action == "updateVirtualSensor") { + // Get POST data + $virtualsensor_description = clean($_POST['virtualsensor_description']); + + $query = "UPDATE ".$db_prefix."virtual_sensors SET description='".$virtualsensor_description."' where id = '".$getID."'"; + $result = $mysqli->query($query); + + // for every para starts with virtualsensor_value_ + foreach (array_keys($_POST) as $postkey) { + $para_prefix = "virtualsensor_value_"; + if(strpos($postkey, $para_prefix) === 0){ + $configId = substr($postkey, strlen($para_prefix)); + $configValue = clean($_POST[$postkey]);; + $queryInsert2 = "UPDATE ".$db_prefix."virtual_sensors_config SET + value='".$configValue."' + WHERE sensor_id='".$getID."' + AND config_id='".$configId."'"; + $resultInsert2 = $mysqli->query($queryInsert2); + } + } + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=04"); + exit(); + } + + /* Delete schedule + --------------------------------------------------------------------------- */ + if ($action == "deleteVirtualSensor") { + $query = "DELETE FROM ".$db_prefix."virtual_sensors WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + + $query2 = "DELETE FROM ".$db_prefix."virtual_sensors_config WHERE sensor_id='".$getID."'"; + $result2 = $mysqli->query($query2); + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=02"); + exit(); + } + + /* add virtual sensor type + --------------------------------------------------------------------------- */ +/* if ($action == "addVirtualSensorType") { + // Get POST data + $type_description = clean($_POST['type_description']); + $type_image = clean($_POST['type_image']); + $type_php = clean($_POST['type_php']); + + $query = "INSERT INTO ".$db_prefix."virtual_sensors_types SET + type_description='".$type_description."', + phpfile='".$type_php."', + icon='".$type_image."'"; + $result = $mysqli->query($query); + + // Redirect + header("Location: ?page=settings&view=virtualsensors_types&msg=01"); + exit(); + } */ + + /* delete virtual sensor type + --------------------------------------------------------------------------- */ +/* if ($action == "deleteVirtualSensorType") { + $query = "DELETE FROM ".$db_prefix."virtual_sensors_types WHERE type_int='".$getID."'"; + $result = $mysqli->query($query); + + //echo $query; + + $query2 = "DELETE FROM ".$db_prefix."virtual_sensors_types_config WHERE type_int='".$getID."'"; + $result2 = $mysqli->query($query2); + + // Redirect + header("Location: ?page=settings&view=virtualsensors_types&msg=01"); + exit(); + } */ + + /* Put on main + --------------------------------------------------------------------------- */ + if ($action == "putOnMainVirtualSensor") { + + $getCurrentValue = getField("show_in_main", "".$db_prefix."virtual_sensors", "WHERE id='".$getID."'"); + + if ($getCurrentValue == 0) { + $query = "UPDATE ".$db_prefix."virtual_sensors SET show_in_main='1' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } else { + $query = "UPDATE ".$db_prefix."virtual_sensors SET show_in_main='0' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=03"); + exit(); + } + + /* + --------------------------------------------------------------------------- */ + if ($action == "setMonitoring") { + $getCurrentValue = getField("monitoring", "".$db_prefix."virtual_sensors", "WHERE id='".$getID."'"); + + if ($getCurrentValue == 0) { + $query = "UPDATE ".$db_prefix."virtual_sensors SET monitoring='1' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } else { + $query = "UPDATE ".$db_prefix."virtual_sensors SET monitoring='0' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=03"); + exit(); + } + + /* + --------------------------------------------------------------------------- */ + if ($action == "setOnline") { + $getCurrentValue = getField("online", "".$db_prefix."virtual_sensors", "WHERE id='".$getID."'"); + + if ($getCurrentValue == 0) { + $query = "UPDATE ".$db_prefix."virtual_sensors SET online='1' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } else { + $query = "UPDATE ".$db_prefix."virtual_sensors SET online='0' WHERE user_id='".$user['user_id']."' AND id='".$getID."'"; + $result = $mysqli->query($query); + } + + // Redirect + header("Location: ?page=settings&view=virtualsensors&msg=03"); + exit(); + } + + + if ($action == "sendTestNotification") { + $pushover_key = clean($_GET['pushover_key']); + $subject = clean($_GET['subject']); + $message = clean($_GET['message']); + + sendNotification($pushover_key, $subject, $message); + + // Redirect + header("Location: ?page=settings&view=user&action=edit&id=$getID&msg=04"); + exit(); + } ?> \ No newline at end of file diff --git a/inc/settings_general.php b/inc/settings_general.php index b177f25..520e058 100644 --- a/inc/settings_general.php +++ b/inc/settings_general.php @@ -35,6 +35,13 @@ '> + +
    + +
    + '> +
    +
    "; diff --git a/inc/settings_schedule.php b/inc/settings_schedule.php index 6f42af9..c9aa636 100644 --- a/inc/settings_schedule.php +++ b/inc/settings_schedule.php @@ -3,6 +3,8 @@ /* Get parameters --------------------------------------------------------------------------- */ + $action = ""; + $getID = ""; if (isset($_GET['id'])) $getID = clean($_GET['id']); if (isset($_GET['action'])) $action = clean($_GET['action']); @@ -23,6 +25,7 @@ $device = $row['device']; $device_set_state = $row['device_set_state']; $send_to_mail = $row['send_to_mail']; + $send_to_pushover = $row['send_to_pushover']; $mail_primary = $row['notification_mail_primary']; $mail_secondary = $row['notification_mail_secondary']; @@ -30,6 +33,7 @@ $warning_value = 30; $repeat_alert = 60; $send_to_mail = 1; + $send_to_pushover = 1; $mail_primary = $user['mail']; $mail_secondary = ""; } @@ -185,12 +189,20 @@ // Send to echo ""; echo "{$lang['Send to']}"; - echo ""; - echo ""; - echo ""; + echo "
    "; + echo "
    "; + echo "
    "; + echo "
    "; echo ""; @@ -325,7 +337,7 @@ echo ""; echo ""; echo ""; diff --git a/inc/settings_user.php b/inc/settings_user.php index 0d07ed0..f61fbe5 100644 --- a/inc/settings_user.php +++ b/inc/settings_user.php @@ -25,9 +25,6 @@ } } - - - /* Get userdata --------------------------------------------------------------------------- */ $result = $mysqli->query("SELECT * FROM ".$db_prefix."users WHERE user_id='".$getID."'"); @@ -50,6 +47,7 @@ if ($_GET['msg'] == 01) echo "
    ".$lang['Userdata updated']."
    "; elseif ($_GET['msg'] == 02) echo "
    ".$lang['Old password is wrong']."
    "; elseif ($_GET['msg'] == 03) echo "
    ".$lang['New password does not match']."
    "; + elseif ($_GET['msg'] == 04) echo "
    ".$lang['Test message sent']."
    "; } @@ -114,31 +112,40 @@ - + "; - echo "{$lang['Chart']}"; + echo "{$lang['Notification']}"; echo "
    "; - echo ""; + echo ""; echo "
    "; - echo "
    "; echo "
    "; echo ""; ?> - + + + + +
    "; + $syncTelldusOff = ""; + $syncTelldusOn = ""; if ($selectedUserTelldusConf['sync_from_telldus'] == 1) $syncTelldusOn = "checked='checked'"; else $syncTelldusOff = "checked='checked'"; diff --git a/inc/settings_virtualsensors.php b/inc/settings_virtualsensors.php new file mode 100644 index 0000000..1045474 --- /dev/null +++ b/inc/settings_virtualsensors.php @@ -0,0 +1,217 @@ +".$lang['Virtual sensors'].""; + + /* Get parameters + --------------------------------------------------------------------------- */ + $action = ""; + $getID = ""; + if (isset($_GET['id'])) $getID = clean($_GET['id']); + if (isset($_GET['action'])) $action = clean($_GET['action']); + + /* Messages + --------------------------------------------------------------------------- */ + if (isset($_GET['msg'])) { + if ($_GET['msg'] == 01) echo "
    {$lang['Virtual sensor added']}
    "; + if ($_GET['msg'] == 02) echo "
    {$lang['Virtual sensor deleted']}
    "; + if ($_GET['msg'] == 03) echo "
    {$lang['Data saved']}
    "; + if ($_GET['msg'] == 04) echo "
    {$lang['Virtual sensor updated']}
    "; + } + + $description = ""; + $sensor_type = "-1"; + $sensor_type_description = ""; + if ($action == "edit") { + // load data + $query = "SELECT * FROM ".$db_prefix."virtual_sensors vs, ".$db_prefix."virtual_sensors_types vst where vs.sensor_type = vst.type_int and vs.id='$getID' LIMIT 1"; + $result = $mysqli->query($query); + $row = $result->fetch_array(); + + $description = $row['description']; + $sensor_type = $row['sensor_type']; + $sensor_type_description = $row['type_description']; + + // load config data + } + + + /* Form + --------------------------------------------------------------------------- */ + echo "
    "; + echo "{$lang['Add virtual sensor']}"; + + + if ($action == "edit") { + echo "
    "; + echo "
    "; + } else { + echo "
    "; + echo ""; + } + + // add hidden field with actual virtual sensor id + echo ""; + + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + + echo "
    ".$lang['Description'].""; + echo ""; + echo "
    ".$lang['Type'].""; + $disabledSelect = ""; + if ($action == "edit") { + $disabledSelect = "disabled"; + } + echo " "; + echo "
    "; + echo "
    "; + if ($action == "edit") { + echo "{$lang['Cancel']}   "; + echo ""; + } else { + echo ""; + } + echo "
    "; + echo "
    "; + + echo "
    "; + + /* Virtual sensors + --------------------------------------------------------------------------- */ + echo "
    "; + echo "{$lang['Sensors']}"; + + //$query = "SELECT * FROM ".$db_prefix."virtual_sensors WHERE user_id='{$user['user_id']}' ORDER BY description ASC"; + $query = "SELECT * FROM ".$db_prefix."virtual_sensors s, ".$db_prefix."virtual_sensors_types st WHERE user_id='{$user['user_id']}' and st.type_int = s.sensor_type ORDER BY description ASC"; + $result = $mysqli->query($query); + $numRows = $result->num_rows; + + if ($numRows > 0) { + + while($row = $result->fetch_array()) { + echo "
    "; + + // Tools + echo "
    "; + + echo "
    "; + + $toggleClass = ""; + if ($row['show_in_main'] == 1){ + $toggleClass = "btn-success"; + } else { + $toggleClass = "btn-warning"; + } + + if ($row['online'] == 0){ + $toggleClass = "btn-danger"; + } + + echo ""; + echo "{$lang['Action']}"; + echo ""; + echo ""; + + echo ""; + echo "
    "; + + echo "
    "; + + echo "
    ".$row['description']."
    "; + + echo "
    "; + echo "{$lang['Type']}: ".$row['type_description']. "
    "; + //echo "{$lang['Value']}: ".$row["config_value"]. "
    "; + echo "{$lang['Online']}: ".$lang["boolean_".$row['online']]. "
    "; + echo "{$lang['Monitor']}: ".$lang["boolean_".$row['monitoring']]. "
    "; + echo "
    "; + + echo "
    "; + echo ago(getLastVirtualSensorCheck($row['id'])); + echo "
    "; + + echo "
    "; + + } + + } + + echo "
    "; + +?> + + \ No newline at end of file diff --git a/inc/settings_virtualsensors_types.php b/inc/settings_virtualsensors_types.php new file mode 100644 index 0000000..1a2a797 --- /dev/null +++ b/inc/settings_virtualsensors_types.php @@ -0,0 +1,102 @@ +Virtual sensor plugin configuration"; + + + + /* Messages + --------------------------------------------------------------------------- */ + if (isset($_GET['msg'])) { + if ($_GET['msg'] == 01) echo "
    Plugin activated
    "; + if ($_GET['msg'] == 02) echo "
    Plugin disabled
    "; + if ($_GET['msg'] == 03) echo "
    Plugin visiblaty changed
    "; + if ($_GET['msg'] == 04) echo "
    Plugin updated
    "; + } + + /* Form + --------------------------------------------------------------------------- */ + echo "
    "; + echo "Available plugins"; + + $plugins = getAvailablePlugins(); + + // for every plugin + foreach (array_keys($plugins) as $key) { + $activated = isPluginActivated($plugins[$key]["directory"]); + $name=$plugins[$key]["name"]; + $version=$plugins[$key]["version"]; + + $query = "select * from ".$db_prefix."virtual_sensors_types where plugin_path='".$plugins[$key]["directory"]."' and type_description='".$name."'"; + $result = $mysqli->query($query); + $pluginConfig = $result->fetch_assoc(); + + + echo "
    "; + echo "
    "; + + echo "
    "; + echo ""; + echo "{$lang['Action']}"; + echo ""; + echo ""; + + echo ""; + echo "
    "; + + echo "
    "; + + + $newVersionAvailable=($pluginConfig['activated_version']<$version and $activated); + $newVersion = $version; + if (isset($newVersion)and $activated) { + $version = $pluginConfig['activated_version']; + } + + echo "

    ".$name."
    "; + if ($activated) { + echo "
    activated"; + if ($pluginConfig['hidden']==1){ + echo " (hidden)"; + } + echo "
    "; + } else { + echo "
    disabled
    "; + } + echo "

    "; + // list options + echo "
    "; + $description=$plugins[$key]["description"]; + $author=$plugins[$key]["author"]; + echo "Author: ".$author. "
    "; + echo "Version: ".$version. ""; + if ($newVersionAvailable){ + echo ""; + echo " (New version - ".$newVersion." - available. Click to update.) "; + echo ""; + } + echo "
    "; + echo "Description: ".$description. "
    "; + echo "Plugin path: ".$plugins[$key]["directory"]. "
    "; + echo "
    "; + + echo "
    "; + } + + echo "
    "; + +?> + + + + diff --git a/inc/vSensor_exec.php b/inc/vSensor_exec.php new file mode 100644 index 0000000..f85266b --- /dev/null +++ b/inc/vSensor_exec.php @@ -0,0 +1,47 @@ +query($query); + + header("Location: ?page=settings&view=virtualsensors_types&msg=03"); + exit(); + } + +?> \ No newline at end of file diff --git a/inc/virtualHosts/getVirtualSensorTypeConfig.php b/inc/virtualHosts/getVirtualSensorTypeConfig.php new file mode 100644 index 0000000..0659c00 --- /dev/null +++ b/inc/virtualHosts/getVirtualSensorTypeConfig.php @@ -0,0 +1,45 @@ +connect_errno) { + die('Connect Error: ' . $mysqli->connect_errno); + } + + // Set DB charset + mysqli_set_charset($mysqli, "utf8"); + + $type_int = clean($_GET['type_int']); + $sensor_id = clean($_GET['sensor_id']); + + // decide if the current config value should be attached or not + if (strlen($sensor_id) > 0) { + $query = "SELECT vstc.id, vstc.value_key, vstc.value_type, vstc.description, vsc.value as config_value + FROM ".$db_prefix."virtual_sensors_types_config vstc, ".$db_prefix."virtual_sensors_config vsc + where vsc.config_id = vstc.id + and vsc.sensor_id=$sensor_id + and vstc.type_int=$type_int + and vstc.value_type not like 'return%'"; + } else { + $query = "SELECT id, value_key, value_type, description, '' as config_value + FROM ".$db_prefix."virtual_sensors_types_config + WHERE type_int=$type_int + and value_type not like 'return%'"; + } + + $result = $mysqli->query($query); + + $rows = array(); + while($r = $result->fetch_assoc()) { + $rows[] = $r; + } + echo json_encode($rows); + + +?> \ No newline at end of file diff --git a/include_script.inc.php b/include_script.inc.php index 849d041..1288d4d 100644 --- a/include_script.inc.php +++ b/include_script.inc.php @@ -14,7 +14,7 @@ if (file_exists("$directory/$page.$extension")) include("$directory/$page.$extension"); else - echo "

    Error 404

    \n

    Siden du leter etter finnes ikke lengre!

    \n"; + echo "

    Error 404

    \n"; } } diff --git a/index.php b/index.php index 0c7c500..6bf1868 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,9 @@ @@ -19,6 +22,7 @@ + @@ -33,6 +37,8 @@ + + diff --git a/lib/base.inc.php b/lib/base.inc.php index 65339fa..e57de91 100644 --- a/lib/base.inc.php +++ b/lib/base.inc.php @@ -40,6 +40,9 @@ --------------------------------------------------------------------------- */ require ('php_functions/global.functions.inc.php'); require ('php_functions/datetime.functions.inc.php'); + require ('php_functions/vSensorPlugin.functions.inc.php'); + require ('php_functions/telldus.functions.inc.php'); + diff --git a/lib/config.inc.php b/lib/config.inc.php index 4975a32..bbafce6 100644 --- a/lib/config.inc.php +++ b/lib/config.inc.php @@ -4,9 +4,9 @@ /* Connect to database --------------------------------------------------------------------------- */ $host = "localhost"; // Host name - $username = "USERNAME"; // Mysql username - $password = "PASSWORD"; // Mysql password - $db_name = "DATABASE_NAME"; // Mysql DB + $username = "telldus"; // Mysql username + $password = "lb77rghf"; // Mysql password + $db_name = "telldus_log"; // Mysql DB @@ -19,7 +19,7 @@ /* Timezone --------------------------------------------------------------------------- */ - date_default_timezone_set('Europe/Oslo'); + date_default_timezone_set('Europe/Berlin'); @@ -28,5 +28,6 @@ /* PHP error reporting --------------------------------------------------------------------------- */ error_reporting(E_ALL ^ E_NOTICE); + ini_set('include_path', '/www/'); -?> \ No newline at end of file +?> diff --git a/lib/jscripts/futelldus_lights.js b/lib/jscripts/futelldus_lights.js index fce962e..5ec53cb 100644 --- a/lib/jscripts/futelldus_lights.js +++ b/lib/jscripts/futelldus_lights.js @@ -43,4 +43,6 @@ function lightControl(state, deviceID) { ajaxRequest.open("GET", "ajax_light_control.php?state=" + state + "&id=" + deviceID + "&&rand=" + Math.random()*9999, true); ajaxRequest.send(null); } -} \ No newline at end of file +} + + diff --git a/lib/jscripts/jquery.bootstrap.confirm.popover.js b/lib/jscripts/jquery.bootstrap.confirm.popover.js new file mode 100644 index 0000000..b1c8560 --- /dev/null +++ b/lib/jscripts/jquery.bootstrap.confirm.popover.js @@ -0,0 +1,109 @@ +/* +Copyright (c) 2011 Damien Antipa, http://www.nethead.at/, http://damien.antipa.at + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +/* + * jQuery Plugin: Confirmation Dialog + * + * Requirements: jQuery 1.6.4, Bootstrap 1.3.0 + * http://jquery.com/ + * http://twitter.github.com/bootstrap/ + * + * This Plugin can be used for anchors, to show a confirmation popup before redirecting to the link. + * + */ +(function($){ + $.fn.extend({ + confirmDialog: function(options) { + var defaults = { + message: 'Are you sure', + dialog: '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    ' + + '

    ' + + '
    ' + + '
    ' + + '
    ', + cancelButton: 'Cancel' + }; + var options = $.extend(defaults, options); + + return this.each(function() { + var o = options; + var $elem = $(this) + + $elem.bind('click', function(e) { + e.preventDefault(); + if(!$('#confirm-dialog').length) { + + var offset = $elem.offset(); + var $dialog = $(o.dialog).appendTo('body'); + + var x; + if(offset.left > $dialog.width()) { + //dialog can be left + x = e.pageX - $dialog.width(); + $dialog.addClass('left'); + } else { + x = e.pageX; + $dialog.addClass('right'); + } + var y = e.pageY - $dialog.height() / 2 - $elem.innerHeight() / 2; + + $dialog.css({ + display: 'block', + position: 'absolute', + top: y, + left: x + }); + + $dialog.find('p.button-group').css({ + marginTop: '5px', + textAlign: 'right' + }); + + $dialog.find('a.btn').css({ + marginLeft: '3px' + }); + + $dialog.find('p.message').html(o.message); + + $dialog.find('a.btn:eq(0)').text($elem.text()).bind('click', function(e) { + window.location.href = $elem.attr('href'); + }); + + $dialog.find('a.btn:eq(1)').text(o.cancelButton).bind('click', function(e) { + $dialog.remove(); + }); + + $dialog.bind('mouseleave', function() { + $dialog.fadeOut('slow', function() { + $dialog.remove(); + }); + }); + } + }); + }); + } + }); +})(jQuery); \ No newline at end of file diff --git a/lib/languages/en.php b/lib/languages/en.php index 41fa2c2..482e982 100644 --- a/lib/languages/en.php +++ b/lib/languages/en.php @@ -13,18 +13,30 @@ "Sensors" => "Sensors", "Chart" => "Chart", "Report" => "Report", - "Lights" => "Lights", + "Lights" => "Devices", "Settings" => "Settings", "Log out" => "Log out", "Page settings" => "Page settings", "Users" => "Users", - "Shared sensors" => "Delte sensorer", + "Shared sensors" => "Shared sensors", "Test cron-files" => "Test cron-files", "View public page" => "View public page", "View public sensors" => "View public sensors", + "Virtual sensors" => "Virtual sensors", + "Pushover API token" => "Pushover API token", - + + // Common + "Icon" => "Icon", + "Test" => "Test", + "Close" => "Close", + "Send" => "Send", + "Pushover" => "Pushover", + "Send Notification" => "Send Notification", + "Your Pushover key" => "Your Pushover key", + "Select device" => "Select device", + "Test message sent" => "Test message sent", // User "Usersettings" => "Usersettings", @@ -92,6 +104,7 @@ "Combine charts" => "Combine charts", "Split charts" => "Split charts", "View chart" => "View chart", + "Charts" => "Charts", // Sensors @@ -107,6 +120,27 @@ // Shared sensors "Add shared sensor" => "Add shared sensor", + // Virtual sensors + "Add virtual sensor" => "Add virtual sensor", + "Host available" => "Host available", + "Device as sensor" => "Device as sensor", + "Add sensor" => "Add sensor", + "Update sensor" => "Update sensor", + "Virtual sensor added" => "Virtual sensor added", + "Virtual sensor updated" => "Virtual sensor updated", + "Virtual sensor deleted" => "Virtual sensor deleted", + "boolean_0" => "false", + "boolean_1" => "true", + "Virtual sensor types" => "Virtual sensor types", + "Virtual sensor plugins" => "Virtual sensor plugins", + "Virtual sensor type added" => "Virtual sensor type added", + "Virtual sensor type deleted" => "Virtual sensor type deleted", + "Add virtual sensor type" => "Add virtual sensor type", + "Add type" => "Add type", + "description_icon" => "The ralative path to an icon", + "description_typephp" => "The PHP file with getVirtualSensorVal($parameter) method", + "PHPFile" => "PHP file", + // Schedule "Schedule" => "Schedule", @@ -120,6 +154,8 @@ "Last sent" => "Last sent", "Device action" => "Device action", "No device action" => "No device action", + "Notification" => "Notification", + "Pushover key" => "Pushover key", // Mail notifications "Notification mail low temperature" => "Warning: Temperature is low!

    Sensor: %%sensor%%
    Temperature is %%value%% °", @@ -135,6 +171,8 @@ "Off" => "Off", "Groups" => "Groups", "Devices" => "Devices", + "Scenes" => "Scenes", + "Scene" => "Scene", @@ -186,7 +224,8 @@ // Time (ago) "since" => "since", - + "right now" => "right now", + "secound" => "secound", "minute" => "minute", "hour" => "hour", @@ -203,6 +242,7 @@ "months" => "months", "years" => "years", + ); ?> \ No newline at end of file diff --git a/lib/packages/bootstrap-editable-1.4.4/CHANGELOG.txt b/lib/packages/bootstrap-editable-1.4.4/CHANGELOG.txt new file mode 100644 index 0000000..3c86e5e --- /dev/null +++ b/lib/packages/bootstrap-editable-1.4.4/CHANGELOG.txt @@ -0,0 +1,181 @@ +X-editable changelog +============================= + + +Version 1.4.4 May 4, 2013 +---------------------------- +[enh #219] added `error` callback (joekaiser) +[enh #198] new value of showbuttons: 'bottom' (vitalets) +[enh #192] add class editable-popup to have diferent css for popup and inline (vitalets) +[enh] update to bootstrap-datepicker 1.0.2 (vitalets) +[enh] update to combodate 1.0.3 with yearDescending and roundTime options (vitalets) +[enh] add 'use strict' directive (vitalets) +[enh #202] allow pk=0 (mdeweerd) +[enh #183] move datepicker icon to center of button (vitalets) +[enh] upgrade to select2 3.3.2 (vitalets) +[enh #176] update to bootstrap 2.3.1 (vitalets) +[bug #171] clear in date & datetime when showbuttons=false (vitalets) +[bug #166] clear button for input type=number (vitalets) +[bug #65] checklist don't show checked for single value (vitalets) +[enh #188] added bootstrap datetime (adeg, vitalets) +[bug] editable-poshytip on inline mode tries to write in $.Poshytip (vitalets) + + +Version 1.4.3 Mar 8, 2013 +---------------------------- +[bug #32] hotfix for jQuery UI 1.9+ (vitalets) + + +Version 1.4.2 Mar 7, 2013 +---------------------------- +[enh #132] combodate options can be defined via data-combodate json string (vitalets) +[enh] source defined as function now has scope of element and can return string used as url (vitalets) +[bug #99] select2 with Hierarchical Data (kev360) +[bug #81] wysihtml5: fix inserting image (vitalets) +[bug] remove $.browser from wysihtml5 input to support jQuery 1.9 (vitalets) +[bug #142] editable poshytip jquery 1.9+ compatibility (spiderpug) +[enh #126] Update bootstrap datepicker library and add minViewMode to options (kev360) +[enh #150] select2 with showbuttons = false (vitalets) +[bug #149] datepicker not shown when showbuttons = false (vitalets) +[bug #133] clear button incorect position due to parent line-height property (vitalets) +[bug #141] data-value ignored for empty elements (vitalets) +[bug #137] fix empty class for delegated element (vitalets) +[enh #121] add support of momentjs 2.0.0 in combodate (vitalets) + + +Version 1.4.1 Jan 18, 2013 +---------------------------- +[enh #62] new option `selector` to work with delegated targets (vitalets) +[enh] new option `unsavedclass` to set css class when value was not sent to server (vitalets) +[enh] new option `emptyclass` to set css class when element is empty (vitalets) +[enh #59] select2 input (vitalets) +[enh #17] typeahead input (vitalets) +[enh] select: support of OPTGROUP via `children` key in source (vitalets) +[enh] checklist: set checked via prop instead of attr (vitalets) + + +Version 1.4.0 Jan 11, 2013 +---------------------------- +[enh] added new input type: combodate (vitalets) +[bug #68] allow arrays for data attributes (adimitrov) +[enh] setValue method updates input if form is open (vitalets) +[enh] select: change source via option method, see #61 (vitalets) +[bug] select: source loaded twice if sourceCache = false (vitalets) +[enh] added `destroy` method, see #61 (vitalets) +[enh] textarea: added `rows` property (vitalets) +[enh #60] added wysihtml5 input (vitalets) +[enh] added IOS-style clear button for text inputs (vitalets) +[enh] date inputs changed in inline mode (vitalets) +[enh #51] popup/inline modes can be toggled via `mode` config option. No more *-inline.js versions of files (vitalets) +[enh] update bootstrap-datepicker to upstream (vitalets) +[enh] 'display' method: added param 'response' allowing to show text directly from server (vitalets) +[enh] new util method `$.fn.editableutils.itemsByValue` to easily get selected items for sourced-inputs (vitalets) +[enh] convert newlines to
    in error message for more pretty display (vitalets) +[enh #57] remove css height for textarea (vitalets) +[enh] if new value for select is 'null' source should not load (vitalets) +[enh #53] 'name' no more appended to source defined as url (vitalets) +[enh #46] move 'img' dir outside 'css' (vitalets) +[enh #48] fix handling of newlines in textarea input (jmfontaine) +[enh #47] set select source to function (brianchance) +[bug] fix inline container move on next line in IE7 (vitalets) + + +Version 1.3.0 Dec 10, 2012 +---------------------------- +[enh] added html5 inputs support: password, email, url, tel, number, range (vitalets) +[bug #43] fix for bootstrap 2.2.2 (vitalets) +[enh #41] 'abstract' class renamed to 'abstractinput' as abstract is reserved word (vitalets) +[enh #40] 'params' option defined as function overwrites original ajax data instead of appending (vitalets) +[bug] datepicker: error when click on arrows after clear date (vitalets) +[enh] 'hidden' event: added possible value of reason param - 'nochange'. Occurs when form is submitted but value was not changed (vitalets) +[enh] 'submit' method changed: error-callback's parameter simplified (vitalets) +[enh] 'submit' method changed: now when response 200 OK it does not set pk automatically (vitalets) +[enh] 'submit' method changed: removed dataType='json'. Use 'ajaxOptions' to specify dataType if needed (vitalets) +[enh] removed default ajax dataType='json'. Use 'ajaxOptions' to specify dataType if needed (vitalets) +[enh] select: do not show 'sourceError' in element during autotext execution (vitalets) + + +Version 1.2.0 Dec 6, 2012 +---------------------------- +[enh #36] 'submit' method: added 'ajaxOptions' property to modify ajax request (vitalets) +[enh] inputs now internally use 'value2submit' method instead of previous 'value2str' (vitalets) +[enh] editableContainer removed from docs (vitalets) +[enh] editableContainer: removed 'autohide' option and 'cancel' event. Use 'hidden' event instead (vitalets) +[enh] 'hidden' event: added param 'reason' that points to reason caused hiding (vitalets) +[enh] 'select' submit by enter (vitalets) +[bug #37] fix incorrectly shown datepicker in jquery 1.7.1 + webkit (vitalets) +[enh] added url param 'jquery' to run tests in different versions of jquery, e.g. '&jquery=1.7.2' (vitalets) +[enh] 'enablefocus' option removed. More efficient to use 'save/hide' events to set focus to any element (vitalets) +[enh] 'init' event was added due to removal of render event (vitalets) +[enh] 'render' event was removed, use 'display' callback instead (vitalets) +[enh] 'checklist' submit value as array, not comma separated string (vitalets) +[enh] 'checklist' was refactored: options 'viewseparator', 'limit', 'limitText' are supressed by 'display' callback (vitalets) +[enh] new option: 'display' callback. Makes far more flexible rendering value into element's text. (vitalets) +[bug] fix typos (atrophic) +[enh] all callbacks scope changed to element (vitalets) +[enh] new option: 'savenochange' to save or cancel value when it was not changed in form (vitalets) +[enh] composite pk can be defined as JSON in data-pk attribute (vitalets) +[enh #30] new option 'sourceCache' true|false to disable cache for select (vitalets) +[bug #34] inputclass span* broken with fluid bootstrap layout. Classes changed to 'input-*'. (vitalets) +[enh] utils now added to $.fn.editableutils instead of $.fn.editableform.utils (vitalets) +[enh] input types now added to $.fn.editabletypes instead of $.fn.editableform.types (vitalets) +[enh] playground and tests now use requirejs (vitalets) +[bug #27] 'today' button toggle bug in bootstrap-datepicker (vitalets) + + +Version 1.1.1 Nov 30, 2012 +---------------------------- +[enh] 'showbuttons' option to hide buttons in form (vitalets) +[enh] object can be passed in 'option' method to set several options at once (vitalets) +[enh #20] toggle editable by 'dblclick' and 'mouseenter' (vitalets) +[enh] added 'inputs-ext' directory with sample input 'address'. They will not be concatenated to main files (vitalets) +[enh #13] 'onblur' option: to cancel, submit or ignore when user clicks outside the form (vitalets) +[enh] 'ajaxOptions' parameter for advanced ajax configuration (vitalets) +[enh] 'success' callback can return object to overwrite submitted value (vitalets) + + +Version 1.1.0 Nov 27, 2012 +---------------------------- +[enh #11] icon cancel changed to 'cross' (tarciozemel) +[enh] added support for IE7+ (vitalets) +[enh #9] 'name' or 'id' is not required anymore (vitalets) +[enh] 'clear' button added in date and dateui (vitalets) +[enh] form template changed: added DIV.editable-input, DIV.editable.buttons and $.fn.editableform.buttons (vitalets) +[enh] new input type: checklist (vitalets) +[enh] updated docs: inputs dropdown menu, global templates section (vitalets) + + +Version 1.0.1 Nov 22, 2012 +---------------------------- +[enh] contribution guide in README.md (vitalets) +[enh #7] 'shown', 'hidden' events added (vitalets) +[enh #1] params can be a function to calculate it dynamically (vitalets) +[enh #6] do not preventDefault() in click when toggle='manual'. This allows to have clickable links (vitalets) +[bug #3] should not mark element with unsave css if url is user's function (vitalets) + + +Version 1.0.0 Nov 19, 2012 +---------------------------- +Initial release. This library is new life of bootstrap-editable (1.1.4) that was strongly refactored and improved. +Main features: +- support not only bootstrap but any core library: bootstrap, jquery-ui or pure jquery +- different container classes to show form: popover, tooltip, poshytip, etc +- inline and popup versions +- new directory structure and logic in separate js files allowing easy contribution + +It is not fully compatible with bootstrap-editable but has mainly the same interface and api. +Here list of differences to help you to upgrade your application: + +[change] 'toggle' option value can be only click|manual (not toggling element id). In case of 'manual' you should write handler calling 'show' method. +[change] 'validate' option cannot be defined as object anymore. +[change] events 'init', 'update', 'shown', 'hidden' removed. Events 'save', 'cancel' added. Event 'render' remains. +[change] input's option 'template' renamed to 'tpl' (to exclude conflict with container's template). +[change] value can be stored internally as object (previously was always string). Useful for date input. +[change] 'error' callback option is removed. 'success' callback remained. +[enh] 'source' option in select can be array of structure [{value: 1, text: 'abc'}, {...}]. This allows to keep ordering of items in dropdown list. Previous format is supported for compatibility. +[enh] api method 'setValue' to set manually value of editable. +[change] locales directory is excluded from bootstrap-datepicker input. If you need localization you should jus download corresponding file from github. +[change] date and dateui specific options can be set only via 'datepicker' option in first level of config (previously it was possible to set some options directly in config, e.g. weekStart). +[change] if 'url' option defined as function - it is used as submit method instead of ajax (previously it was dynamically return url string and ajax occurred anyway) + +Also all known bugs of bootstrap-editable were closed. diff --git a/lib/packages/bootstrap-editable-1.4.4/LICENSE-MIT b/lib/packages/bootstrap-editable-1.4.4/LICENSE-MIT new file mode 100644 index 0000000..eeb9357 --- /dev/null +++ b/lib/packages/bootstrap-editable-1.4.4/LICENSE-MIT @@ -0,0 +1,22 @@ +Copyright (c) 2012 Vitaliy Potapov + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/packages/bootstrap-editable-1.4.4/README.md b/lib/packages/bootstrap-editable-1.4.4/README.md new file mode 100644 index 0000000..18f0d7e --- /dev/null +++ b/lib/packages/bootstrap-editable-1.4.4/README.md @@ -0,0 +1,71 @@ +# X-editable + +In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery. +It is a new life of [bootstrap-editable plugin](http://github.com/vitalets/bootstrap-editable) that was strongly refactored and improved. + +## Demo + Docs + Download +See **http://vitalets.github.com/x-editable** + +## Reporting issues +When creating issues please provide [jsFiddle](http://jsfiddle.net) example. You can easily fork one of **jsFiddle templates**: +1. [bootstrap](http://jsfiddle.net/xBB5x/195) +2. [jqueryui](http://jsfiddle.net/xBB5x/196) +3. [plain](http://jsfiddle.net/xBB5x/197) +Your feedback is very appreciated! + +## Contribution +A few steps how to start contributing: + +1.[Fork X-editable](https://github.com/vitalets/x-editable/fork) and pull the latest changes from dev branch + +2.Arrange local directory structure. It should be: +**x-editable** + | -- **lib** (repo related to dev and master branches) + | -- **gh-pages** (repo related to gh-pages branch for docs & demo) + | -- **playground** (simple node-server and html page for testing, [playground_1.2.zip](https://github.com/downloads/vitalets/x-editable/playground_1.2.zip), **updated in 1.2.0!**) + +To make it easy follow this script ( _assuming you have [nodejs](http://nodejs.org) installed_ ). +Please replace <your-github-name> with your name: +```` +mkdir x-editable +cd x-editable + +#lib +git clone https://github.com//x-editable.git -b dev lib +cd lib +#install gruntjs globally - building tool +npm install -g grunt +#install other dependencies - grunt-contrib +npm install +cd .. + +#gh-pages +git clone https://github.com//x-editable.git -b gh-pages gh-pages +cd gh-pages +npm install +cd .. + +#playground +#download playground.zip from https://github.com/downloads/vitalets/x-editable/playground_1.2.zip +unzip playground.zip +cd playground +npm install +```` +3.That's it! You can start editing files in **lib/src** directory or create new editable input/container/whatever. +To test the result go to **playground**, start server node server.js and open in your browser [http://localhost:3000/playground](http://localhost:3000/playground). + +4.To run unit tests you can open it directly in browser **lib/test/index.html**. +Or use grunt's _qunit_ task grunt test. For that you also need to [install PhantomJS](https://github.com/gruntjs/grunt/blob/master/docs/faq.md#why-does-grunt-complain-that-phantomjs-isnt-installed) + +5.To build distributive run grunt build in **lib** directory. Result will appear in **lib/dist**. + +6.To build docs run build data-docs-dist in **gh-pages** directory. Result will appear in **gh-pages/*.html**. +Do not edit **index.html** and **docs.html** directly! Instead look at [Handlebars](https://github.com/wycats/handlebars.js) templates in **generator/templates**. + +7.Commit changes on dev / gh-pages-dev branch and make pull request as usual. + +Thanks for your support! + +## License +Copyright (c) 2012 Vitaliy Potapov +Licensed under the MIT licenses. \ No newline at end of file diff --git a/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/css/bootstrap-editable.css b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/css/bootstrap-editable.css new file mode 100644 index 0000000..3a3d72e --- /dev/null +++ b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/css/bootstrap-editable.css @@ -0,0 +1,495 @@ +/*! X-editable - v1.4.4 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ + +.editableform { + margin-bottom: 0; /* overwrites bootstrap margin */ +} + +.editableform .control-group { + margin-bottom: 0; /* overwrites bootstrap margin */ + white-space: nowrap; /* prevent wrapping buttons on new line */ + line-height: 20px; /* overwriting bootstrap line-height. See #133 */ +} + +.editable-buttons { + display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ + vertical-align: top; + margin-left: 7px; + /* inline-block emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-buttons.editable-buttons-bottom { + display: block; + margin-top: 7px; + margin-left: 0; +} + +.editable-input { + vertical-align: top; + display: inline-block; /* should be inline to take effect of parent's white-space: nowrap */ + width: auto; /* bootstrap-responsive has width: 100% that breakes layout */ + white-space: normal; /* reset white-space decalred in parent*/ + /* display-inline emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-buttons .editable-cancel { + margin-left: 7px; +} + +/*for jquery-ui buttons need set height to look more pretty*/ +.editable-buttons button.ui-button-icon-only { + height: 24px; + width: 30px; +} + +.editableform-loading { + background: url('../img/loading.gif') center center no-repeat; + height: 25px; + width: auto; + min-width: 25px; +} + +.editable-inline .editableform-loading { + background-position: left 5px; +} + + .editable-error-block { + max-width: 300px; + margin: 5px 0 0 0; + width: auto; + white-space: normal; +} + +/*add padding for jquery ui*/ +.editable-error-block.ui-state-error { + padding: 3px; +} + +.editable-error { + color: red; +} + +/* ---- For specific types ---- */ + +.editableform .editable-date { + padding: 0; + margin: 0; + float: left; +} + +/* move datepicker icon to center of add-on button. See https://github.com/vitalets/x-editable/issues/183 */ +.editable-inline .add-on .icon-th { + margin-top: 3px; + margin-left: 1px; +} + + +/* checklist vertical alignment */ +.editable-checklist label input[type="checkbox"], +.editable-checklist label span { + vertical-align: middle; + margin: 0; +} + +.editable-checklist label { + white-space: nowrap; +} + +/* set exact width of textarea to fit buttons toolbar */ +.editable-wysihtml5 { + width: 566px; + height: 250px; +} + +/* clear button shown as link in date inputs */ +.editable-clear { + clear: both; + font-size: 0.9em; + text-decoration: none; + text-align: right; +} + +/* IOS-style clear button for text inputs */ +.editable-clear-x { + background: url('../img/clear.png') center center no-repeat; + display: block; + width: 13px; + height: 13px; + position: absolute; + opacity: 0.6; + z-index: 100; + + top: 50%; + right: 6px; + margin-top: -6px; + +} + +.editable-clear-x:hover { + opacity: 1; +} +.editable-container.editable-popup { + max-width: none !important; /* without this rule poshytip/tooltip does not stretch */ +} + +.editable-container.popover { + width: auto; /* without this rule popover does not stretch */ +} + +.editable-container.editable-inline { + display: inline-block; + vertical-align: middle; + width: auto; + /* inline-block emulation for IE7*/ + zoom: 1; + *display: inline; +} + +.editable-container.ui-widget { + font-size: inherit; /* jqueryui widget font 1.1em too big, overwrite it */ +} +.editable-click, +a.editable-click, +a.editable-click:hover { + text-decoration: none; + border-bottom: dashed 1px #0088cc; +} + +.editable-click.editable-disabled, +a.editable-click.editable-disabled, +a.editable-click.editable-disabled:hover { + color: #585858; + cursor: default; + border-bottom: none; +} + +.editable-empty, .editable-empty:hover, .editable-empty:focus{ + font-style: italic; + color: #DD1144; + /* border-bottom: none; */ + text-decoration: none; +} + +.editable-unsaved { + font-weight: bold; +} + +.editable-unsaved:after { +/* content: '*'*/ +} + +/*see https://github.com/vitalets/x-editable/issues/139 */ +.form-horizontal .editable +{ + padding-top: 5px; + display:inline-block; +} + + +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Improvements by Andrew Rowls + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + */ +.datepicker { + padding: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + direction: ltr; + /*.dow { + border-top: 1px solid #ddd !important; + }*/ + +} +.datepicker-inline { + width: 220px; +} +.datepicker.datepicker-rtl { + direction: rtl; +} +.datepicker.datepicker-rtl table tr td span { + float: right; +} +.datepicker-dropdown { + top: 0; + left: 0; +} +.datepicker-dropdown:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 6px; +} +.datepicker-dropdown:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 7px; +} +.datepicker > div { + display: none; +} +.datepicker.days div.datepicker-days { + display: block; +} +.datepicker.months div.datepicker-months { + display: block; +} +.datepicker.years div.datepicker-years { + display: block; +} +.datepicker table { + margin: 0; +} +.datepicker td, +.datepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: none; +} +.table-striped .datepicker table tr td, +.table-striped .datepicker table tr th { + background-color: transparent; +} +.datepicker table tr td.day:hover { + background: #eeeeee; + cursor: pointer; +} +.datepicker table tr td.old, +.datepicker table tr td.new { + color: #999999; +} +.datepicker table tr td.disabled, +.datepicker table tr td.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #fde19a; + background-image: -moz-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -ms-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fdd49a), to(#fdf59a)); + background-image: -webkit-linear-gradient(top, #fdd49a, #fdf59a); + background-image: -o-linear-gradient(top, #fdd49a, #fdf59a); + background-image: linear-gradient(top, #fdd49a, #fdf59a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0); + border-color: #fdf59a #fdf59a #fbed50; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #000 !important; +} +.datepicker table tr td.today:hover, +.datepicker table tr td.today:hover:hover, +.datepicker table tr td.today.disabled:hover, +.datepicker table tr td.today.disabled:hover:hover, +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today:hover.disabled, +.datepicker table tr td.today.disabled.disabled, +.datepicker table tr td.today.disabled:hover.disabled, +.datepicker table tr td.today[disabled], +.datepicker table tr td.today:hover[disabled], +.datepicker table tr td.today.disabled[disabled], +.datepicker table tr td.today.disabled:hover[disabled] { + background-color: #fdf59a; +} +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active { + background-color: #fbf069 \9; +} +.datepicker table tr td.active, +.datepicker table tr td.active:hover, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active:hover.disabled, +.datepicker table tr td.active.disabled.disabled, +.datepicker table tr td.active.disabled:hover.disabled, +.datepicker table tr td.active[disabled], +.datepicker table tr td.active:hover[disabled], +.datepicker table tr td.active.disabled[disabled], +.datepicker table tr td.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span { + display: block; + width: 23%; + height: 54px; + line-height: 54px; + float: left; + margin: 1%; + cursor: pointer; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.datepicker table tr td span:hover { + background: #eeeeee; +} +.datepicker table tr td span.disabled, +.datepicker table tr td span.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td span.active, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active.disabled:hover { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active:hover.disabled, +.datepicker table tr td span.active.disabled.disabled, +.datepicker table tr td span.active.disabled:hover.disabled, +.datepicker table tr td span.active[disabled], +.datepicker table tr td span.active:hover[disabled], +.datepicker table tr td span.active.disabled[disabled], +.datepicker table tr td span.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span.old { + color: #999999; +} +.datepicker th.switch { + width: 145px; +} +.datepicker thead tr:first-child th, +.datepicker tfoot tr:first-child th { + cursor: pointer; +} +.datepicker thead tr:first-child th:hover, +.datepicker tfoot tr:first-child th:hover { + background: #eeeeee; +} +.datepicker .cw { + font-size: 10px; + width: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; +} +.datepicker thead tr:first-child th.cw { + cursor: default; + background-color: transparent; +} +.input-append.date .add-on i, +.input-prepend.date .add-on i { + display: block; + cursor: pointer; + width: 16px; + height: 16px; +} diff --git a/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/clear.png b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/clear.png new file mode 100644 index 0000000000000000000000000000000000000000..580b52a5be8a644f826def0c7ed6a13f90c0915c GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4u6ByT*@`3|I*lDyqr82-2SpV<%OaTa()76WMy zFm^kcZ3hx8D{xE)(qO#|GLJ6IVqjoo^>lFzk+^JnVSiR|B17B9``_m-NuMxfR?C!* zh07MrSo)3sr09zP_wDzlhX=fPF>UDu=?vANf(w(JrZ%)>D41|8J9+Zugm01epVrPx zINBDzitE|2b$6T`9`!DJFmdU=eKrTz=_*cb3=s8r9VQ%8yxBf7d%v=5Nxh^7ZP6=ia-yr`GWA z@1JRG_RM@X%BIyHqImbIN_g6wl?zIFvhME$`)4a}gbAnqdolZft=%U7gPsvQH z#I2z#?2;8wgCxj?;QX|b^2DN42FH~Aq*MjZ+{Ezopr E0Crl)MgRZ+ literal 0 HcmV?d00001 diff --git a/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/loading.gif b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/img/loading.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b33f7e54f4e55b6b8774d86d96895db9af044b4 GIT binary patch literal 1849 zcma*odr(tX9tZI2z31lM+(&YVk%mZ}5P~KlG2s=WSbGzm0!x7^P##Mnh7t-jP!X0Q zk_SQ}Po-L1tlDK;6l?(>v)e5ZBQx4|Y-Q?nr@Px3?9h(3ZWr3^tj=`TP57gKr87N$ zp2wWee1GRRCwo_xahnw)5cxNPJbCg2L6DV|6`#+yw6v6!mDS$f9-JvFD^n;GQ&UrZ zzh5jCkByB101O60U0q#p_1BM>Cv-vP?&s4@g_((4_1L=L$(a91)0=J91Gas#R{McE znYG^9*0A5YZ>#;~+Wkn(W5B0^yELIYLP!K}mB~<)AM@1&nqekynuaEGqPrzoH|KodRXJy)%+w_fu3nE5>@Bd_b zqC$EQ;{c`T&?EsNO|igL9gC7Ygxv?aQUEXMq?~>wg{EyW;VcJ37CUF#HjrT=KQO_* zS>M9yydXk18D(+QDJ1>r);Lav_uYKp$T?4vr{Q$lTo&pKv^?(>L-)G2*lwH!Ah7k? z7oH<8h-(KTKt5V6$8gF)C7Io&P5=SjTh)=zV=E2EUhQZP##L8S{d%UK>>+y82>+FV+#^BzW7u3F)Bb>=lYQ%%j`F>ASe zo*cw@V#u6T`A2He;70mR(V&iV&-7{qP~=SRf&jm9-T{*ZeZ}$rd0#6c&fLG^xJcf5 z+p<`wJYgW+_s*V{uI$nMB;%8`S_3>PfGOj3Rq}@Cx^+j?rk92fANSFDBYnOqQ>Vdj z)(|$AhP4t&Lb=Gvo2#3Gl%9<=Gv`Mz?Po@P4iLF!x}GUWJICDlFk-hS^Whyh7x~VH z@0vD1>HYD4&e+~yzS*-sFR{9`{QEEZO1zg7>R&7cHts-6j!xHVdA8eI+ZlVzd%`es zJT@$#GX(gvCJ1oJN%yLBK}{V=V;seo;!w|Yte!W1%5qLNFWqvZW>h&IiH+oPT=b@E zPhGzv5=(Un*X>v`>%8h_nj^NdYcE6NHS_ifkCV$*D)Tqrbu`s;<=t<4 zAHNqNV?6(g<1PY-w@#I-WYFViz?9TrkMr)u0g`O`u|>T;k|2sV*YF^punvT;$SuTy{j3Gv)yqD!R_CF>yR)MzmmYS5v+~R zXAdD%ng9?df;wd8GxR#%3O+gz};Vo;)sK%Bj-q>Oq%R7JU-KD?vYu>#2UjaDo z&8$>5xW~?KPD_#XFToU1hIb*VOMidUr6iYiO0N|i-7s`T8!cFT`rN!^1Pt78J93i6 z5HI1wIM$94m{3SLDvISDe6$ZG1;eq_D9RTaaC>=cO{@Bs>$IlPCPJJ$h$)-3vzNUQ6OsN#_zWxey!_9%hxwH2_dEJi=yY|1c7nDm2_Lm!Cof8-R_+9UkS zcBE(o47yE)oMR(Q=dp1a2wTX5KvvGyLqlWTa7V&!A*|w|)ax~1_~aJ0=_Lilg*0iQk7#ZD EAHN$8j{pDw literal 0 HcmV?d00001 diff --git a/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/js/bootstrap-editable.js b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/js/bootstrap-editable.js new file mode 100644 index 0000000..6f6ac82 --- /dev/null +++ b/lib/packages/bootstrap-editable-1.4.4/bootstrap-editable/js/bootstrap-editable.js @@ -0,0 +1,6192 @@ +/*! X-editable - v1.4.4 +* In-place editing with Twitter Bootstrap, jQuery UI or pure jQuery +* http://github.com/vitalets/x-editable +* Copyright (c) 2013 Vitaliy Potapov; Licensed MIT */ + +/** +Form with single input element, two buttons and two states: normal/loading. +Applied as jQuery method to DIV tag (not to form tag!). This is because form can be in loading state when spinner shown. +Editableform is linked with one of input types, e.g. 'text', 'select' etc. + +@class editableform +@uses text +@uses textarea +**/ +(function ($) { + "use strict"; + + var EditableForm = function (div, options) { + this.options = $.extend({}, $.fn.editableform.defaults, options); + this.$div = $(div); //div, containing form. Not form tag. Not editable-element. + if(!this.options.scope) { + this.options.scope = this; + } + //nothing shown after init + }; + + EditableForm.prototype = { + constructor: EditableForm, + initInput: function() { //called once + //take input from options (as it is created in editable-element) + this.input = this.options.input; + + //set initial value + //todo: may be add check: typeof str === 'string' ? + this.value = this.input.str2value(this.options.value); + }, + initTemplate: function() { + this.$form = $($.fn.editableform.template); + }, + initButtons: function() { + var $btn = this.$form.find('.editable-buttons'); + $btn.append($.fn.editableform.buttons); + if(this.options.showbuttons === 'bottom') { + $btn.addClass('editable-buttons-bottom'); + } + }, + /** + Renders editableform + + @method render + **/ + render: function() { + //init loader + this.$loading = $($.fn.editableform.loading); + this.$div.empty().append(this.$loading); + + //init form template and buttons + this.initTemplate(); + if(this.options.showbuttons) { + this.initButtons(); + } else { + this.$form.find('.editable-buttons').remove(); + } + + //show loading state + this.showLoading(); + + /** + Fired when rendering starts + @event rendering + @param {Object} event event object + **/ + this.$div.triggerHandler('rendering'); + + //init input + this.initInput(); + + //append input to form + this.input.prerender(); + this.$form.find('div.editable-input').append(this.input.$tpl); + + //append form to container + this.$div.append(this.$form); + + //render input + $.when(this.input.render()) + .then($.proxy(function () { + //setup input to submit automatically when no buttons shown + if(!this.options.showbuttons) { + this.input.autosubmit(); + } + + //attach 'cancel' handler + this.$form.find('.editable-cancel').click($.proxy(this.cancel, this)); + + if(this.input.error) { + this.error(this.input.error); + this.$form.find('.editable-submit').attr('disabled', true); + this.input.$input.attr('disabled', true); + //prevent form from submitting + this.$form.submit(function(e){ e.preventDefault(); }); + } else { + this.error(false); + this.input.$input.removeAttr('disabled'); + this.$form.find('.editable-submit').removeAttr('disabled'); + this.input.value2input(this.value); + //attach submit handler + this.$form.submit($.proxy(this.submit, this)); + } + + /** + Fired when form is rendered + @event rendered + @param {Object} event event object + **/ + this.$div.triggerHandler('rendered'); + + this.showForm(); + + //call postrender method to perform actions required visibility of form + if(this.input.postrender) { + this.input.postrender(); + } + }, this)); + }, + cancel: function() { + /** + Fired when form was cancelled by user + @event cancel + @param {Object} event event object + **/ + this.$div.triggerHandler('cancel'); + }, + showLoading: function() { + var w, h; + if(this.$form) { + //set loading size equal to form + w = this.$form.outerWidth(); + h = this.$form.outerHeight(); + if(w) { + this.$loading.width(w); + } + if(h) { + this.$loading.height(h); + } + this.$form.hide(); + } else { + //stretch loading to fill container width + w = this.$loading.parent().width(); + if(w) { + this.$loading.width(w); + } + } + this.$loading.show(); + }, + + showForm: function(activate) { + this.$loading.hide(); + this.$form.show(); + if(activate !== false) { + this.input.activate(); + } + /** + Fired when form is shown + @event show + @param {Object} event event object + **/ + this.$div.triggerHandler('show'); + }, + + error: function(msg) { + var $group = this.$form.find('.control-group'), + $block = this.$form.find('.editable-error-block'), + lines; + + if(msg === false) { + $group.removeClass($.fn.editableform.errorGroupClass); + $block.removeClass($.fn.editableform.errorBlockClass).empty().hide(); + } else { + //convert newline to
    for more pretty error display + if(msg) { + lines = msg.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('
    ').text(lines[i]).html(); + } + msg = lines.join('
    '); + } + $group.addClass($.fn.editableform.errorGroupClass); + $block.addClass($.fn.editableform.errorBlockClass).html(msg).show(); + } + }, + + submit: function(e) { + e.stopPropagation(); + e.preventDefault(); + + var error, + newValue = this.input.input2value(); //get new value from input + + //validation + if (error = this.validate(newValue)) { + this.error(error); + this.showForm(); + return; + } + + //if value not changed --> trigger 'nochange' event and return + /*jslint eqeq: true*/ + if (!this.options.savenochange && this.input.value2str(newValue) == this.input.value2str(this.value)) { + /*jslint eqeq: false*/ + /** + Fired when value not changed but form is submitted. Requires savenochange = false. + @event nochange + @param {Object} event event object + **/ + this.$div.triggerHandler('nochange'); + return; + } + + //sending data to server + $.when(this.save(newValue)) + .done($.proxy(function(response) { + //run success callback + var res = typeof this.options.success === 'function' ? this.options.success.call(this.options.scope, response, newValue) : null; + + //if success callback returns false --> keep form open and do not activate input + if(res === false) { + this.error(false); + this.showForm(false); + return; + } + + //if success callback returns string --> keep form open, show error and activate input + if(typeof res === 'string') { + this.error(res); + this.showForm(); + return; + } + + //if success callback returns object like {newValue: } --> use that value instead of submitted + //it is usefull if you want to chnage value in url-function + if(res && typeof res === 'object' && res.hasOwnProperty('newValue')) { + newValue = res.newValue; + } + + //clear error message + this.error(false); + this.value = newValue; + /** + Fired when form is submitted + @event save + @param {Object} event event object + @param {Object} params additional params + @param {mixed} params.newValue submitted value + @param {Object} params.response ajax response + + @example + $('#form-div').on('save'), function(e, params){ + if(params.newValue === 'username') {...} + }); + **/ + this.$div.triggerHandler('save', {newValue: newValue, response: response}); + }, this)) + .fail($.proxy(function(xhr) { + var msg; + if(typeof this.options.error === 'function') { + msg = this.options.error.call(this.options.scope, xhr, newValue); + } else { + msg = typeof xhr === 'string' ? xhr : xhr.responseText || xhr.statusText || 'Unknown error!'; + } + + this.error(msg); + this.showForm(); + }, this)); + }, + + save: function(newValue) { + //convert value for submitting to server + var submitValue = this.input.value2submit(newValue); + + //try parse composite pk defined as json string in data-pk + this.options.pk = $.fn.editableutils.tryParseJson(this.options.pk, true); + + var pk = (typeof this.options.pk === 'function') ? this.options.pk.call(this.options.scope) : this.options.pk, + send = !!(typeof this.options.url === 'function' || (this.options.url && ((this.options.send === 'always') || (this.options.send === 'auto' && pk !== null && pk !== undefined)))), + params; + + if (send) { //send to server + this.showLoading(); + + //standard params + params = { + name: this.options.name || '', + value: submitValue, + pk: pk + }; + + //additional params + if(typeof this.options.params === 'function') { + params = this.options.params.call(this.options.scope, params); + } else { + //try parse json in single quotes (from data-params attribute) + this.options.params = $.fn.editableutils.tryParseJson(this.options.params, true); + $.extend(params, this.options.params); + } + + if(typeof this.options.url === 'function') { //user's function + return this.options.url.call(this.options.scope, params); + } else { + //send ajax to server and return deferred object + return $.ajax($.extend({ + url : this.options.url, + data : params, + type : 'POST' + }, this.options.ajaxOptions)); + } + } + }, + + validate: function (value) { + if (value === undefined) { + value = this.value; + } + if (typeof this.options.validate === 'function') { + return this.options.validate.call(this.options.scope, value); + } + }, + + option: function(key, value) { + if(key in this.options) { + this.options[key] = value; + } + + if(key === 'value') { + this.setValue(value); + } + + //do not pass option to input as it is passed in editable-element + }, + + setValue: function(value, convertStr) { + if(convertStr) { + this.value = this.input.str2value(value); + } else { + this.value = value; + } + + //if form is visible, update input + if(this.$form && this.$form.is(':visible')) { + this.input.value2input(this.value); + } + } + }; + + /* + Initialize editableform. Applied to jQuery object. + + @method $().editableform(options) + @params {Object} options + @example + var $form = $('<div>').editableform({ + type: 'text', + name: 'username', + url: '/post', + value: 'vitaliy' + }); + + //to display form you should call 'render' method + $form.editableform('render'); + */ + $.fn.editableform = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + data = $this.data('editableform'), + options = typeof option === 'object' && option; + if (!data) { + $this.data('editableform', (data = new EditableForm(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //keep link to constructor to allow inheritance + $.fn.editableform.Constructor = EditableForm; + + //defaults + $.fn.editableform.defaults = { + /* see also defaults for input */ + + /** + Type of input. Can be text|textarea|select|date|checklist + + @property type + @type string + @default 'text' + **/ + type: 'text', + /** + Url for submit, e.g. '/post' + If function - it will be called instead of ajax. Function should return deferred object to run fail/done callbacks. + + @property url + @type string|function + @default null + @example + url: function(params) { + var d = new $.Deferred; + if(params.value === 'abc') { + return d.reject('error message'); //returning error via deferred object + } else { + //async saving data in js model + someModel.asyncSaveMethod({ + ..., + success: function(){ + d.resolve(); + } + }); + return d.promise(); + } + } + **/ + url:null, + /** + Additional params for submit. If defined as object - it is **appended** to original ajax data (pk, name and value). + If defined as function - returned object **overwrites** original ajax data. + @example + params: function(params) { + //originally params contain pk, name and value + params.a = 1; + return params; + } + + @property params + @type object|function + @default null + **/ + params:null, + /** + Name of field. Will be submitted on server. Can be taken from id attribute + + @property name + @type string + @default null + **/ + name: null, + /** + Primary key of editable object (e.g. record id in database). For composite keys use object, e.g. {id: 1, lang: 'en'}. + Can be calculated dynamically via function. + + @property pk + @type string|object|function + @default null + **/ + pk: null, + /** + Initial value. If not defined - will be taken from element's content. + For __select__ type should be defined (as it is ID of shown text). + + @property value + @type string|object + @default null + **/ + value: null, + /** + Strategy for sending data on server. Can be auto|always|never. + When 'auto' data will be sent on server only if pk defined, otherwise new value will be stored in element. + + @property send + @type string + @default 'auto' + **/ + send: 'auto', + /** + Function for client-side validation. If returns string - means validation not passed and string showed as error. + + @property validate + @type function + @default null + @example + validate: function(value) { + if($.trim(value) == '') { + return 'This field is required'; + } + } + **/ + validate: null, + /** + Success callback. Called when value successfully sent on server and **response status = 200**. + Usefull to work with json response. For example, if your backend response can be {success: true} + or {success: false, msg: "server error"} you can check it inside this callback. + If it returns **string** - means error occured and string is shown as error message. + If it returns **object like** {newValue: <something>} - it overwrites value, submitted by user. + Otherwise newValue simply rendered into element. + + @property success + @type function + @default null + @example + success: function(response, newValue) { + if(!response.success) return response.msg; + } + **/ + success: null, + /** + Error callback. Called when request failed (response status != 200). + Usefull when you want to parse error response and display a custom message. + Must return **string** - the message to be displayed in the error block. + + @property error + @type function + @default null + @since 1.4.4 + @example + error: function(response, newValue) { + if(response.status === 500) { + return 'Service unavailable. Please try later.'; + } else { + return response.responseText; + } + } + **/ + error: null, + /** + Additional options for submit ajax request. + List of values: http://api.jquery.com/jQuery.ajax + + @property ajaxOptions + @type object + @default null + @since 1.1.1 + @example + ajaxOptions: { + type: 'put', + dataType: 'json' + } + **/ + ajaxOptions: null, + /** + Where to show buttons: left(true)|bottom|false + Form without buttons is auto-submitted. + + @property showbuttons + @type boolean|string + @default true + @since 1.1.1 + **/ + showbuttons: true, + /** + Scope for callback methods (success, validate). + If null means editableform instance itself. + + @property scope + @type DOMElement|object + @default null + @since 1.2.0 + @private + **/ + scope: null, + /** + Whether to save or cancel value when it was not changed but form was submitted + + @property savenochange + @type boolean + @default false + @since 1.2.0 + **/ + savenochange: false + }; + + /* + Note: following params could redefined in engine: bootstrap or jqueryui: + Classes 'control-group' and 'editable-error-block' must always present! + */ + $.fn.editableform.template = '
    '+ + '
    ' + + '
    '+ + '
    ' + + '
    ' + + '
    '; + + //loading div + $.fn.editableform.loading = '
    '; + + //buttons + $.fn.editableform.buttons = ''+ + ''; + + //error class attached to control-group + $.fn.editableform.errorGroupClass = null; + + //error class attached to editable-error-block + $.fn.editableform.errorBlockClass = 'editable-error'; +}(window.jQuery)); + +/** +* EditableForm utilites +*/ +(function ($) { + "use strict"; + + //utils + $.fn.editableutils = { + /** + * classic JS inheritance function + */ + inherit: function (Child, Parent) { + var F = function() { }; + F.prototype = Parent.prototype; + Child.prototype = new F(); + Child.prototype.constructor = Child; + Child.superclass = Parent.prototype; + }, + + /** + * set caret position in input + * see http://stackoverflow.com/questions/499126/jquery-set-cursor-position-in-text-area + */ + setCursorPosition: function(elem, pos) { + if (elem.setSelectionRange) { + elem.setSelectionRange(pos, pos); + } else if (elem.createTextRange) { + var range = elem.createTextRange(); + range.collapse(true); + range.moveEnd('character', pos); + range.moveStart('character', pos); + range.select(); + } + }, + + /** + * function to parse JSON in *single* quotes. (jquery automatically parse only double quotes) + * That allows such code as: + * safe = true --> means no exception will be thrown + * for details see http://stackoverflow.com/questions/7410348/how-to-set-json-format-to-html5-data-attributes-in-the-jquery + */ + tryParseJson: function(s, safe) { + if (typeof s === 'string' && s.length && s.match(/^[\{\[].*[\}\]]$/)) { + if (safe) { + try { + /*jslint evil: true*/ + s = (new Function('return ' + s))(); + /*jslint evil: false*/ + } catch (e) {} finally { + return s; + } + } else { + /*jslint evil: true*/ + s = (new Function('return ' + s))(); + /*jslint evil: false*/ + } + } + return s; + }, + + /** + * slice object by specified keys + */ + sliceObj: function(obj, keys, caseSensitive /* default: false */) { + var key, keyLower, newObj = {}; + + if (!$.isArray(keys) || !keys.length) { + return newObj; + } + + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if (obj.hasOwnProperty(key)) { + newObj[key] = obj[key]; + } + + if(caseSensitive === true) { + continue; + } + + //when getting data-* attributes via $.data() it's converted to lowercase. + //details: http://stackoverflow.com/questions/7602565/using-data-attributes-with-jquery + //workaround is code below. + keyLower = key.toLowerCase(); + if (obj.hasOwnProperty(keyLower)) { + newObj[key] = obj[keyLower]; + } + } + + return newObj; + }, + + /* + exclude complex objects from $.data() before pass to config + */ + getConfigData: function($element) { + var data = {}; + $.each($element.data(), function(k, v) { + if(typeof v !== 'object' || (v && typeof v === 'object' && (v.constructor === Object || v.constructor === Array))) { + data[k] = v; + } + }); + return data; + }, + + /* + returns keys of object + */ + objectKeys: function(o) { + if (Object.keys) { + return Object.keys(o); + } else { + if (o !== Object(o)) { + throw new TypeError('Object.keys called on a non-object'); + } + var k=[], p; + for (p in o) { + if (Object.prototype.hasOwnProperty.call(o,p)) { + k.push(p); + } + } + return k; + } + + }, + + /** + method to escape html. + **/ + escape: function(str) { + return $('
    ').text(str).html(); + }, + + /* + returns array items from sourceData having value property equal or inArray of 'value' + */ + itemsByValue: function(value, sourceData, valueProp) { + if(!sourceData || value === null) { + return []; + } + + valueProp = valueProp || 'value'; + + var isValArray = $.isArray(value), + result = [], + that = this; + + $.each(sourceData, function(i, o) { + if(o.children) { + result = result.concat(that.itemsByValue(value, o.children, valueProp)); + } else { + /*jslint eqeq: true*/ + if(isValArray) { + if($.grep(value, function(v){ return v == (o && typeof o === 'object' ? o[valueProp] : o); }).length) { + result.push(o); + } + } else { + if(value == (o && typeof o === 'object' ? o[valueProp] : o)) { + result.push(o); + } + } + /*jslint eqeq: false*/ + } + }); + + return result; + }, + + /* + Returns input by options: type, mode. + */ + createInput: function(options) { + var TypeConstructor, typeOptions, input, + type = options.type; + + //`date` is some kind of virtual type that is transformed to one of exact types + //depending on mode and core lib + if(type === 'date') { + //inline + if(options.mode === 'inline') { + if($.fn.editabletypes.datefield) { + type = 'datefield'; + } else if($.fn.editabletypes.dateuifield) { + type = 'dateuifield'; + } + //popup + } else { + if($.fn.editabletypes.date) { + type = 'date'; + } else if($.fn.editabletypes.dateui) { + type = 'dateui'; + } + } + + //if type still `date` and not exist in types, replace with `combodate` that is base input + if(type === 'date' && !$.fn.editabletypes.date) { + type = 'combodate'; + } + } + + //`datetime` should be datetimefield in 'inline' mode + if(type === 'datetime' && options.mode === 'inline') { + type = 'datetimefield'; + } + + //change wysihtml5 to textarea for jquery UI and plain versions + if(type === 'wysihtml5' && !$.fn.editabletypes[type]) { + type = 'textarea'; + } + + //create input of specified type. Input will be used for converting value, not in form + if(typeof $.fn.editabletypes[type] === 'function') { + TypeConstructor = $.fn.editabletypes[type]; + typeOptions = this.sliceObj(options, this.objectKeys(TypeConstructor.defaults)); + input = new TypeConstructor(typeOptions); + return input; + } else { + $.error('Unknown type: '+ type); + return false; + } + } + + }; +}(window.jQuery)); + +/** +Attaches stand-alone container with editable-form to HTML element. Element is used only for positioning, value is not stored anywhere.
    +This method applied internally in $().editable(). You should subscribe on it's events (save / cancel) to get profit of it.
    +Final realization can be different: bootstrap-popover, jqueryui-tooltip, poshytip, inline-div. It depends on which js file you include.
    +Applied as jQuery method. + +@class editableContainer +@uses editableform +**/ +(function ($) { + "use strict"; + + var Popup = function (element, options) { + this.init(element, options); + }; + + var Inline = function (element, options) { + this.init(element, options); + }; + + //methods + Popup.prototype = { + containerName: null, //tbd in child class + innerCss: null, //tbd in child class + containerClass: 'editable-container editable-popup', //css class applied to container element + init: function(element, options) { + this.$element = $(element); + //since 1.4.1 container do not use data-* directly as they already merged into options. + this.options = $.extend({}, $.fn.editableContainer.defaults, options); + this.splitOptions(); + + //set scope of form callbacks to element + this.formOptions.scope = this.$element[0]; + + this.initContainer(); + + //bind 'destroyed' listener to destroy container when element is removed from dom + this.$element.on('destroyed', $.proxy(function(){ + this.destroy(); + }, this)); + + //attach document handler to close containers on click / escape + if(!$(document).data('editable-handlers-attached')) { + //close all on escape + $(document).on('keyup.editable', function (e) { + if (e.which === 27) { + $('.editable-open').editableContainer('hide'); + //todo: return focus on element + } + }); + + //close containers when click outside + //(mousedown could be better than click, it closes everything also on drag drop) + $(document).on('click.editable', function(e) { + var $target = $(e.target), i, + exclude_classes = ['.editable-container', + '.ui-datepicker-header', + '.datepicker', //in inline mode datepicker is rendered into body + '.modal-backdrop', + '.bootstrap-wysihtml5-insert-image-modal', + '.bootstrap-wysihtml5-insert-link-modal' + ]; + + //check if element is detached. It occurs when clicking in bootstrap datepicker + if (!$.contains(document.documentElement, e.target)) { + return; + } + + //for some reason FF 20 generates extra event (click) in select2 widget with e.target = document + //we need to filter it via construction below. See https://github.com/vitalets/x-editable/issues/199 + //Possibly related to http://stackoverflow.com/questions/10119793/why-does-firefox-react-differently-from-webkit-and-ie-to-click-event-on-selec + if($target.is(document)) { + return; + } + + //if click inside one of exclude classes --> no nothing + for(i=0; i'); + + //insert form into container body + if(this.tip().is(this.innerCss)) { + //for inline container + this.tip().append(this.$form); + } else { + this.tip().find(this.innerCss).append(this.$form); + } + + //render form + this.renderForm(); + }, + + /** + Hides container with form + @method hide() + @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|undefined (=manual) + **/ + hide: function(reason) { + if(!this.tip() || !this.tip().is(':visible') || !this.$element.hasClass('editable-open')) { + return; + } + + this.$element.removeClass('editable-open'); + this.innerHide(); + + /** + Fired when container was hidden. It occurs on both save or cancel. + **Note:** Bootstrap popover has own `hidden` event that now cannot be separated from x-editable's one. + The workaround is to check `arguments.length` that is always `2` for x-editable. + + @event hidden + @param {object} event event object + @param {string} reason Reason caused hiding. Can be save|cancel|onblur|nochange|manual + @example + $('#username').on('hidden', function(e, reason) { + if(reason === 'save' || reason === 'cancel') { + //auto-open next editable + $(this).closest('tr').next().find('.editable').editable('show'); + } + }); + **/ + this.$element.triggerHandler('hidden', reason || 'manual'); + }, + + /* internal show method. To be overwritten in child classes */ + innerShow: function () { + + }, + + /* internal hide method. To be overwritten in child classes */ + innerHide: function () { + + }, + + /** + Toggles container visibility (show / hide) + @method toggle() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + toggle: function(closeAll) { + if(this.container() && this.tip() && this.tip().is(':visible')) { + this.hide(); + } else { + this.show(closeAll); + } + }, + + /* + Updates the position of container when content changed. + @method setPosition() + */ + setPosition: function() { + //tbd in child class + }, + + save: function(e, params) { + /** + Fired when new value was submitted. You can use $(this).data('editableContainer') inside handler to access to editableContainer instance + + @event save + @param {Object} event event object + @param {Object} params additional params + @param {mixed} params.newValue submitted value + @param {Object} params.response ajax response + @example + $('#username').on('save', function(e, params) { + //assuming server response: '{success: true}' + var pk = $(this).data('editableContainer').options.pk; + if(params.response && params.response.success) { + alert('value: ' + params.newValue + ' with pk: ' + pk + ' saved!'); + } else { + alert('error!'); + } + }); + **/ + this.$element.triggerHandler('save', params); + + //hide must be after trigger, as saving value may require methods od plugin, applied to input + this.hide('save'); + }, + + /** + Sets new option + + @method option(key, value) + @param {string} key + @param {mixed} value + **/ + option: function(key, value) { + this.options[key] = value; + if(key in this.containerOptions) { + this.containerOptions[key] = value; + this.setContainerOption(key, value); + } else { + this.formOptions[key] = value; + if(this.$form) { + this.$form.editableform('option', key, value); + } + } + }, + + setContainerOption: function(key, value) { + this.call('option', key, value); + }, + + /** + Destroys the container instance + @method destroy() + **/ + destroy: function() { + this.hide(); + this.innerDestroy(); + this.$element.off('destroyed'); + this.$element.removeData('editableContainer'); + }, + + /* to be overwritten in child classes */ + innerDestroy: function() { + + }, + + /* + Closes other containers except one related to passed element. + Other containers can be cancelled or submitted (depends on onblur option) + */ + closeOthers: function(element) { + $('.editable-open').each(function(i, el){ + //do nothing with passed element and it's children + if(el === element || $(el).find(element).length) { + return; + } + + //otherwise cancel or submit all open containers + var $el = $(el), + ec = $el.data('editableContainer'); + + if(!ec) { + return; + } + + if(ec.options.onblur === 'cancel') { + $el.data('editableContainer').hide('onblur'); + } else if(ec.options.onblur === 'submit') { + $el.data('editableContainer').tip().find('form').submit(); + } + }); + + }, + + /** + Activates input of visible container (e.g. set focus) + @method activate() + **/ + activate: function() { + if(this.tip && this.tip().is(':visible') && this.$form) { + this.$form.data('editableform').input.activate(); + } + } + + }; + + /** + jQuery method to initialize editableContainer. + + @method $().editableContainer(options) + @params {Object} options + @example + $('#edit').editableContainer({ + type: 'text', + url: '/post', + pk: 1, + value: 'hello' + }); + **/ + $.fn.editableContainer = function (option) { + var args = arguments; + return this.each(function () { + var $this = $(this), + dataKey = 'editableContainer', + data = $this.data(dataKey), + options = typeof option === 'object' && option, + Constructor = (options.mode === 'inline') ? Inline : Popup; + + if (!data) { + $this.data(dataKey, (data = new Constructor(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + //store constructors + $.fn.editableContainer.Popup = Popup; + $.fn.editableContainer.Inline = Inline; + + //defaults + $.fn.editableContainer.defaults = { + /** + Initial value of form input + + @property value + @type mixed + @default null + @private + **/ + value: null, + /** + Placement of container relative to element. Can be top|right|bottom|left. Not used for inline container. + + @property placement + @type string + @default 'top' + **/ + placement: 'top', + /** + Whether to hide container on save/cancel. + + @property autohide + @type boolean + @default true + @private + **/ + autohide: true, + /** + Action when user clicks outside the container. Can be cancel|submit|ignore. + Setting ignore allows to have several containers open. + + @property onblur + @type string + @default 'cancel' + @since 1.1.1 + **/ + onblur: 'cancel', + + /** + Animation speed (inline mode) + @property anim + @type string + @default false + **/ + anim: false, + + /** + Mode of editable, can be `popup` or `inline` + + @property mode + @type string + @default 'popup' + @since 1.4.0 + **/ + mode: 'popup' + }; + + /* + * workaround to have 'destroyed' event to destroy popover when element is destroyed + * see http://stackoverflow.com/questions/2200494/jquery-trigger-event-when-an-element-is-removed-from-the-dom + */ + jQuery.event.special.destroyed = { + remove: function(o) { + if (o.handler) { + o.handler(); + } + } + }; + +}(window.jQuery)); + +/** +* Editable Inline +* --------------------- +*/ +(function ($) { + "use strict"; + + //copy prototype from EditableContainer + //extend methods + $.extend($.fn.editableContainer.Inline.prototype, $.fn.editableContainer.Popup.prototype, { + containerName: 'editableform', + innerCss: '.editable-inline', + containerClass: 'editable-container editable-inline', //css class applied to container element + + initContainer: function(){ + //container is element + this.$tip = $(''); + + //convert anim to miliseconds (int) + if(!this.options.anim) { + this.options.anim = 0; + } + }, + + splitOptions: function() { + //all options are passed to form + this.containerOptions = {}; + this.formOptions = this.options; + }, + + tip: function() { + return this.$tip; + }, + + innerShow: function () { + this.$element.hide(); + this.tip().insertAfter(this.$element).show(); + }, + + innerHide: function () { + this.$tip.hide(this.options.anim, $.proxy(function() { + this.$element.show(); + this.innerDestroy(); + }, this)); + }, + + innerDestroy: function() { + if(this.tip()) { + this.tip().empty().remove(); + } + } + }); + +}(window.jQuery)); +/** +Makes editable any HTML element on the page. Applied as jQuery method. + +@class editable +@uses editableContainer +**/ +(function ($) { + "use strict"; + + var Editable = function (element, options) { + this.$element = $(element); + //data-* has more priority over js options: because dynamically created elements may change data-* + this.options = $.extend({}, $.fn.editable.defaults, options, $.fn.editableutils.getConfigData(this.$element)); + if(this.options.selector) { + this.initLive(); + } else { + this.init(); + } + }; + + Editable.prototype = { + constructor: Editable, + init: function () { + var isValueByText = false, + doAutotext, finalize; + + //name + this.options.name = this.options.name || this.$element.attr('id'); + + //create input of specified type. Input needed already here to convert value for initial display (e.g. show text by id for select) + //also we set scope option to have access to element inside input specific callbacks (e. g. source as function) + this.options.scope = this.$element[0]; + this.input = $.fn.editableutils.createInput(this.options); + if(!this.input) { + return; + } + + //set value from settings or by element's text + if (this.options.value === undefined || this.options.value === null) { + this.value = this.input.html2value($.trim(this.$element.html())); + isValueByText = true; + } else { + /* + value can be string when received from 'data-value' attribute + for complext objects value can be set as json string in data-value attribute, + e.g. data-value="{city: 'Moscow', street: 'Lenina'}" + */ + this.options.value = $.fn.editableutils.tryParseJson(this.options.value, true); + if(typeof this.options.value === 'string') { + this.value = this.input.str2value(this.options.value); + } else { + this.value = this.options.value; + } + } + + //add 'editable' class to every editable element + this.$element.addClass('editable'); + + //attach handler activating editable. In disabled mode it just prevent default action (useful for links) + if(this.options.toggle !== 'manual') { + this.$element.addClass('editable-click'); + this.$element.on(this.options.toggle + '.editable', $.proxy(function(e){ + //prevent following link + e.preventDefault(); + + //stop propagation not required because in document click handler it checks event target + //e.stopPropagation(); + + if(this.options.toggle === 'mouseenter') { + //for hover only show container + this.show(); + } else { + //when toggle='click' we should not close all other containers as they will be closed automatically in document click listener + var closeAll = (this.options.toggle !== 'click'); + this.toggle(closeAll); + } + }, this)); + } else { + this.$element.attr('tabindex', -1); //do not stop focus on element when toggled manually + } + + //check conditions for autotext: + switch(this.options.autotext) { + case 'always': + doAutotext = true; + break; + case 'auto': + //if element text is empty and value is defined and value not generated by text --> run autotext + doAutotext = !$.trim(this.$element.text()).length && this.value !== null && this.value !== undefined && !isValueByText; + break; + default: + doAutotext = false; + } + + //depending on autotext run render() or just finilize init + $.when(doAutotext ? this.render() : true).then($.proxy(function() { + if(this.options.disabled) { + this.disable(); + } else { + this.enable(); + } + /** + Fired when element was initialized by `$().editable()` method. + Please note that you should setup `init` handler **before** applying `editable`. + + @event init + @param {Object} event event object + @param {Object} editable editable instance (as here it cannot accessed via data('editable')) + @since 1.2.0 + @example + $('#username').on('init', function(e, editable) { + alert('initialized ' + editable.options.name); + }); + $('#username').editable(); + **/ + this.$element.triggerHandler('init', this); + }, this)); + }, + + /* + Initializes parent element for live editables + */ + initLive: function() { + //store selector + var selector = this.options.selector; + //modify options for child elements + this.options.selector = false; + this.options.autotext = 'never'; + //listen toggle events + this.$element.on(this.options.toggle + '.editable', selector, $.proxy(function(e){ + var $target = $(e.target); + if(!$target.data('editable')) { + //if delegated element initially empty, we need to clear it's text (that was manually set to `empty` by user) + //see https://github.com/vitalets/x-editable/issues/137 + if($target.hasClass(this.options.emptyclass)) { + $target.empty(); + } + $target.editable(this.options).trigger(e); + } + }, this)); + }, + + /* + Renders value into element's text. + Can call custom display method from options. + Can return deferred object. + @method render() + @param {mixed} response server response (if exist) to pass into display function + */ + render: function(response) { + //do not display anything + if(this.options.display === false) { + return; + } + + //if input has `value2htmlFinal` method, we pass callback in third param to be called when source is loaded + if(this.input.value2htmlFinal) { + return this.input.value2html(this.value, this.$element[0], this.options.display, response); + //if display method defined --> use it + } else if(typeof this.options.display === 'function') { + return this.options.display.call(this.$element[0], this.value, response); + //else use input's original value2html() method + } else { + return this.input.value2html(this.value, this.$element[0]); + } + }, + + /** + Enables editable + @method enable() + **/ + enable: function() { + this.options.disabled = false; + this.$element.removeClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + if(this.options.toggle !== 'manual') { + if(this.$element.attr('tabindex') === '-1') { + this.$element.removeAttr('tabindex'); + } + } + }, + + /** + Disables editable + @method disable() + **/ + disable: function() { + this.options.disabled = true; + this.hide(); + this.$element.addClass('editable-disabled'); + this.handleEmpty(this.isEmpty); + //do not stop focus on this element + this.$element.attr('tabindex', -1); + }, + + /** + Toggles enabled / disabled state of editable element + @method toggleDisabled() + **/ + toggleDisabled: function() { + if(this.options.disabled) { + this.enable(); + } else { + this.disable(); + } + }, + + /** + Sets new option + + @method option(key, value) + @param {string|object} key option name or object with several options + @param {mixed} value option new value + @example + $('.editable').editable('option', 'pk', 2); + **/ + option: function(key, value) { + //set option(s) by object + if(key && typeof key === 'object') { + $.each(key, $.proxy(function(k, v){ + this.option($.trim(k), v); + }, this)); + return; + } + + //set option by string + this.options[key] = value; + + //disabled + if(key === 'disabled') { + return value ? this.disable() : this.enable(); + } + + //value + if(key === 'value') { + this.setValue(value); + } + + //transfer new option to container! + if(this.container) { + this.container.option(key, value); + } + + //pass option to input directly (as it points to the same in form) + if(this.input.option) { + this.input.option(key, value); + } + + }, + + /* + * set emptytext if element is empty + */ + handleEmpty: function (isEmpty) { + //do not handle empty if we do not display anything + if(this.options.display === false) { + return; + } + + this.isEmpty = isEmpty !== undefined ? isEmpty : $.trim(this.$element.text()) === ''; + + //emptytext shown only for enabled + if(!this.options.disabled) { + if (this.isEmpty) { + this.$element.text(this.options.emptytext); + if(this.options.emptyclass) { + this.$element.addClass(this.options.emptyclass); + } + } else if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } else { + //below required if element disable property was changed + if(this.isEmpty) { + this.$element.empty(); + if(this.options.emptyclass) { + this.$element.removeClass(this.options.emptyclass); + } + } + } + }, + + /** + Shows container with form + @method show() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + show: function (closeAll) { + if(this.options.disabled) { + return; + } + + //init editableContainer: popover, tooltip, inline, etc.. + if(!this.container) { + var containerOptions = $.extend({}, this.options, { + value: this.value, + input: this.input //pass input to form (as it is already created) + }); + this.$element.editableContainer(containerOptions); + //listen `save` event + this.$element.on("save.internal", $.proxy(this.save, this)); + this.container = this.$element.data('editableContainer'); + } else if(this.container.tip().is(':visible')) { + return; + } + + //show container + this.container.show(closeAll); + }, + + /** + Hides container with form + @method hide() + **/ + hide: function () { + if(this.container) { + this.container.hide(); + } + }, + + /** + Toggles container visibility (show / hide) + @method toggle() + @param {boolean} closeAll Whether to close all other editable containers when showing this one. Default true. + **/ + toggle: function(closeAll) { + if(this.container && this.container.tip().is(':visible')) { + this.hide(); + } else { + this.show(closeAll); + } + }, + + /* + * called when form was submitted + */ + save: function(e, params) { + //mark element with unsaved class if needed + if(this.options.unsavedclass) { + /* + Add unsaved css to element if: + - url is not user's function + - value was not sent to server + - params.response === undefined, that means data was not sent + - value changed + */ + var sent = false; + sent = sent || typeof this.options.url === 'function'; + sent = sent || this.options.display === false; + sent = sent || params.response !== undefined; + sent = sent || (this.options.savenochange && this.input.value2str(this.value) !== this.input.value2str(params.newValue)); + + if(sent) { + this.$element.removeClass(this.options.unsavedclass); + } else { + this.$element.addClass(this.options.unsavedclass); + } + } + + //set new value + this.setValue(params.newValue, false, params.response); + + /** + Fired when new value was submitted. You can use $(this).data('editable') to access to editable instance + + @event save + @param {Object} event event object + @param {Object} params additional params + @param {mixed} params.newValue submitted value + @param {Object} params.response ajax response + @example + $('#username').on('save', function(e, params) { + alert('Saved value: ' + params.newValue); + }); + **/ + //event itself is triggered by editableContainer. Description here is only for documentation + }, + + validate: function () { + if (typeof this.options.validate === 'function') { + return this.options.validate.call(this, this.value); + } + }, + + /** + Sets new value of editable + @method setValue(value, convertStr) + @param {mixed} value new value + @param {boolean} convertStr whether to convert value from string to internal format + **/ + setValue: function(value, convertStr, response) { + if(convertStr) { + this.value = this.input.str2value(value); + } else { + this.value = value; + } + if(this.container) { + this.container.option('value', this.value); + } + $.when(this.render(response)) + .then($.proxy(function() { + this.handleEmpty(); + }, this)); + }, + + /** + Activates input of visible container (e.g. set focus) + @method activate() + **/ + activate: function() { + if(this.container) { + this.container.activate(); + } + }, + + /** + Removes editable feature from element + @method destroy() + **/ + destroy: function() { + this.disable(); + + if(this.container) { + this.container.destroy(); + } + + if(this.options.toggle !== 'manual') { + this.$element.removeClass('editable-click'); + this.$element.off(this.options.toggle + '.editable'); + } + + this.$element.off("save.internal"); + + this.$element.removeClass('editable editable-open editable-disabled'); + this.$element.removeData('editable'); + } + }; + + /* EDITABLE PLUGIN DEFINITION + * ======================= */ + + /** + jQuery method to initialize editable element. + + @method $().editable(options) + @params {Object} options + @example + $('#username').editable({ + type: 'text', + url: '/post', + pk: 1 + }); + **/ + $.fn.editable = function (option) { + //special API methods returning non-jquery object + var result = {}, args = arguments, datakey = 'editable'; + switch (option) { + /** + Runs client-side validation for all matched editables + + @method validate() + @returns {Object} validation errors map + @example + $('#username, #fullname').editable('validate'); + // possible result: + { + username: "username is required", + fullname: "fullname should be minimum 3 letters length" + } + **/ + case 'validate': + this.each(function () { + var $this = $(this), data = $this.data(datakey), error; + if (data && (error = data.validate())) { + result[data.options.name] = error; + } + }); + return result; + + /** + Returns current values of editable elements. + Note that it returns an **object** with name-value pairs, not a value itself. It allows to get data from several elements. + If value of some editable is `null` or `undefined` it is excluded from result object. + + @method getValue() + @returns {Object} object of element names and values + @example + $('#username, #fullname').editable('getValue'); + // possible result: + { + username: "superuser", + fullname: "John" + } + **/ + case 'getValue': + this.each(function () { + var $this = $(this), data = $this.data(datakey); + if (data && data.value !== undefined && data.value !== null) { + result[data.options.name] = data.input.value2submit(data.value); + } + }); + return result; + + /** + This method collects values from several editable elements and submit them all to server. + Internally it runs client-side validation for all fields and submits only in case of success. + See
    creating new records for details. + + @method submit(options) + @param {object} options + @param {object} options.url url to submit data + @param {object} options.data additional data to submit + @param {object} options.ajaxOptions additional ajax options + @param {function} options.error(obj) error handler + @param {function} options.success(obj,config) success handler + @returns {Object} jQuery object + **/ + case 'submit': //collects value, validate and submit to server for creating new record + var config = arguments[1] || {}, + $elems = this, + errors = this.editable('validate'), + values; + + if($.isEmptyObject(errors)) { + values = this.editable('getValue'); + if(config.data) { + $.extend(values, config.data); + } + + $.ajax($.extend({ + url: config.url, + data: values, + type: 'POST' + }, config.ajaxOptions)) + .success(function(response) { + //successful response 200 OK + if(typeof config.success === 'function') { + config.success.call($elems, response, config); + } + }) + .error(function(){ //ajax error + if(typeof config.error === 'function') { + config.error.apply($elems, arguments); + } + }); + } else { //client-side validation error + if(typeof config.error === 'function') { + config.error.call($elems, errors); + } + } + return this; + } + + //return jquery object + return this.each(function () { + var $this = $(this), + data = $this.data(datakey), + options = typeof option === 'object' && option; + + if (!data) { + $this.data(datakey, (data = new Editable(this, options))); + } + + if (typeof option === 'string') { //call method + data[option].apply(data, Array.prototype.slice.call(args, 1)); + } + }); + }; + + + $.fn.editable.defaults = { + /** + Type of input. Can be text|textarea|select|date|checklist and more + + @property type + @type string + @default 'text' + **/ + type: 'text', + /** + Sets disabled state of editable + + @property disabled + @type boolean + @default false + **/ + disabled: false, + /** + How to toggle editable. Can be click|dblclick|mouseenter|manual. + When set to manual you should manually call show/hide methods of editable. + **Note**: if you call show or toggle inside **click** handler of some DOM element, + you need to apply e.stopPropagation() because containers are being closed on any click on document. + + @example + $('#edit-button').click(function(e) { + e.stopPropagation(); + $('#username').editable('toggle'); + }); + + @property toggle + @type string + @default 'click' + **/ + toggle: 'click', + /** + Text shown when element is empty. + + @property emptytext + @type string + @default 'Empty' + **/ + emptytext: 'Empty', + /** + Allows to automatically set element's text based on it's value. Can be auto|always|never. Useful for select and date. + For example, if dropdown list is {1: 'a', 2: 'b'} and element's value set to 1, it's html will be automatically set to 'a'. + auto - text will be automatically set only if element is empty. + always|never - always(never) try to set element's text. + + @property autotext + @type string + @default 'auto' + **/ + autotext: 'auto', + /** + Initial value of input. If not set, taken from element's text. + Note, that if element's text is empty - text is automatically generated from value and can be customized (see `autotext` option). + For example, to display currency sign: + @example + + + + @property value + @type mixed + @default element's text + **/ + value: null, + /** + Callback to perform custom displaying of value in element's text. + If `null`, default input's display used. + If `false`, no displaying methods will be called, element's text will never change. + Runs under element's scope. + _**Parameters:**_ + + * `value` current value to be displayed + * `response` server response (if display called after ajax submit), since 1.4.0 + + For _inputs with source_ (select, checklist) parameters are different: + + * `value` current value to be displayed + * `sourceData` array of items for current input (e.g. dropdown items) + * `response` server response (if display called after ajax submit), since 1.4.0 + + To get currently selected items use `$.fn.editableutils.itemsByValue(value, sourceData)`. + + @property display + @type function|boolean + @default null + @since 1.2.0 + @example + display: function(value, sourceData) { + //display checklist as comma-separated values + var html = [], + checked = $.fn.editableutils.itemsByValue(value, sourceData); + + if(checked.length) { + $.each(checked, function(i, v) { html.push($.fn.editableutils.escape(v.text)); }); + $(this).html(html.join(', ')); + } else { + $(this).empty(); + } + } + **/ + display: null, + /** + Css class applied when editable text is empty. + + @property emptyclass + @type string + @since 1.4.1 + @default editable-empty + **/ + emptyclass: 'editable-empty', + /** + Css class applied when value was stored but not sent to server (`pk` is empty or `send = 'never'`). + You may set it to `null` if you work with editables locally and submit them together. + + @property unsavedclass + @type string + @since 1.4.1 + @default editable-unsaved + **/ + unsavedclass: 'editable-unsaved', + /** + If selector is provided, editable will be delegated to the specified targets. + Usefull for dynamically generated DOM elements. + **Please note**, that delegated targets can't be initialized with `emptytext` and `autotext` options, + as they actually become editable only after first click. + You should manually set class `editable-click` to these elements. + Also, if element originally empty you should add class `editable-empty`, set `data-value=""` and write emptytext into element: + + @property selector + @type string + @since 1.4.1 + @default null + @example +
    + + Empty + + Operator +
    + + + **/ + selector: null + }; + +}(window.jQuery)); + +/** +AbstractInput - base class for all editable inputs. +It defines interface to be implemented by any input type. +To create your own input you can inherit from this class. + +@class abstractinput +**/ +(function ($) { + "use strict"; + + //types + $.fn.editabletypes = {}; + + var AbstractInput = function () { }; + + AbstractInput.prototype = { + /** + Initializes input + + @method init() + **/ + init: function(type, options, defaults) { + this.type = type; + this.options = $.extend({}, defaults, options); + }, + + /* + this method called before render to init $tpl that is inserted in DOM + */ + prerender: function() { + this.$tpl = $(this.options.tpl); //whole tpl as jquery object + this.$input = this.$tpl; //control itself, can be changed in render method + this.$clear = null; //clear button + this.error = null; //error message, if input cannot be rendered + }, + + /** + Renders input from tpl. Can return jQuery deferred object. + Can be overwritten in child objects + + @method render() + **/ + render: function() { + + }, + + /** + Sets element's html by value. + + @method value2html(value, element) + @param {mixed} value + @param {DOMElement} element + **/ + value2html: function(value, element) { + $(element).text(value); + }, + + /** + Converts element's html to value + + @method html2value(html) + @param {string} html + @returns {mixed} + **/ + html2value: function(html) { + return $('
    ').html(html).text(); + }, + + /** + Converts value to string (for internal compare). For submitting to server used value2submit(). + + @method value2str(value) + @param {mixed} value + @returns {string} + **/ + value2str: function(value) { + return value; + }, + + /** + Converts string received from server into value. Usually from `data-value` attribute. + + @method str2value(str) + @param {string} str + @returns {mixed} + **/ + str2value: function(str) { + return str; + }, + + /** + Converts value for submitting to server. Result can be string or object. + + @method value2submit(value) + @param {mixed} value + @returns {mixed} + **/ + value2submit: function(value) { + return value; + }, + + /** + Sets value of input. + + @method value2input(value) + @param {mixed} value + **/ + value2input: function(value) { + this.$input.val(value); + }, + + /** + Returns value of input. Value can be object (e.g. datepicker) + + @method input2value() + **/ + input2value: function() { + return this.$input.val(); + }, + + /** + Activates input. For text it sets focus. + + @method activate() + **/ + activate: function() { + if(this.$input.is(':visible')) { + this.$input.focus(); + } + }, + + /** + Creates input. + + @method clear() + **/ + clear: function() { + this.$input.val(null); + }, + + /** + method to escape html. + **/ + escape: function(str) { + return $('
    ').text(str).html(); + }, + + /** + attach handler to automatically submit form when value changed (useful when buttons not shown) + **/ + autosubmit: function() { + + }, + + // -------- helper functions -------- + setClass: function() { + if(this.options.inputclass) { + this.$input.addClass(this.options.inputclass); + } + }, + + setAttr: function(attr) { + if (this.options[attr] !== undefined && this.options[attr] !== null) { + this.$input.attr(attr, this.options[attr]); + } + }, + + option: function(key, value) { + this.options[key] = value; + } + + }; + + AbstractInput.defaults = { + /** + HTML template of input. Normally you should not change it. + + @property tpl + @type string + @default '' + **/ + tpl: '', + /** + CSS class automatically applied to input + + @property inputclass + @type string + @default input-medium + **/ + inputclass: 'input-medium', + //scope for external methods (e.g. source defined as function) + //for internal use only + scope: null, + + //need to re-declare showbuttons here to get it's value from common config (passed only options existing in defaults) + showbuttons: true + }; + + $.extend($.fn.editabletypes, {abstractinput: AbstractInput}); + +}(window.jQuery)); + +/** +List - abstract class for inputs that have source option loaded from js array or via ajax + +@class list +@extends abstractinput +**/ +(function ($) { + "use strict"; + + var List = function (options) { + + }; + + $.fn.editableutils.inherit(List, $.fn.editabletypes.abstractinput); + + $.extend(List.prototype, { + render: function () { + var deferred = $.Deferred(); + + this.error = null; + this.onSourceReady(function () { + this.renderList(); + deferred.resolve(); + }, function () { + this.error = this.options.sourceError; + deferred.resolve(); + }); + + return deferred.promise(); + }, + + html2value: function (html) { + return null; //can't set value by text + }, + + value2html: function (value, element, display, response) { + var deferred = $.Deferred(), + success = function () { + if(typeof display === 'function') { + //custom display method + display.call(element, value, this.sourceData, response); + } else { + this.value2htmlFinal(value, element); + } + deferred.resolve(); + }; + + //for null value just call success without loading source + if(value === null) { + success.call(this); + } else { + this.onSourceReady(success, function () { deferred.resolve(); }); + } + + return deferred.promise(); + }, + + // ------------- additional functions ------------ + + onSourceReady: function (success, error) { + //if allready loaded just call success + if($.isArray(this.sourceData)) { + success.call(this); + return; + } + + // try parse json in single quotes (for double quotes jquery does automatically) + try { + this.options.source = $.fn.editableutils.tryParseJson(this.options.source, false); + } catch (e) { + error.call(this); + return; + } + + var source = this.options.source; + + //run source if it function + if ($.isFunction(source)) { + source = source.call(this.options.scope); + } + + //loading from url + if (typeof source === 'string') { + //try to get from cache + if(this.options.sourceCache) { + var cacheID = source, + cache; + + if (!$(document).data(cacheID)) { + $(document).data(cacheID, {}); + } + cache = $(document).data(cacheID); + + //check for cached data + if (cache.loading === false && cache.sourceData) { //take source from cache + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + return; + } else if (cache.loading === true) { //cache is loading, put callback in stack to be called later + cache.callbacks.push($.proxy(function () { + this.sourceData = cache.sourceData; + this.doPrepend(); + success.call(this); + }, this)); + + //also collecting error callbacks + cache.err_callbacks.push($.proxy(error, this)); + return; + } else { //no cache yet, activate it + cache.loading = true; + cache.callbacks = []; + cache.err_callbacks = []; + } + } + + //loading sourceData from server + $.ajax({ + url: source, + type: 'get', + cache: false, + dataType: 'json', + success: $.proxy(function (data) { + if(cache) { + cache.loading = false; + } + this.sourceData = this.makeArray(data); + if($.isArray(this.sourceData)) { + if(cache) { + //store result in cache + cache.sourceData = this.sourceData; + //run success callbacks for other fields waiting for this source + $.each(cache.callbacks, function () { this.call(); }); + } + this.doPrepend(); + success.call(this); + } else { + error.call(this); + if(cache) { + //run error callbacks for other fields waiting for this source + $.each(cache.err_callbacks, function () { this.call(); }); + } + } + }, this), + error: $.proxy(function () { + error.call(this); + if(cache) { + cache.loading = false; + //run error callbacks for other fields + $.each(cache.err_callbacks, function () { this.call(); }); + } + }, this) + }); + } else { //options as json/array + this.sourceData = this.makeArray(source); + + if($.isArray(this.sourceData)) { + this.doPrepend(); + success.call(this); + } else { + error.call(this); + } + } + }, + + doPrepend: function () { + if(this.options.prepend === null || this.options.prepend === undefined) { + return; + } + + if(!$.isArray(this.prependData)) { + //run prepend if it is function (once) + if ($.isFunction(this.options.prepend)) { + this.options.prepend = this.options.prepend.call(this.options.scope); + } + + //try parse json in single quotes + this.options.prepend = $.fn.editableutils.tryParseJson(this.options.prepend, true); + + //convert prepend from string to object + if (typeof this.options.prepend === 'string') { + this.options.prepend = {'': this.options.prepend}; + } + + this.prependData = this.makeArray(this.options.prepend); + } + + if($.isArray(this.prependData) && $.isArray(this.sourceData)) { + this.sourceData = this.prependData.concat(this.sourceData); + } + }, + + /* + renders input list + */ + renderList: function() { + // this method should be overwritten in child class + }, + + /* + set element's html by value + */ + value2htmlFinal: function(value, element) { + // this method should be overwritten in child class + }, + + /** + * convert data to array suitable for sourceData, e.g. [{value: 1, text: 'abc'}, {...}] + */ + makeArray: function(data) { + var count, obj, result = [], item, iterateItem; + if(!data || typeof data === 'string') { + return null; + } + + if($.isArray(data)) { //array + /* + function to iterate inside item of array if item is object. + Caclulates count of keys in item and store in obj. + */ + iterateItem = function (k, v) { + obj = {value: k, text: v}; + if(count++ >= 2) { + return false;// exit from `each` if item has more than one key. + } + }; + + for(var i = 0; i < data.length; i++) { + item = data[i]; + if(typeof item === 'object') { + count = 0; //count of keys inside item + $.each(item, iterateItem); + //case: [{val1: 'text1'}, {val2: 'text2} ...] + if(count === 1) { + result.push(obj); + //case: [{value: 1, text: 'text1'}, {value: 2, text: 'text2'}, ...] + } else if(count > 1) { + //removed check of existance: item.hasOwnProperty('value') && item.hasOwnProperty('text') + if(item.children) { + item.children = this.makeArray(item.children); + } + result.push(item); + } + } else { + //case: ['text1', 'text2' ...] + result.push({value: item, text: item}); + } + } + } else { //case: {val1: 'text1', val2: 'text2, ...} + $.each(data, function (k, v) { + result.push({value: k, text: v}); + }); + } + return result; + }, + + option: function(key, value) { + this.options[key] = value; + if(key === 'source') { + this.sourceData = null; + } + if(key === 'prepend') { + this.prependData = null; + } + } + + }); + + List.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + Source data for list. + If **array** - it should be in format: `[{value: 1, text: "text1"}, {value: 2, text: "text2"}, ...]` + For compability, object format is also supported: `{"1": "text1", "2": "text2" ...}` but it does not guarantee elements order. + + If **string** - considered ajax url to load items. In that case results will be cached for fields with the same source and name. See also `sourceCache` option. + + If **function**, it should return data in format above (since 1.4.0). + + Since 1.4.1 key `children` supported to render OPTGROUP (for **select** input only). + `[{text: "group1", children: [{value: 1, text: "text1"}, {value: 2, text: "text2"}]}, ...]` + + + @property source + @type string | array | object | function + @default null + **/ + source: null, + /** + Data automatically prepended to the beginning of dropdown list. + + @property prepend + @type string | array | object | function + @default false + **/ + prepend: false, + /** + Error message when list cannot be loaded (e.g. ajax error) + + @property sourceError + @type string + @default Error when loading list + **/ + sourceError: 'Error when loading list', + /** + if true and source is **string url** - results will be cached for fields with the same source. + Usefull for editable column in grid to prevent extra requests. + + @property sourceCache + @type boolean + @default true + @since 1.2.0 + **/ + sourceCache: true + }); + + $.fn.editabletypes.list = List; + +}(window.jQuery)); + +/** +Text input + +@class text +@extends abstractinput +@final +@example +awesome + +**/ +(function ($) { + "use strict"; + + var Text = function (options) { + this.init('text', options, Text.defaults); + }; + + $.fn.editableutils.inherit(Text, $.fn.editabletypes.abstractinput); + + $.extend(Text.prototype, { + render: function() { + this.renderClear(); + this.setClass(); + this.setAttr('placeholder'); + }, + + activate: function() { + if(this.$input.is(':visible')) { + this.$input.focus(); + $.fn.editableutils.setCursorPosition(this.$input.get(0), this.$input.val().length); + if(this.toggleClear) { + this.toggleClear(); + } + } + }, + + //render clear button + renderClear: function() { + if (this.options.clear) { + this.$clear = $(''); + this.$input.after(this.$clear) + .css('padding-right', 24) + .keyup($.proxy(function(e) { + //arrows, enter, tab, etc + if(~$.inArray(e.keyCode, [40,38,9,13,27])) { + return; + } + + clearTimeout(this.t); + var that = this; + this.t = setTimeout(function() { + that.toggleClear(e); + }, 100); + + }, this)) + .parent().css('position', 'relative'); + + this.$clear.click($.proxy(this.clear, this)); + } + }, + + postrender: function() { + /* + //now `clear` is positioned via css + if(this.$clear) { + //can position clear button only here, when form is shown and height can be calculated +// var h = this.$input.outerHeight(true) || 20, + var h = this.$clear.parent().height(), + delta = (h - this.$clear.height()) / 2; + + //this.$clear.css({bottom: delta, right: delta}); + } + */ + }, + + //show / hide clear button + toggleClear: function(e) { + if(!this.$clear) { + return; + } + + var len = this.$input.val().length, + visible = this.$clear.is(':visible'); + + if(len && !visible) { + this.$clear.show(); + } + + if(!len && visible) { + this.$clear.hide(); + } + }, + + clear: function() { + this.$clear.hide(); + this.$input.val('').focus(); + } + }); + + Text.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default + **/ + tpl: '', + /** + Placeholder attribute of input. Shown when input is empty. + + @property placeholder + @type string + @default null + **/ + placeholder: null, + + /** + Whether to show `clear` button + + @property clear + @type boolean + @default true + **/ + clear: true + }); + + $.fn.editabletypes.text = Text; + +}(window.jQuery)); + +/** +Textarea input + +@class textarea +@extends abstractinput +@final +@example +awesome comment! + +**/ +(function ($) { + "use strict"; + + var Textarea = function (options) { + this.init('textarea', options, Textarea.defaults); + }; + + $.fn.editableutils.inherit(Textarea, $.fn.editabletypes.abstractinput); + + $.extend(Textarea.prototype, { + render: function () { + this.setClass(); + this.setAttr('placeholder'); + this.setAttr('rows'); + + //ctrl + enter + this.$input.keydown(function (e) { + if (e.ctrlKey && e.which === 13) { + $(this).closest('form').submit(); + } + }); + }, + + value2html: function(value, element) { + var html = '', lines; + if(value) { + lines = value.split("\n"); + for (var i = 0; i < lines.length; i++) { + lines[i] = $('
    ').text(lines[i]).html(); + } + html = lines.join('
    '); + } + $(element).html(html); + }, + + html2value: function(html) { + if(!html) { + return ''; + } + + var regex = new RegExp(String.fromCharCode(10), 'g'); + var lines = html.split(//i); + for (var i = 0; i < lines.length; i++) { + var text = $('
    ').html(lines[i]).text(); + + // Remove newline characters (\n) to avoid them being converted by value2html() method + // thus adding extra
    tags + text = text.replace(regex, ''); + + lines[i] = text; + } + return lines.join("\n"); + }, + + activate: function() { + $.fn.editabletypes.text.prototype.activate.call(this); + } + }); + + Textarea.defaults = $.extend({}, $.fn.editabletypes.abstractinput.defaults, { + /** + @property tpl + @default + **/ + tpl:'', + /** + @property inputclass + @default input-large + **/ + inputclass: 'input-large', + /** + Placeholder attribute of input. Shown when input is empty. + + @property placeholder + @type string + @default null + **/ + placeholder: null, + /** + Number of rows in textarea + + @property rows + @type integer + @default 7 + **/ + rows: 7 + }); + + $.fn.editabletypes.textarea = Textarea; + +}(window.jQuery)); + +/** +Select (dropdown) + +@class select +@extends list +@final +@example + + +**/ +(function ($) { + "use strict"; + + var Select = function (options) { + this.init('select', options, Select.defaults); + }; + + $.fn.editableutils.inherit(Select, $.fn.editabletypes.list); + + $.extend(Select.prototype, { + renderList: function() { + this.$input.empty(); + + var fillItems = function($el, data) { + if($.isArray(data)) { + for(var i=0; i', {label: data[i].text}), data[i].children)); + } else { + $el.append($('

    ucI*Nxg$*x;8sTf_T7|`O!W*7B}ab^oSMRL#PoM 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

    ^veP+& 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 z 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%0D37Iynyd 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∋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