diff options
Diffstat (limited to 'src')
62 files changed, 15574 insertions, 0 deletions
diff --git a/src/SemanticScuttle/Config.php b/src/SemanticScuttle/Config.php new file mode 100644 index 0000000..756c303 --- /dev/null +++ b/src/SemanticScuttle/Config.php @@ -0,0 +1,123 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Configuration handling + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Config +{ + /** + * Prefix for configuration files. + * Used to inject stream wrapper protocol for unit testing + * + * @var string + */ + public $filePrefix = ''; + + + + /** + * Finds the correct data directory + * + * @return string Full path to the data directory with a trailing slash + */ + protected function getDataDir() + { + if ('@data_dir@' == '@' . 'data_dir@') { + //non pear-install + $datadir = dirname(__FILE__) . '/../../data/'; + } else { + //pear installation; files are in include path + $datadir = '@data_dir@/SemanticScuttle/'; + } + + return $datadir; + } + + + + /** + * Tries to find a configuration file by looking in different + * places: + * - pear data_dir/SemanticScuttle/config-$hostname.php + * - pear data_dir/SemanticScuttle/config.php + * - /etc/semanticscuttle/config-$hostname.php + * - /etc/semanticscuttle/config.php + * + * Paths with host name have priority. + * + * When open_basedir restrictions are in effect and /etc is not part of + * the setting, /etc/semanticscuttle/ is not checked for config files. + * + * @return array Array with config file path as first value + * and default config file path as second value. + * Any may be NULL if not found + */ + public function findFiles() + { + //use basename to prevent path injection + $host = basename($_SERVER['HTTP_HOST']); + $datadir = $this->getDataDir(); + + $openbase = ini_get('open_basedir'); + if ($openbase && strpos($openbase, '/etc') === false) { + //open_basedir restrictions enabled and /etc not allowed? + // then don't look in /etc for config files. + // the check is not perfect, but it covers most cases + $arFiles = array( + $datadir . 'config.' . $host . '.php', + $datadir . 'config.php', + ); + } else { + $arFiles = array( + $datadir . 'config.' . $host . '.php', + '/etc/semanticscuttle/config.' . $host . '.php', + $datadir . 'config.php', + '/etc/semanticscuttle/config.php', + ); + } + + $configfile = null; + foreach ($arFiles as $file) { + if (file_exists($this->filePrefix . $file)) { + $configfile = $file; + break; + } + } + + //find default file + $arDefaultFiles = array_unique( + array( + substr($configfile, 0, -3) . 'default.php', + $datadir . 'config.default.php', + '/etc/semanticscuttle/config.default.php', + ) + ); + $defaultfile = null; + foreach ($arDefaultFiles as $file) { + if (file_exists($this->filePrefix . $file)) { + $defaultfile = $file; + break; + } + } + return array($configfile, $defaultfile); + } +} + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/DbService.php b/src/SemanticScuttle/DbService.php new file mode 100644 index 0000000..875ee98 --- /dev/null +++ b/src/SemanticScuttle/DbService.php @@ -0,0 +1,67 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Base class for services utilizing the database. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_DbService extends SemanticScuttle_Service +{ + /** + * Database object + * + * @var sql_db + */ + protected $db; + + + + /** + * Database table name + * + * @var string + */ + protected $tablename; + + + + /** + * Returns database table name + * + * @return string Table name + */ + public function getTableName() + { + return $this->tablename; + } + + + + /** + * Set the database table name + * + * @param string $value New table name + * + * @return void + */ + function setTableName($value) + { + $this->tablename = $value; + } + +} diff --git a/src/SemanticScuttle/Environment.php b/src/SemanticScuttle/Environment.php new file mode 100644 index 0000000..7ccb466 --- /dev/null +++ b/src/SemanticScuttle/Environment.php @@ -0,0 +1,48 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Server environment handling methods + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Environment +{ + /** + * Determines the correct $_SERVER['PATH_INFO'] value + * + * @return string New value + */ + public static function getServerPathInfo() + { + if (isset($_SERVER['PATH_INFO'])) { + return $_SERVER['PATH_INFO']; + } + + if (isset($_SERVER['ORIG_PATH_INFO'])) { + //1&1 servers + if ($_SERVER['ORIG_PATH_INFO'] == $_SERVER['SCRIPT_NAME']) { + return ''; + } + return $_SERVER['ORIG_PATH_INFO']; + } + + //fallback when no special path after the php file is given + return ''; + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/Bookmark.php b/src/SemanticScuttle/Model/Bookmark.php new file mode 100644 index 0000000..1330642 --- /dev/null +++ b/src/SemanticScuttle/Model/Bookmark.php @@ -0,0 +1,63 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Bookmark model class, keeping the data of a single bookmark. + * It will slowly replace the old array style format. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_Bookmark +{ + /** + * Status "public" / visible for all + */ + const SPUBLIC = 0; + + /** + * Status "shared" / visible for people on your watchlist + */ + const SWATCHLIST = 1; + + /** + * Status "private" / visible for yourself only + */ + const SPRIVATE = 2; + + + + /** + * Checks if the given URL is valid and may be used with this + * SemanticScuttle installation. + * + * @param string $url URL to verify. + * + * @return boolean True if the URL is allowed, false if not + */ + public static function isValidUrl($url) + { + $scheme = parse_url($url, PHP_URL_SCHEME); + if (array_search($scheme, $GLOBALS['allowedProtocols']) === false) { + return false; + } + return true; + } + +} + + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/RemoteUser.php b/src/SemanticScuttle/Model/RemoteUser.php new file mode 100644 index 0000000..6d48e3a --- /dev/null +++ b/src/SemanticScuttle/Model/RemoteUser.php @@ -0,0 +1,48 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Remote User helper methods. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_RemoteUser +{ + /** + * Returns the remote user's IP. + * + * @return string IP address. NULL if not found. + */ + public static function getIp() + { + $ip = null; + if (getenv('REMOTE_ADDR')) { + $ip = getenv('REMOTE_ADDR'); + } else if (getenv('HTTP_CLIENT_IP')) { + $ip = getenv('HTTP_CLIENT_IP'); + } else if (getenv('HTTP_X_FORWARDED_FOR')) { + $ip = getenv('HTTP_X_FORWARDED_FOR'); + } + + return $ip; + } + +} + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/Template.php b/src/SemanticScuttle/Model/Template.php new file mode 100644 index 0000000..234e23f --- /dev/null +++ b/src/SemanticScuttle/Model/Template.php @@ -0,0 +1,107 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle HTML templating system. + * This templating system is really, really simple and based + * on including php files while proving a set of + * variables in the template scope. + * When rendering templates, they are directly echoed to the + * browser. There is no in-built way to capture their output. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_Template +{ + /** + * Array of variables to be available in template + * scope. + * + * @var array + */ + protected $vars = array(); + + /** + * File name of template + */ + protected $file = ''; + + /** + * Template service instance + * + * @var SemanticScuttle_Service_Template + */ + protected $ts; + + + + /** + * Create a new template instance + * + * @param string $file Template filename, + * full path + * @param array $vars Template variables + * @param SemanticScuttle_Service_Template $ts Template service + */ + public function __construct( + $file, $vars = null, + SemanticScuttle_Service_Template $ts = null + ) { + $this->vars = $vars; + $this->file = $file; + $this->ts = $ts; + } + + + + /** + * Sets variables and includes the template file, + * causing it to be rendered. + * + * Does not take care of themes and so. + * The include path must be set so the correct theme is used. + * + * @return void + */ + public function parse() + { + if (isset($this->vars)) { + extract($this->vars); + } + include $this->file; + } + + + + /** + * Loads another template + * + * @param string $file Filename of template, relative + * to template directory + * + * @return SemanticScuttle_Service_Template Template object + */ + public function includeTemplate($file) + { + return $this->ts->loadTemplate($file, $this->vars); + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/Theme.php b/src/SemanticScuttle/Model/Theme.php new file mode 100644 index 0000000..65861b8 --- /dev/null +++ b/src/SemanticScuttle/Model/Theme.php @@ -0,0 +1,97 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL v3 or later http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * A theme, the visual representation of SemanticScuttle. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL v3 or later http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_Theme +{ + /** + * Theme name. Also the path part of template and resource files + * + * @var string + */ + protected $name = null; + + /** + * Local path to the www themes directory. + * Needs to have a trailing slash. + * + * @var string + */ + protected $wwwThemeDir = null; + + + + /** + * Create a new theme instance. + * + * @param string $name Theme name "data/templates/(*)/" + */ + public function __construct($name = 'default') + { + $this->name = $name; + $this->wwwThemeDir = $GLOBALS['wwwdir'] . '/themes/'; + //TODO: implement theme hierarchies with parent fallback + } + + + + /** + * Returns the URL path to a resource file (www/themes/$name/$file). + * Automatically falls back to the parent theme if the file does not exist + * in the theme. + * + * Must always be used when adding i.e. images to the output. + * + * @param string $file File name to find the path for, i.e. "scuttle.css". + * + * @return string Full path + */ + public function resource($file) + { + $themeFile = $this->wwwThemeDir . $this->name . '/' . $file; + if (file_exists($themeFile)) { + return ROOT . 'themes/' . $this->name . '/' . $file; + } + + $defaultFile = $this->wwwThemeDir . 'default/' . $file; + if (file_exists($defaultFile)) { + return ROOT . 'themes/default/' . $file; + } + + //file does not exist. fall back to the theme file + // to guide the theme author a bit. + // TODO: logging. in admin mode, there should be a message + return ROOT . 'themes/' . $this->name . '/' . $file; + } + + + + /** + * Returns the theme name. + * + * @return string Theme name + */ + public function getName() + { + return $this->name; + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/User.php b/src/SemanticScuttle/Model/User.php new file mode 100644 index 0000000..e5d29af --- /dev/null +++ b/src/SemanticScuttle/Model/User.php @@ -0,0 +1,209 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle user object. + * Rarely used fields are filled if required. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_User +{ + var $id; + var $username; + var $name; + var $email; + var $homepage; + var $content; + var $datetime; + var $isAdmin; + var $privateKey; + + /** + * Create a new user object + * + * @param integer $id User ID + * @param string $username Username + */ + public function __construct($id, $username) + { + $this->id = $id; + $this->username = $username; + } + + /** + * Returns user ID + * + * @return integer ID + */ + public function getId() + { + return (int)$this->id; + } + + /** + * Returns logon user name + * + * @return string User name + */ + public function getUsername() + { + return $this->username; + } + + /** + * Returns private key + * + * @param boolean return sanitized value which basically drops + * leading dash if exists + * + * @return string private key + */ + public function getPrivateKey($sanitized = false) + { + // Look for value only if not already set + if (!isset($this->privateKey)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->privateKey = $user['privateKey']; + } + if ($sanitized == true) { + return substr($this->privateKey, -32); + } else { + return $this->privateKey; + } + } + + /** + * Returns full user name as specified in the profile. + * + * @return string Full name + */ + public function getName() + { + // Look for value only if not already set + if (!isset($this->name)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->name = $user['name']; + } + return $this->name; + } + + /** + * Returns user email address + * + * @return string Email address + */ + public function getEmail() + { + // Look for value only if not already set + if (!isset($this->email)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->email = $user['email']; + } + return $this->email; + } + + /** + * Returns user homepage as specified in the profile. + * + * @return string Homepage + */ + public function getHomepage() + { + // Look for value only if not already set + if(!isset($this->homepage)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->homepage = $user['homepage']; + } + return $this->homepage; + } + + /** + * Returns custom user description as specified in the profile. + * + * @return string User description + */ + public function getContent() + { + // Look for value only if not already set + if(!isset($this->content)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->content = $user['uContent']; + } + return $this->content; + } + + /** + * Returns user creation time. + * UTC/Zulu time zone is used. + * + * @return string Datetime value: "YYYY-MM-DD HH:MM:SS" + */ + public function getDatetime() + { + // Look for value only if not already set + if(!isset($this->content)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->datetime = $user['uDatetime']; + } + return $this->datetime; + } + + /** + * Tells you if the user is an administrator + * + * @return boolean True if the user is admin + */ + public function isAdmin() + { + // Look for value only if not already set + if(!isset($this->isAdmin)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $this->isAdmin = $us->isAdmin($this->username); + } + return $this->isAdmin; + } + + /** + * Returns the number of bookmarks the user owns + * + * @param string $range Range of bookmarks: + * 'public', 'shared', 'private' + * or 'all' + * + * @return integer Number of bookmarks + * + * @uses SemanticScuttle_Service_Bookmark::countBookmarks() + */ + public function getNbBookmarks($range = 'public') + { + $bs = SemanticScuttle_Service_Factory::get('Bookmark'); + return $bs->countBookmarks($this->getId(), $range); + } + +} +?> diff --git a/src/SemanticScuttle/Model/User/SslClientCert.php b/src/SemanticScuttle/Model/User/SslClientCert.php new file mode 100644 index 0000000..383b601 --- /dev/null +++ b/src/SemanticScuttle/Model/User/SslClientCert.php @@ -0,0 +1,148 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SSL client certificate model. Represents one single client certificate + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_User_SslClientCert +{ + public $id; + public $uId; + public $sslSerial; + public $sslClientIssuerDn; + public $sslName; + public $sslEmail; + + + + /** + * Creates and returns a new object and fills it with + * the passed values from the database. + * + * @param array $arCertRow Database row array + * + * @return SemanticScuttle_Model_User_SslClientCert + */ + public static function fromDb($arCertRow) + { + $cert = new self(); + foreach (get_object_vars($cert) as $variable => $dummy) { + if (isset($arCertRow[$variable])) { + $cert->$variable = $arCertRow[$variable]; + } + } + return $cert; + } + + + + /** + * Loads the user's/browser's client certificate information into + * an object and returns it. + * Expects that all information is available. + * Better check with + * SemanticScuttle_Service_User_SslClientCert::hasValidCert() before. + * + * @return SemanticScuttle_Model_User_SslClientCert + * + * @see SemanticScuttle_Service_User_SslClientCert::hasValidCert() + */ + public static function fromCurrentCert() + { + $cert = new self(); + $cert->sslSerial = $_SERVER['SSL_CLIENT_M_SERIAL']; + $cert->sslClientIssuerDn = $_SERVER['SSL_CLIENT_I_DN']; + $cert->sslName = $_SERVER['SSL_CLIENT_S_DN_CN']; + $cert->sslEmail = $_SERVER['SSL_CLIENT_S_DN_Email']; + return $cert; + } + + + + /** + * Tells you if this certificate is the one the user is currently browsing + * with. + * + * @return boolean True if this certificate is the current browser's + */ + public function isCurrent() + { + if (!isset($_SERVER['SSL_CLIENT_M_SERIAL']) + || !isset($_SERVER['SSL_CLIENT_I_DN']) + ) { + return false; + } + + return $this->sslSerial == $_SERVER['SSL_CLIENT_M_SERIAL'] + && $this->sslClientIssuerDn == $_SERVER['SSL_CLIENT_I_DN']; + } + + + + /** + * Checks if this certificate is registered (exists) in the certificate + * array + * + * @param array $arCertificates Array of certificate objects + * + * @return boolean True or false + */ + public function isRegistered($arCertificates) + { + foreach ($arCertificates as $cert) { + if ($cert->equals($this)) { + return true; + } + } + return false; + } + + + + /** + * Deletes this certificate from database + * + * @return boolean True if all went well, false if not + */ + public function delete() + { + $ok = SemanticScuttle_Service_Factory::get('User_SslClientCert') + ->delete($this); + if ($ok) { + $this->id = null; + } + return $ok; + } + + + + /** + * Compares this certificate with the given one. + * + * @param SemanticScuttle_Service_Factory $cert Another user certificate + * + * @return boolean True if both match. + */ + public function equals(SemanticScuttle_Model_User_SslClientCert $cert) + { + return $this->sslSerial == $cert->sslSerial + && $this->sslClientIssuerDn == $cert->sslClientIssuerDn; + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Model/UserArray.php b/src/SemanticScuttle/Model/UserArray.php new file mode 100644 index 0000000..a0d9c9b --- /dev/null +++ b/src/SemanticScuttle/Model/UserArray.php @@ -0,0 +1,41 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Mostly static methods that help working with a user row array from database. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Model_UserArray +{ + /** + * Returns full user name as specified in the profile if it is set, + * otherwise the nickname/loginname is returned. + * + * @param array $row User row array from database + * + * @return string Full name or username + */ + public static function getName($row) + { + if (isset($row['name']) && $row['name']) { + return $row['name']; + } + return $row['username']; + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service.php b/src/SemanticScuttle/Service.php new file mode 100644 index 0000000..cd79f2c --- /dev/null +++ b/src/SemanticScuttle/Service.php @@ -0,0 +1,60 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle base service class. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service +{ + /** + * SQL database object + * + * @var sql_db + */ + protected $db; + + + + /** + * Returns the single service instance + * + * @internal + * This function can be used once PHP 5.3 is minimum, because only + * 5.3 supports late static binding. For all lower php versions, + * we still need a copy of this method in each service class. + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new static($db); + } + return $instance; + } + +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service/AuthUser.php b/src/SemanticScuttle/Service/AuthUser.php new file mode 100644 index 0000000..9447ee4 --- /dev/null +++ b/src/SemanticScuttle/Service/AuthUser.php @@ -0,0 +1,232 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once 'Auth.php'; +require_once 'SemanticScuttle/Service/User.php'; + +/** + * SemanticScuttle extendet user management service utilizing + * the PEAR Auth package to enable authentication against + * different services, i.e. LDAP or other databases. + * + * Requires the Log packages for debugging purposes. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_AuthUser extends SemanticScuttle_Service_User +{ + /** + * PEAR Auth instance + * + * @var Auth + */ + protected $auth = null; + + /** + * If we want to debug authentication process + * + * @var boolean + */ + protected $authdebug = false; + + /** + * Authentication type (i.e. LDAP) + * + * @var string + * + * @link http://pear.php.net/manual/en/package.authentication.auth.intro-storage.php + */ + var $authtype = null; + + /** + * Authentication options + * + * @var array + * + * @link http://pear.php.net/manual/en/package.authentication.auth.intro.php + */ + var $authoptions = null; + + + + /** + * Returns the single service instance + * + * @param sql_db $db Database object + * + * @return SemanticScuttle_Service_AuthUser + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + + /** + * Create new instance + * + * @var sql_db $db Database object + */ + protected function __construct($db) + { + parent::__construct($db); + + $this->authtype = $GLOBALS['authType']; + $this->authoptions = $GLOBALS['authOptions']; + $this->authdebug = $GLOBALS['authDebug']; + + //FIXME: throw error when no authtype set? + if (!$this->authtype) { + return; + } + require_once 'Auth.php'; + $this->auth = new Auth($this->authtype, $this->authoptions); + //FIXME: check if it worked (i.e. db connection) + if ($this->authdebug) { + require_once 'Log.php'; + $this->auth->logger = Log::singleton( + 'display', '', '', array(), PEAR_LOG_DEBUG + ); + $this->auth->enableLogging = true; + } + $this->auth->setShowLogin(false); + } + + + + /** + * Return current user id based on session or cookie + * + * @return mixed Integer user id or boolean false when user + * could not be found or is not logged on. + */ + public function getCurrentUserId() + { + if (!$this->auth) { + return parent::getCurrentUserId(); + } + + //FIXME: caching? + $name = $this->auth->getUsername(); + if (!$name) { + return parent::getCurrentUserId(); + } + return $this->getIdFromUser($name); + } + + + + /** + * Try to authenticate and login a user with + * username and password. + * + * @param string $username Name of user + * @param string $password Password + * @param boolean $remember If a long-time cookie shall be set + * + * @return boolean True if the user could be authenticated, + * false if not. + */ + public function login($username, $password, $remember = false) + { + if (!$this->auth) { + return parent::login($username, $password, $remember); + } + + $ok = $this->loginAuth($username, $password); + if (!$ok) { + return false; + } + + //utilize real login method to get longtime cookie support etc. + $ok = parent::login($username, $password, $remember); + if ($ok) { + return $ok; + } + + //user must have changed password in external auth. + //we need to update the local database. + $user = $this->getUserByUsername($username); + $this->_updateuser( + $user['uId'], $this->getFieldName('password'), + $this->sanitisePassword($password) + ); + + return parent::login($username, $password, $remember); + } + + + /** + * Uses PEAR's Auth class to authenticate the user against a container. + * This allows us to use LDAP, a different database or some other + * external system. + * + * @param string $username Username to check + * @param string $password Password to check + * + * @return boolean If the user has been successfully authenticated or not + */ + public function loginAuth($username, $password) + { + $this->auth->post = array( + 'username' => $username, + 'password' => $password, + ); + $this->auth->start(); + + if (!$this->auth->checkAuth()) { + return false; + } + + //put user in database + if (!$this->getUserByUsername($username)) { + $this->addUser( + $username, $password, + $username . $GLOBALS['authEmailSuffix'] + ); + } + + return true; + } + + + + + /** + * Logs the current user out of the system. + * + * @return void + */ + public function logout() + { + parent::logout(); + + if ($this->auth) { + $this->auth->logout(); + $this->auth = null; + } + } + +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service/Bookmark.php b/src/SemanticScuttle/Service/Bookmark.php new file mode 100644 index 0000000..1315350 --- /dev/null +++ b/src/SemanticScuttle/Service/Bookmark.php @@ -0,0 +1,1161 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +require_once 'SemanticScuttle/Model/RemoteUser.php'; + +/** + * SemanticScuttle bookmark service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + + /** + * Creates a new instance. Initializes the table name. + * + * @param DB $db Database object + * + * @uses $GLOBALS['tableprefix'] + */ + public function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'bookmarks'; + } + + + + /** + * Retrieves the first bookmark whose $fieldname equals + * the given $value. + * + * @param string $fieldname Name of database field + * @param mixed $value Desired value of $fieldname + * @param boolean $all Retrieve from all users (true) + * or only bookmarks owned by the current + * user (false) + * + * @return mixed Database row array when found, boolean false + * when no bookmark matched. + * + * @TODO: merge with getBookmark() + */ + protected function _getbookmark($fieldname, $value, $all = false) + { + if (!$all) { + $userservice = SemanticScuttle_Service_Factory::get('User'); + $uId = $userservice->getCurrentUserId(); + $range = ' AND uId = '. $uId; + } else { + $range = ''; + } + + $query = 'SELECT * FROM '. $this->getTableName() + . ' WHERE ' . $fieldname . ' =' + . ' "' . $this->db->sql_escape($value) .'"' + . $range; + + if (!($dbresult = $this->db->sql_query_limit($query, 1, 0))) { + message_die( + GENERAL_ERROR, + 'Could not get bookmark', '', __LINE__, __FILE__, + $query, $this->db + ); + } + + if ($row = $this->db->sql_fetchrow($dbresult)) { + $output = $row; + } else { + $output = false; + } + $this->db->sql_freeresult($dbresult); + return $output; + } + + + + /** + * Load a single bookmark and return it. + * When a user is logged on, the returned array will contain + * keys "hasVoted" and "vote". + * + * DOES NOT RESPECT PRIVACY SETTINGS! + * + * @param integer $bid Bookmark ID + * @param boolean $include_tags If tags shall be loaded + * + * @return mixed Array with bookmark data or false in case + * of an error. + */ + function getBookmark($bid, $include_tags = false) + { + if (!is_numeric($bid)) { + return false; + } + + $userservice = SemanticScuttle_Service_Factory::get('User'); + + $query_1 = 'B.*'; + $query_2 = $this->getTableName() . ' as B'; + + //Voting system + //needs to be directly after FROM bookmarks + if ($GLOBALS['enableVoting'] && $userservice->isLoggedOn()) { + $cuid = $userservice->getCurrentUserId(); + $vs = SemanticScuttle_Service_Factory::get('Vote'); + $query_1 .= ', !ISNULL(V.bId) as hasVoted, V.vote as vote'; + $query_2 .= ' LEFT JOIN ' . $vs->getTableName() . ' AS V' + . ' ON B.bId = V.bId' + . ' AND V.uId = ' . (int)$cuid; + } + + $sql = 'SELECT ' . $query_1 . ' FROM ' + . $query_2 + .' WHERE B.bId = '. $this->db->sql_escape($bid); + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + if ($row = $this->db->sql_fetchrow($dbresult)) { + if ($include_tags) { + $b2tservice = SemanticScuttle_Service_Factory::get( + 'Bookmark2Tag' + ); + $row['tags'] = $b2tservice->getTagsForBookmark($bid); + } + $output = $row; + } else { + $output = false; + } + $this->db->sql_freeresult($dbresult); + return $output; + } + + + + /** + * Retrieves a bookmark with the given URL. + * DOES NOT RESPECT PRIVACY SETTINGS! + * + * @param string $address URL to get bookmarks for + * @param boolean $all Retrieve from all users (true) + * or only bookmarks owned by the current + * user (false) + * + * @return mixed Array with bookmark data or false in case + * of an error (i.e. not found). + * + * @uses getBookmarkByHash() + * @see getBookmarkByShortname() + */ + public function getBookmarkByAddress($address, $all = true) + { + return $this->getBookmarkByHash($this->getHash($address), $all); + } + + + + /** + * Retrieves a bookmark with the given hash. + * DOES NOT RESPECT PRIVACY SETTINGS! + * + * @param string $hash URL hash + * @param boolean $all Retrieve from all users (true) + * or only bookmarks owned by the current + * user (false) + * + * @return mixed Array with bookmark data or false in case + * of an error (i.e. not found). + * + * @see getHash() + */ + public function getBookmarkByHash($hash, $all = true) + { + return $this->_getbookmark('bHash', $hash, $all); + } + + + + /** + * Returns the hash value of a given address. + * + * @param string $address URL to hash + * @param boolean $bNormalize If the address shall be normalized before + * being hashed + * + * @return string Hash value + */ + public function getHash($address, $bNormalize = true) + { + if ($bNormalize) { + $address = $this->normalize($address); + } + return md5($address); + } + + + + /** + * Retrieves a bookmark that has a given short + * name. + * + * @param string $short Short URL name + * + * @return mixed Array with bookmark data or false in case + * of an error (i.e. not found). + */ + public function getBookmarkByShortname($short) + { + return $this->_getbookmark('bShort', $short, true); + } + + + + /** + * Counts bookmarks for a user. + * + * @param integer $uId User ID + * @param string $status Bookmark visibility/privacy settings: + * 'public', 'shared', 'private' + * or 'all' + * + * @return integer Number of bookmarks + */ + public function countBookmarks($uId, $status = 'public') + { + $sql = 'SELECT COUNT(*) as "0" FROM '. $this->getTableName(); + $sql.= ' WHERE uId = ' . intval($uId); + switch ($status) { + case 'all': + //no constraints + break; + case 'private': + $sql .= ' AND bStatus = 2'; + break; + case 'shared': + $sql .= ' AND bStatus = 1'; + break; + case 'public': + default: + $sql .= ' AND bStatus = 0'; + break; + } + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get vars', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + $count = $this->db->sql_fetchfield(0, 0); + $this->db->sql_freeresult($dbresult); + return $count; + } + + + + /** + * Check if a bookmark may be edited by the current user + * + * @param integer|array $bookmark Bookmark uId or bookmark array + * + * @return boolean True if allowed + */ + function editAllowed($bookmark) + { + if (!is_numeric($bookmark) + && (!is_array($bookmark) + || !isset($bookmark['bId']) + || !is_numeric($bookmark['bId']) + ) + ) { + return false; + } + + if (!is_array($bookmark) + && !($bookmark = $this->getBookmark($bookmark)) + ) { + return false; + } + + $userservice = SemanticScuttle_Service_Factory::get('User'); + $user = $userservice->getCurrentObjectUser(); + if ($user === null) { + return false; + } + + //user has to be either admin, or owner + if ($GLOBALS['adminsCanModifyBookmarksFromOtherUsers'] + && $userservice->isAdmin($user->username) + ) { + return true; + } else { + return ($bookmark['uId'] == $user->id); + } + } + + + + /** + * Checks if a bookmark for the given URL exists + * already + * + * @param string $address URL of bookmark to check + * @param integer $uid User id the bookmark has to belong to. + * null for all users + * + * @return boolean True when the bookmark with the given URL + * exists for the user, false if not. + */ + public function bookmarkExists($address = false, $uid = null) + { + if (!$address) { + return false; + } + + $crit = array('bHash' => $this->getHash($address)); + if (isset ($uid)) { + $crit['uId'] = $uid; + } + + $sql = 'SELECT COUNT(*) as "0" FROM ' + . $GLOBALS['tableprefix'] . 'bookmarks' + . ' WHERE '. $this->db->sql_build_array('SELECT', $crit); + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get vars', '', + __LINE__, __FILE__, $sql, $this->db + ); + } + if ($this->db->sql_fetchfield(0, 0) > 0) { + $output = true; + } else { + $output = false; + } + $this->db->sql_freeresult($dbresult); + return $output; + } + + + + /** + * Checks if the given addresses exist + * + * @param array $addresses Array of addresses + * @param integer $uid User ID the addresses shall belong to + * + * @return array Array with addresses as keys, true/false for existence + * as value + */ + public function bookmarksExist($addresses, $uid = null) + { + if (count($addresses) == 0) { + return array(); + } + + $hashes = array(); + $sql = '(0'; + foreach ($addresses as $key => $address) { + $hash = $this->getHash($address); + $hashes[$hash] = $address; + $sql .= ' OR bHash = "' + . $this->db->sql_escape($hash) + . '"'; + } + $sql .= ')'; + if ($uid !== null) { + $sql .= ' AND uId = ' . intval($uid); + } + + $sql = 'SELECT bHash, COUNT(*) as "count" FROM ' + . $this->getTableName() + . ' WHERE ' . $sql + . ' GROUP BY bHash'; + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get bookmark counts', '', + __LINE__, __FILE__, $sql, $this->db + ); + } + + $existence = array_combine( + $addresses, + array_fill(0, count($addresses), false) + ); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $existence[$hashes[$row['bHash']]] = $row['count'] > 0; + } + + $this->db->sql_freeresult($dbresult); + return $existence; + } + + + + /** + * Adds a bookmark to the database. + * + * Security checks are being made here, but no error reasons will be + * returned. It is the responsibility of the code that calls + * addBookmark() to verify the data. + * + * @param string $address Full URL of the bookmark + * @param string $title Bookmark title + * @param string $description Long bookmark description + * @param string $privateNote Private note for the user. + * @param string $status Bookmark visibility / privacy settings: + * 0 - public + * 1 - shared + * 2 - private + * @param array $tags Array of tags + * @param string $short Short URL name. May be null + * @param string $date Date when the bookmark has been created + * originally. Used in combination with + * $fromImport. Has to be a strtotime() + * interpretable string. + * @param boolean $fromApi True when api call is responsible. + * @param boolean $fromImport True when the bookmark is from an import. + * @param integer $sId ID of user who creates the bookmark. + * + * @return mixed Integer bookmark ID if saving succeeded, false in + * case of an error. Error reasons are not returned. + */ + public function addBookmark( + $address, $title, $description, $privateNote, $status, $tags, + $short = null, + $date = null, $fromApi = false, $fromImport = false, $sId = null + ) { + if ($sId === null) { + $userservice = SemanticScuttle_Service_Factory::get('User'); + $sId = $userservice->getCurrentUserId(); + } + + $address = $this->normalize($address); + if (!SemanticScuttle_Model_Bookmark::isValidUrl($address)) { + return false; + } + + /* + * Note that if date is NULL, then it's added with a date and + * time of now, and if it's present, + * it's expected to be a string that's interpretable by strtotime(). + */ + if (is_null($date) || $date == '') { + $time = time(); + } else { + $time = strtotime($date); + } + $datetime = gmdate('Y-m-d H:i:s', $time); + + if ($short === '') { + $short = null; + } + + // Set up the SQL insert statement and execute it. + $values = array( + 'uId' => intval($sId), + 'bIp' => SemanticScuttle_Model_RemoteUser::getIp(), + 'bDatetime' => $datetime, + 'bModified' => $datetime, + 'bTitle' => $title, + 'bAddress' => $address, + 'bDescription' => $description, + 'bPrivateNote' => $privateNote, + 'bStatus' => intval($status), + 'bHash' => $this->getHash($address), + 'bShort' => $short + ); + + $sql = 'INSERT INTO '. $this->getTableName() + .' ' . $this->db->sql_build_array('INSERT', $values); + $this->db->sql_transaction('begin'); + + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, + 'Could not insert bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + // Get the resultant row ID for the bookmark. + $bId = $this->db->sql_nextid($dbresult); + if (!isset($bId) || !is_int($bId)) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, + 'Could not insert bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + $uriparts = explode('.', $address); + $extension = end($uriparts); + unset($uriparts); + + $b2tservice = SemanticScuttle_Service_Factory::get('Bookmark2Tag'); + $attachok = $b2tservice->attachTags( + $bId, $tags, $fromApi, $extension, false, $fromImport + ); + if (!$attachok) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, + 'Could not insert bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + $this->db->sql_transaction('commit'); + + // Everything worked out, so return the new bookmark's bId. + return $bId; + }//public function addBookmark(..) + + + + /** + * Update an existing bookmark. + * + * @param integer $bId Bookmark ID + * @param string $address Full URL of the bookmark + * @param string $title Bookmark title + * @param string $description Long bookmark description + * @param string $privateNote Private note for the user. + * @param string $status Bookmark visibility / privacy setting: + * 0 - public + * 1 - shared + * 2 - private + * @param array $categories Array of tags + * @param string $short Short URL name. May be null. + * @param string $date Date when the bookmark has been created + * originally. Used in combination with + * $fromImport. Has to be a strtotime() + * interpretable string. + * @param boolean $fromApi True when api call is responsible. + * + * @return boolean True if all went well, false if not. + */ + public function updateBookmark( + $bId, $address, $title, $description, $privateNote, $status, + $categories, $short = null, $date = null, $fromApi = false + ) { + if (!is_numeric($bId)) { + return false; + } + + // Get the the date; note that the date is in GMT. + $moddatetime = gmdate('Y-m-d H:i:s', time()); + + $address = $this->normalize($address); + + //check if a new address ($address) doesn't already exist + // for another bookmark from the same user + $bookmark = $this->getBookmark($bId); + if ($bookmark['bAddress'] != $address + && $this->bookmarkExists($address, $bookmark['uId']) + ) { + message_die( + GENERAL_ERROR, + 'Could not update bookmark (URL already exists: ' . $address . ')', + '', __LINE__, __FILE__ + ); + return false; + } + + if ($short === '') { + $short = null; + } + + // Set up the SQL update statement and execute it. + $updates = array( + 'bModified' => $moddatetime, + 'bTitle' => $title, + 'bAddress' => $address, + 'bDescription' => $description, + 'bPrivateNote' => $privateNote, + 'bStatus' => $status, + 'bHash' => $this->getHash($address, false), + 'bShort' => $short + ); + + if (!is_null($date)) { + $datetime = gmdate('Y-m-d H:i:s', strtotime($date)); + $updates['bDatetime'] = $datetime; + } + + $sql = 'UPDATE '. $GLOBALS['tableprefix'] . 'bookmarks' + . ' SET '. $this->db->sql_build_array('UPDATE', $updates) + . ' WHERE bId = ' . intval($bId); + $this->db->sql_transaction('begin'); + + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not update bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + $uriparts = explode('.', $address); + $extension = end($uriparts); + unset($uriparts); + + $b2tservice = SemanticScuttle_Service_Factory :: get('Bookmark2Tag'); + if (!$b2tservice->attachTags($bId, $categories, $fromApi, $extension)) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not update bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + $this->db->sql_transaction('commit'); + // Everything worked out, so return true. + return true; + } + + + + /** + * Only get the bookmarks that are visible to the current user. + * Our rules: + * - if the $user is NULL, that means get bookmarks from ALL users, + * so we need to make sure to check the logged-in user's + * watchlist and get the contacts-only bookmarks from + * those users. + * If the user isn't logged-in, just get the public bookmarks. + * + * - if the $user is set and isn't the logged-in user, then get + * that user's bookmarks, and if that user is on the logged-in + * user's watchlist, get the public AND contacts-only + * bookmarks; otherwise, just get the public bookmarks. + * + * - if the $user is set and IS the logged-in user, then + * get all bookmarks. + * + * In case voting is enabled and a user is logged in, + * each bookmark array contains two additional keys: + * 'hasVoted' and 'vote'. + * + * @param integer $start Page number + * @param integer $perpage Number of bookmarks per page + * @param integer $user User ID + * @param mixed $tags Array of tags or tags separated + * by "+" signs + * @param string $terms Search terms separated by spaces + * @param string $sortOrder One of the following values: + * "date_asc", "date_desc", + * "modified_asc", "modified_desc" + * "title_desc", "title_asc", + * "url_desc", "url_asc", + * "voting_asc", "voting_desc" + * @param boolean $watched True if only watched bookmarks + * shall be returned (FIXME) + * @param integer $startdate Filter for creation date. + * SQL-DateTime value + * "YYYY-MM-DD hh:ii:ss' + * @param integer $enddate Filter for creation date. + * SQL-DateTime value + * "YYYY-MM-DD hh:ii:ss' + * @param string $hash Filter by URL hash + * + * @return array Array with two keys: 'bookmarks' and 'total'. + * First contains an array of bookmarks, 'total' + * the total number of bookmarks (without paging). + */ + public function getBookmarks( + $start = 0, $perpage = null, $user = null, $tags = null, + $terms = null, $sortOrder = null, $watched = null, + $startdate = null, $enddate = null, $hash = null + ) { + $userservice = SemanticScuttle_Service_Factory::get('User'); + $b2tservice = SemanticScuttle_Service_Factory::get('Bookmark2Tag'); + $tag2tagservice = SemanticScuttle_Service_Factory::get('Tag2Tag'); + $sId = $userservice->getCurrentUserId(); + + if ($userservice->isLoggedOn()) { + // All public bookmarks, user's own bookmarks + // and any shared with user + $privacy = ' AND ((B.bStatus = 0) OR (B.uId = '. $sId .')'; + $watchnames = $userservice->getWatchNames($sId, true); + foreach ($watchnames as $watchuser) { + $privacy .= ' OR (U.username = "'. $watchuser .'" AND B.bStatus = 1)'; + } + $privacy .= ')'; + } else { + // Just public bookmarks + $privacy = ' AND B.bStatus = 0'; + } + + // Set up the tags, if need be. + if (!is_array($tags) && !is_null($tags)) { + $tags = explode('+', trim($tags)); + } + + $tagcount = count($tags); + for ($i = 0; $i < $tagcount; $i ++) { + $tags[$i] = trim($tags[$i]); + } + + // Set up the SQL query. + $query_1 = 'SELECT DISTINCT '; + if (SQL_LAYER == 'mysql4') { + $query_1 .= 'SQL_CALC_FOUND_ROWS '; + } + $query_1 .= 'B.*, U.'. $userservice->getFieldName('username') + . ', U.name'; + + $query_2 = ' FROM '. $userservice->getTableName() .' AS U' + . ', '. $this->getTableName() .' AS B'; + + $query_3 = ' WHERE B.uId = U.'. $userservice->getFieldName('primary') . $privacy; + + if ($GLOBALS['enableVoting'] && $GLOBALS['hideBelowVoting'] !== null + && !$userservice->isAdmin($userservice->getCurrentUserId()) + ) { + $query_3 .= ' AND B.bVoting >= ' . (int)$GLOBALS['hideBelowVoting']; + } + + if (is_null($watched)) { + if (!is_null($user)) { + $query_3 .= ' AND B.uId = '. $user; + } + } else { + $arrWatch = $userservice->getWatchlist($user); + if (count($arrWatch) > 0) { + $query_3_1 = ''; + foreach ($arrWatch as $row) { + $query_3_1 .= 'B.uId = '. intval($row) .' OR '; + } + $query_3_1 = substr($query_3_1, 0, -3); + } else { + $query_3_1 = 'B.uId = -1'; + } + $query_3 .= ' AND ('. $query_3_1 .') AND B.bStatus IN (0, 1)'; + } + + $query_5 = ''; + if ($hash == null) { + $query_5.= ' GROUP BY B.bHash'; + } + + + //Voting system + //needs to be directly after FROM bookmarks + if ($GLOBALS['enableVoting'] && $userservice->isLoggedOn()) { + $cuid = $userservice->getCurrentUserId(); + $vs = SemanticScuttle_Service_Factory::get('Vote'); + $query_1 .= ', !ISNULL(V.bId) as hasVoted, V.vote as vote'; + $query_2 .= ' LEFT JOIN ' . $vs->getTableName() . ' AS V' + . ' ON B.bId = V.bId' + . ' AND V.uId = ' . (int)$cuid; + } + + switch($sortOrder) { + case 'date_asc': + $query_5 .= ' ORDER BY B.' . $GLOBALS['dateOrderField'] . ' ASC '; + break; + case 'title_desc': + $query_5 .= ' ORDER BY B.bTitle DESC '; + break; + case 'title_asc': + $query_5 .= ' ORDER BY B.bTitle ASC '; + break; + case 'voting_desc': + $query_5 .= ' ORDER BY B.bVoting DESC '; + break; + case 'voting_asc': + $query_5 .= ' ORDER BY B.bVoting ASC '; + break; + case 'url_desc': + $query_5 .= ' ORDER BY B.bAddress DESC '; + break; + case 'url_asc': + $query_5 .= ' ORDER BY B.bAddress ASC '; + break; + case 'modified_desc': + $query_5 .= ' ORDER BY B.bModified DESC '; + break; + case 'modified_asc': + $query_5 .= ' ORDER BY B.bModified ASC '; + break; + default: + $query_5 .= ' ORDER BY B.' . $GLOBALS['dateOrderField'] . ' DESC '; + } + + // Handle the parts of the query that depend on any tags that are present. + $query_4 = ''; + for ($i = 0; $i < $tagcount; $i ++) { + $query_2 .= ', '. $b2tservice->getTableName() .' AS T'. $i; + $query_4 .= ' AND ('; + + $allLinkedTags = $tag2tagservice->getAllLinkedTags( + $this->db->sql_escape($tags[$i]), '>', $user + ); + + while (is_array($allLinkedTags) && count($allLinkedTags)>0) { + $query_4 .= ' T'. $i .'.tag = "'. array_pop($allLinkedTags) .'"'; + $query_4 .= ' OR'; + } + + $query_4 .= ' T'. $i .'.tag = "'. $this->db->sql_escape($tags[$i]) .'"'; + + $query_4 .= ') AND T'. $i .'.bId = B.bId'; + //die($query_4); + } + + // Search terms + if ($terms) { + // Multiple search terms okay + $aTerms = explode(' ', $terms); + $aTerms = array_map('trim', $aTerms); + + // Search terms in tags as well when none given + if (!count($tags)) { + $query_2 .= ' LEFT JOIN '. $b2tservice->getTableName() .' AS T' + . ' ON B.bId = T.bId'; + $dotags = true; + } else { + $dotags = false; + } + + $query_4 = ''; + for ($i = 0; $i < count($aTerms); $i++) { + $query_4 .= ' AND (B.bTitle LIKE "%' + . $this->db->sql_escape($aTerms[$i]) + . '%"'; + $query_4 .= ' OR B.bDescription LIKE "%' + . $this->db->sql_escape($aTerms[$i]) + . '%"'; + //warning : search in private notes of everybody + // but private notes won't appear if not allowed. + $query_4 .= ' OR B.bPrivateNote LIKE "' + . $this->db->sql_escape($aTerms[$i]) + .'%"'; + $query_4 .= ' OR U.username = "' + . $this->db->sql_escape($aTerms[$i]) + . '"'; //exact match for username + if ($dotags) { + $query_4 .= ' OR T.tag LIKE "' + . $this->db->sql_escape($aTerms[$i]) + . '%"'; + } + $query_4 .= ')'; + } + } + + // Start and end dates + if ($startdate) { + $query_4 .= ' AND B.bDatetime > "'. $startdate .'"'; + } + if ($enddate) { + $query_4 .= ' AND B.bDatetime < "'. $enddate .'"'; + } + + // Hash + if ($hash) { + $query_4 .= ' AND B.bHash = "'. $hash .'"'; + } + + + $query = $query_1 . $query_2 . $query_3 . $query_4 . $query_5; + + $dbresult = $this->db->sql_query_limit( + $query, intval($perpage), intval($start) + ); + if (!$dbresult) { + message_die( + GENERAL_ERROR, 'Could not get bookmarks', + '', __LINE__, __FILE__, $query, $this->db + ); + } + + if (SQL_LAYER == 'mysql4') { + $totalquery = 'SELECT FOUND_ROWS() AS total'; + } else { + if ($hash) { + $totalquery = 'SELECT COUNT(*) AS total'. $query_2 + . $query_3 . $query_4; + } else { + $totalquery = 'SELECT COUNT(DISTINCT bAddress) AS total' + . $query_2 . $query_3 . $query_4; + } + } + + if (!($totalresult = $this->db->sql_query($totalquery)) + || (!($row = $this->db->sql_fetchrow($totalresult))) + ) { + message_die( + GENERAL_ERROR, 'Could not get total bookmarks', + '', __LINE__, __FILE__, $totalquery, $this->db + ); + } + + $total = $row['total']; + $this->db->sql_freeresult($totalresult); + + $bookmarks = array(); + $bookmarkids = array(); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $bookmarks[] = $row; + $bookmarkids[] = $row['bId']; + } + if (count($bookmarkids)) { + $tags = $b2tservice->getTagsForBookmarks($bookmarkids); + foreach ($bookmarks as &$bookmark) { + $bookmark['tags'] = $tags[$bookmark['bId']]; + } + } + + $this->db->sql_freeresult($dbresult); + $output = array ('bookmarks' => $bookmarks, 'total' => $total); + return $output; + } + + + + /** + * Delete the bookmark with the given id. + * Also deletes tags and votes for the given bookmark. + * + * @param integer $bookmark Bookmark ID + * + * @return boolean True if all went well, false if not + */ + public function deleteBookmark($bookmark) + { + $bookmark = (int)$bookmark; + + $query = 'DELETE FROM ' . $GLOBALS['tableprefix'] . 'bookmarks' + . ' WHERE bId = '. $bookmark; + $this->db->sql_transaction('begin'); + if (!($dbres = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not delete bookmark', + '', __LINE__, __FILE__, $query, $this->db + ); + } + + $query = 'DELETE FROM ' . $GLOBALS['tableprefix'] . 'bookmarks2tags' + . ' WHERE bId = '. $bookmark; + $this->db->sql_transaction('begin'); + if (!($dbres = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not delete tags for bookmark', + '', __LINE__, __FILE__, $query, $this->db + ); + } + + $query = 'DELETE FROM '. $GLOBALS['tableprefix'] .'votes' + . ' WHERE bid = '. $bookmark; + $this->db->sql_transaction('begin'); + if (!($dbres = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not delete votes for bookmark', + '', __LINE__, __FILE__, $query, $this->db + ); + } + + $this->db->sql_transaction('commit'); + + return true; + } + + + + /** + * Deletes all bookmarks of the given user + * + * @param integer $uId User ID + * + * @return boolean true when all went well + */ + public function deleteBookmarksForUser($uId) + { + $query = 'DELETE FROM '. $GLOBALS['tableprefix'] . 'bookmarks' + . ' WHERE uId = '. intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not delete bookmarks', + '', __LINE__, __FILE__, $query, $this->db + ); + } + + return true; + } + + + + /** + * Counts the number of bookmarks that have the same address + * as the given address. + * + * @param string|array $addresses Address/URL to look for, string + * of one address or array with + * multiple ones + * + * @return integer Number of bookmarks minus one that have the address. + * In case $addresses was an array, key-value array + * with key being the address, value said number of + * bookmarks + * + * @internal + * We do support fetching counts for multiple addresses at once + * because that allows us to reduce the number of queries + * we need in the web interface when displaying i.e. + * 10 bookmarks - only one SQL query is needed then. + */ + public function countOthers($addresses) + { + if (!$addresses) { + return false; + } + $bArray = is_array($addresses); + + $us = SemanticScuttle_Service_Factory::get('User'); + $sId = (int)$us->getCurrentUserId(); + + if ($us->isLoggedOn()) { + //All public bookmarks, user's own bookmarks + // and any shared with our user + $privacy = ' AND ((B.bStatus = 0) OR (B.uId = ' . $sId . ')'; + $watchnames = $us->getWatchNames($sId, true); + foreach ($watchnames as $watchuser) { + $privacy .= ' OR (U.username = "' + . $this->db->sql_escape($watchuser) + . '" AND B.bStatus = 1)'; + } + $privacy .= ')'; + } else { + //Just public bookmarks + $privacy = ' AND B.bStatus = 0'; + } + + $addressesSql = ' AND (0'; + foreach ((array)$addresses as $address) { + $addressesSql .= ' OR B.bHash = "' + . $this->db->sql_escape($this->getHash($address)) + . '"'; + } + $addressesSql .= ')'; + + + $sql = 'SELECT B.bAddress, COUNT(*) as count FROM ' + . $us->getTableName() . ' AS U' + . ', '. $GLOBALS['tableprefix'] . 'bookmarks AS B' + . ' WHERE U.'. $us->getFieldName('primary') .' = B.uId' + . $addressesSql + . $privacy + . ' GROUP BY B.bHash'; + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get other count', + '', __LINE__, __FILE__, $sql, $this->db + ); + } + + //be sure we also list urls in our array + // that are not found in the database + $counts = array_combine( + (array)$addresses, + array_fill(0, count((array)$addresses), 0) + ); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $counts[$row['bAddress']] + = $row['count'] > 0 ? $row['count'] - 1 : 0; + } + $this->db->sql_freeresult($dbresult); + + return $bArray ? $counts : reset($counts); + } + + + + /** + * Normalizes a given address. + * Prepends http:// if there is no protocol specified, + * and removes the trailing slash + * + * @param string $address URL to check + * + * @return string Fixed URL + */ + public function normalize($address) + { + //you know, there is "callto:" and "mailto:" + if (strpos($address, ':') === false) { + $address = 'http://'. $address; + } + + // Delete final / + if (substr($address, -1) == '/') { + $address = substr($address, 0, count($address)-2); + } + + return $address; + } + + + + /** + * Delete all bookmarks. + * Mainly used in unit tests. + * + * @return void + */ + public function deleteAll() + { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} + +?> diff --git a/src/SemanticScuttle/Service/Bookmark2Tag.php b/src/SemanticScuttle/Service/Bookmark2Tag.php new file mode 100644 index 0000000..a01b5d7 --- /dev/null +++ b/src/SemanticScuttle/Service/Bookmark2Tag.php @@ -0,0 +1,714 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle bookmark-tag combination service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + public function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'bookmarks2tags'; + } + + function isNotSystemTag($var) { + if (utf8_substr($var, 0, 7) == 'system:') + return false; + else + return true; + } + + /** + * Attach tags to a bookmark. + * + * Make sure that categories is an array of trimmed strings. + * If the categories are coming in from an API call, be sure + * that underscores are converted into strings. + * + * @param integer $bookmarkid ID of the bookmark + * @param array $tags Array of tags (strings, trimmed) + * @param boolean $fromApi If this is from an API call + * @param string $extension File extension (i.e. 'pdf') + * @param boolean $replace If existing tags for this bookmark + * are to be replaced + * @param boolean $fromImport If this is from a file import + * + * @return boolean True if all went well + */ + public function attachTags( + $bookmarkid, $tags, $fromApi = false, + $extension = null, $replace = true, $fromImport = false + ) { + if (!is_array($tags)) { + $tags = trim($tags); + if ($tags != '') { + if (substr($tags, -1) == ',') { + $tags = substr($tags, 0, -1); + } + if ($fromApi) { + $tags = explode(' ', $tags); + } else { + $tags = explode(',', $tags); + } + } else { + $tags = null; + } + } + + $tagservice = SemanticScuttle_Service_Factory::get('Tag'); + $tags = $tagservice->normalize($tags); + + $tags_count = is_array($tags)?count($tags):0; + if (is_array($tags)) { + foreach ($tags as $i => $tag) { + $tags[$i] = trim(utf8_strtolower($tags[$i])); + if ($fromApi) { + $tags[$i] = convertTag($tags[$i], 'in'); + } + } + } + + if ($tags_count > 0) { + // Remove system tags + $tags = array_filter($tags, array($this, "isNotSystemTag")); + + // Eliminate any duplicate categories + $temp = array_unique($tags); + $tags = array_values($temp); + } else { + // Unfiled + $tags[] = 'system:unfiled'; + } + + // Media and file types + if (!is_null($extension)) { + include_once 'SemanticScuttle/functions.php'; + + if ($keys = multi_array_search($extension, $GLOBALS['filetypes'])) { + $tags[] = 'system:filetype:'. $extension; + $tags[] = 'system:media:'. array_shift($keys); + } + } + + // Imported + if ($fromImport) { + $tags[] = 'system:imported'; + } + + $this->db->sql_transaction('begin'); + + if ($replace) { + if (!$this->deleteTagsForBookmark($bookmarkid)){ + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not attach tags (deleting old ones failed)', '', __LINE__, __FILE__, $sql, $this->db); + return false; + } + } + + $bs = SemanticScuttle_Service_Factory::get('Bookmark'); + $tts = SemanticScuttle_Service_Factory::get('Tag2Tag'); + + // Create links between tags + foreach ($tags as $key => $tag) { + if (strpos($tag, '=')) { + // case "=" + $pieces = explode('=', $tag); + $nbPieces = count($pieces); + if ($nbPieces <= 1) { + continue; + } + for ($i = 0; $i < $nbPieces-1; $i++) { + $bookmark = $bs->getBookmark($bookmarkid); + $uId = $bookmark['uId']; + $tts->addLinkedTags($pieces[$i], $pieces[$i+1], '=', $uId); + } + // Attach just the last tag to the bookmark + $tags[$key] = $pieces[0]; + } else { + // case ">" + $pieces = explode('>', $tag); + $nbPieces = count($pieces); + if ($nbPieces <= 1) { + continue; + } + for ($i = 0; $i < $nbPieces-1; $i++) { + $bookmark = $bs->getBookmark($bookmarkid); + $uId = $bookmark['uId']; + $tts->addLinkedTags($pieces[$i], $pieces[$i+1], '>', $uId); + } + // Attach just the last tag to the bookmark + $tags[$key] = $pieces[$nbPieces-1]; + } + } + + //after exploding, there may be duplicate keys + //since we are in a transaction, hasTag() may + // not return true for newly added duplicate tags + $tags = array_unique($tags); + + // Add the tags to the DB. + foreach ($tags as $tag) { + if ($tag == '') { + continue; + } + if ($this->hasTag($bookmarkid, $tag)) { + continue; + } + + $values = array( + 'bId' => intval($bookmarkid), + 'tag' => $tag + ); + + $sql = 'INSERT INTO '. $this->getTableName() + . ' ' . $this->db->sql_build_array('INSERT', $values); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not attach tags', + '', __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + } + $this->db->sql_transaction('commit'); + return true; + } + + function deleteTag($uId, $tag) { + $bs =SemanticScuttle_Service_Factory::get('Bookmark'); + + $query = 'DELETE FROM '. $this->getTableName(); + $query.= ' USING '. $this->getTableName() .', '. $bs->getTableName(); + $query.= ' WHERE '. $this->getTableName() .'.bId = '. $bs->getTableName() .'.bId'; + $query.= ' AND '. $bs->getTableName() .'.uId = '. $uId; + $query.= ' AND '. $this->getTableName() .'.tag = "'. $this->db->sql_escape($tag) .'"'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + } + + function deleteTagsForBookmark($bookmarkid) { + if (!is_int($bookmarkid)) { + message_die(GENERAL_ERROR, 'Could not delete tags (invalid bookmarkid)', '', __LINE__, __FILE__, $query); + return false; + } + + $query = 'DELETE FROM '. $this->getTableName() .' WHERE bId = '. intval($bookmarkid); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + } + + /* Allow deletion in admin page */ + function deleteTagsForUser($uId) { + $qmask = 'DELETE FROM %s USING %s, %s WHERE %s.bId = %s.bId AND %s.uId = %d'; + $query = sprintf($qmask, + $this->getTableName(), + $this->getTableName(), + $GLOBALS['tableprefix'].'bookmarks', + $this->getTableName(), + $GLOBALS['tableprefix'].'bookmarks', + $GLOBALS['tableprefix'].'bookmarks', + $uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + } + + + /** + * Retrieves all tags for a given bookmark except system tags. + * + * @param integer $bookmarkid ID of the bookmark + * @param boolean $systemTags Return "system:*" tags or not + * + * @return array Array of tags + */ + public function getTagsForBookmark($bookmarkid, $systemTags = false) + { + if (!is_numeric($bookmarkid)) { + message_die( + GENERAL_ERROR, 'Could not get tags (invalid bookmarkid)', + '', __LINE__, __FILE__, $query + ); + return false; + } + + $query = 'SELECT tag FROM ' . $this->getTableName() + . ' WHERE bId = ' . intval($bookmarkid); + if (!$systemTags) { + $query .= ' AND LEFT(tag, 7) <> "system:"'; + } + $query .= ' ORDER BY id ASC'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not get tags', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $tags = array(); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $tags[] = $row['tag']; + } + $this->db->sql_freeresult($dbresult); + return $tags; + } + + + /** + * Retrieves all tags for an array of bookmark IDs + * + * @param array $bookmarkids Array of bookmark IDs + * + * @return array Array of tag arrays. Key is bookmark ID. + */ + public function getTagsForBookmarks($bookmarkids) + { + if (!is_array($bookmarkids)) { + message_die( + GENERAL_ERROR, 'Could not get tags (invalid bookmarkids)', + '', __LINE__, __FILE__, $query + ); + return false; + } else if (count($bookmarkids) == 0) { + return array(); + } + + $query = 'SELECT tag, bId FROM ' . $this->getTableName() + . ' WHERE bId IN (' . implode(',', $bookmarkids) . ')' + . ' AND LEFT(tag, 7) <> "system:"' + . ' ORDER BY id, bId ASC'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not get tags', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $tags = array_combine( + $bookmarkids, + array_fill(0, count($bookmarkids), array()) + ); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $tags[$row['bId']][] = $row['tag']; + } + $this->db->sql_freeresult($dbresult); + return $tags; + } + + + function &getTags($userid = NULL) { + $userservice =SemanticScuttle_Service_Factory::get('User'); + $logged_on_user = $userservice->getCurrentUserId(); + + $query = 'SELECT T.tag, COUNT(B.bId) AS bCount FROM '. $GLOBALS['tableprefix'] .'bookmarks AS B INNER JOIN '. $userservice->getTableName() .' AS U ON B.uId = U.'. $userservice->getFieldName('primary') .' INNER JOIN '. $GLOBALS['tableprefix'] .'bookmarks2tags AS T ON B.bId = T.bId'; + + $conditions = array(); + if (!is_null($userid)) { + $conditions['U.'. $userservice->getFieldName('primary')] = intval($userid); + if ($logged_on_user != $userid) + $conditions['B.bStatus'] = 0; + } else { + $conditions['B.bStatus'] = 0; + } + + $query .= ' WHERE '. $this->db->sql_build_array('SELECT', $conditions) .' AND LEFT(T.tag, 7) <> "system:" GROUP BY T.tag ORDER BY bCount DESC, tag'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + + + // Returns the tags related to the specified tags; i.e. attached to the same bookmarks + function &getRelatedTags($tags, $for_user = NULL, $logged_on_user = NULL, $limit = 10) { + $conditions = array(); + // Only count the tags that are visible to the current user. + if ($for_user != $logged_on_user || is_null($for_user)) + $conditions['B.bStatus'] = 0; + + if (!is_null($for_user)) + $conditions['B.uId'] = $for_user; + + // Set up the tags, if need be. + if (is_numeric($tags)) + $tags = NULL; + if (!is_array($tags) and !is_null($tags)) + $tags = explode('+', trim($tags)); + + $tagcount = count($tags); + for ($i = 0; $i < $tagcount; $i++) { + $tags[$i] = trim($tags[$i]); + } + + // Set up the SQL query. + $query_1 = 'SELECT DISTINCTROW T0.tag, COUNT(B.bId) AS bCount FROM '. $GLOBALS['tableprefix'] .'bookmarks AS B, '. $this->getTableName() .' AS T0'; + $query_2 = ''; + $query_3 = ' WHERE B.bId = T0.bId '; + if (count($conditions) > 0) + $query_4 = ' AND '. $this->db->sql_build_array('SELECT', $conditions); + else + $query_4 = ''; + // Handle the parts of the query that depend on any tags that are present. + for ($i = 1; $i <= $tagcount; $i++) { + $query_2 .= ', '. $this->getTableName() .' AS T'. $i; + $query_4 .= ' AND T'. $i .'.bId = B.bId AND T'. $i .'.tag = "'. $this->db->sql_escape($tags[$i - 1]) .'" AND T0.tag <> "'. $this->db->sql_escape($tags[$i - 1]) .'"'; + } + $query_5 = ' AND LEFT(T0.tag, 7) <> "system:" GROUP BY T0.tag ORDER BY bCount DESC, T0.tag'; + $query = $query_1 . $query_2 . $query_3 . $query_4 . $query_5; + + if (! ($dbresult = $this->db->sql_query_limit($query, $limit)) ){ + message_die(GENERAL_ERROR, 'Could not get related tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + + // Returns the most popular tags used for a particular bookmark hash + function &getRelatedTagsByHash($hash, $limit = 20) { + $userservice = SemanticScuttle_Service_Factory :: get('User'); + $sId = $userservice->getCurrentUserId(); + // Logged in + if ($userservice->isLoggedOn()) { + $arrWatch = $userservice->getWatchList($sId); + // From public bookmarks or user's own + $privacy = ' AND ((B.bStatus = 0) OR (B.uId = '. $sId .')'; + // From shared bookmarks in watchlist + foreach ($arrWatch as $w) { + $privacy .= ' OR (B.uId = '. $w .' AND B.bStatus = 1)'; + } + $privacy .= ') '; + // Not logged in + } else { + $privacy = ' AND B.bStatus = 0 '; + } + + $query = 'SELECT T.tag, COUNT(T.tag) AS bCount FROM '.$GLOBALS['tableprefix'].'bookmarks AS B LEFT JOIN '.$GLOBALS['tableprefix'].'bookmarks2tags AS T ON B.bId = T.bId WHERE B.bHash = \''. $this->db->sql_escape($hash) .'\' '. $privacy .'AND LEFT(T.tag, 7) <> "system:" GROUP BY T.tag ORDER BY bCount DESC'; + + if (!($dbresult = $this->db->sql_query_limit($query, $limit))) { + message_die(GENERAL_ERROR, 'Could not get related tags for this hash', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + + + + /** + * Returns the tags used by admin users + * + * @param integer $limit Number of tags to return + * @param integer $logged_on_user ID of the user that's currently logged in. + * If the logged in user equals the $user to find + * tags for, tags of private bookmarks are + * returned. + * @param integer $days Bookmarks have to be changed in the last X days + * if their tags shall count + * @param string $beginsWith The tag name shall begin with that string + * + * @return array Array of found tags. Each tag entry is an array with two keys, + * 'tag' (tag name) and 'bCount'. + * + * @see getPopularTags() + */ + public function getAdminTags( + $limit = 30, $logged_on_user = null, $days = null, $beginsWith = null + ) { + // look for admin ids + $userservice = SemanticScuttle_Service_Factory::get('User'); + $adminIds = $userservice->getAdminIds(); + + // ask for their tags + return $this->getPopularTags( + $adminIds, $limit, $logged_on_user, $days, $beginsWith + ); + } + + + + + /** + * Returns the tags used by users that are part of the user's watchlist, + * and the current user's own tags. + * + * @param integer $user ID of the user to get the watchlist from + * @param integer $limit Number of tags to return + * @param integer $logged_on_user ID of the user that's currently logged in. + * If set, that user is added to the list of + * people to get the tags from + * @param integer $days Bookmarks have to be changed in the last X days + * if their tags shall count + * @param string $beginsWith The tag name shall begin with that string + * + * @return array Array of found tags. Each tag entry is an array with two keys, + * 'tag' (tag name) and 'bCount'. + * + * @see getPopularTags() + */ + public function getContactTags( + $user, $limit = 30, $logged_on_user = null, $days = null, + $beginsWith = null + ) { + // look for contact ids + $userservice = SemanticScuttle_Service_Factory::get('User'); + $contacts = $userservice->getWatchlist($user); + + // add the user (to show him also his own tags) + if (!is_null($logged_on_user)) { + $contacts[] = $logged_on_user; + } + + // ask for their tags + return $this->getPopularTags( + $contacts, $limit, $logged_on_user, $days, $beginsWith + ); + } + + + + /** + * The the most popular tags and their usage count + * + * @param mixed $user Integer user ID or array of user IDs to limit tag + * finding to + * @param integer $limit Number of tags to return + * @param integer $logged_on_user ID of the user that's currently logged in. + * If the logged in user equals the $user to find + * tags for, tags of private bookmarks are + * returned. + * @param integer $days Bookmarks have to be changed in the last X days + * if their tags shall count + * @param string $beginsWith The tag name shall begin with that string + * + * @return array Array of found tags. Each tag entry is an array with two keys, + * 'tag' (tag name) and 'bCount'. + * + * @see getAdminTags() + * @see getContactTags() + */ + public function getPopularTags( + $user = null, $limit = 30, $logged_on_user = null, $days = null, + $beginsWith = null + ) { + $query = 'SELECT' + . ' T.tag, COUNT(T.bId) AS bCount' + . ' FROM ' + . $this->getTableName() . ' AS T' + . ', ' . $GLOBALS['tableprefix'] . 'bookmarks AS B' + . ' WHERE'; + + if (is_null($user) || $user === false) { + $query .= ' B.bId = T.bId AND B.bStatus = 0'; + } else if (is_array($user)) { + $query .= ' (1 = 0'; //tricks + foreach ($user as $u) { + if (!is_numeric($u)) { + continue; + } + $query .= ' OR (' + . ' B.uId = ' . $this->db->sql_escape($u) + . ' AND B.bId = T.bId'; + if ($u !== $logged_on_user) { + //public bookmarks of others + $query .= ' AND B.bStatus = 0'; + } + $query .= ')'; + } + $query .= ' )'; + } else { + $query .= ' B.uId = ' . $this->db->sql_escape($user) + . ' AND B.bId = T.bId'; + if ($user !== $logged_on_user) { + $query .= ' AND B.bStatus = 0'; + } + } + + if (is_int($days)) { + $query .= ' AND B.bDatetime > "' + . gmdate('Y-m-d H:i:s', time() - (86400 * $days)) + . '"'; + } + + if (!is_null($beginsWith)) { + $query .= ' AND T.tag LIKE \'' + . $this->db->sql_escape($beginsWith) + . '%\''; + } + + $query .= ' AND LEFT(T.tag, 7) <> "system:"' + . ' GROUP BY T.tag' + . ' ORDER BY bCount DESC, tag'; + + if (!($dbresult = $this->db->sql_query_limit($query, $limit))) { + message_die( + GENERAL_ERROR, 'Could not get popular tags', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + + + + function hasTag($bookmarkid, $tag) { + $query = 'SELECT COUNT(*) AS tCount FROM '. $this->getTableName() .' WHERE bId = '. intval($bookmarkid) .' AND tag ="'. $this->db->sql_escape($tag) .'"'; + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die(GENERAL_ERROR, 'Could not find tag', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $output = false; + if ($row = $this->db->sql_fetchrow($dbresult)) { + if ($row['tCount'] > 0) { + $output = true; + } + } + + $this->db->sql_freeresult($dbresult); + return $output; + } + + function renameTag($userid, $old, $new, $fromApi = false) { + $bookmarkservice =SemanticScuttle_Service_Factory::get('Bookmark'); + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + + if (is_null($userid) || is_null($old) || is_null($new)) + return false; + + // Find bookmarks with old tag + $bookmarksInfo = $bookmarkservice->getBookmarks(0, NULL, $userid, $old); + $bookmarks = $bookmarksInfo['bookmarks']; + + // Delete old tag + $this->deleteTag($userid, $old); + + // Attach new tags + $new = $tagservice->normalize($new); + + foreach (array_keys($bookmarks) as $key) { + $row = $bookmarks[$key]; + $this->attachTags($row['bId'], $new, $fromApi, NULL, false); + } + + return true; + } + + function &tagCloud($tags = NULL, $steps = 5, $sizemin = 90, $sizemax = 225, $sortOrder = NULL) { + + if (is_null($tags) || count($tags) < 1) { + $output = false; + return $output; + } + + $min = $tags[count($tags) - 1]['bCount']; + $max = $tags[0]['bCount']; + + for ($i = 1; $i <= $steps; $i++) { + $delta = ($max - $min) / (2 * $steps - $i); + $limit[$i] = $i * $delta + $min; + } + $sizestep = ($sizemax - $sizemin) / $steps; + foreach ($tags as $row) { + $next = false; + for ($i = 1; $i <= $steps; $i++) { + if (!$next && $row['bCount'] <= $limit[$i]) { + $size = $sizestep * ($i - 1) + $sizemin; + $next = true; + } + } + $tempArray = array('size' => $size .'%'); + $row = array_merge($row, $tempArray); + $output[] = $row; + } + + if ($sortOrder == 'alphabet_asc') { + usort($output, create_function('$a,$b','return strcasecmp(utf8_deaccent($a["tag"]), utf8_deaccent($b["tag"]));')); + } + + return $output; + } + + + + /** + * Deletes all tags in bookmarks2tags + * + * @return void + */ + public function deleteAll() + { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} +?> diff --git a/src/SemanticScuttle/Service/Cache.php b/src/SemanticScuttle/Service/Cache.php new file mode 100644 index 0000000..fa719d3 --- /dev/null +++ b/src/SemanticScuttle/Service/Cache.php @@ -0,0 +1,72 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle caching service + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Cache extends SemanticScuttle_Service +{ + var $basedir; + var $fileextension = '.cache'; + + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + protected function __construct() + { + $this->basedir = $GLOBALS['dir_cache']; + } + + function Start($hash, $time = 300) { + $cachefile = $this->basedir .'/'. $hash . $this->fileextension; + if (file_exists($cachefile) && time() < filemtime($cachefile) + $time) { + @readfile($cachefile); + echo "\n<!-- Cached: ". date('r', filemtime($cachefile)) ." -->\n"; + unset($cachefile); + exit; + } + ob_start("ob_gzhandler"); + } + + function End($hash) { + $cachefile = $this->basedir .'/'. $hash . $this->fileextension; + $handle = fopen($cachefile, 'w'); + fwrite($handle, ob_get_contents()); + fclose($handle); + ob_flush(); + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service/CommonDescription.php b/src/SemanticScuttle/Service/CommonDescription.php new file mode 100644 index 0000000..d03b34a --- /dev/null +++ b/src/SemanticScuttle/Service/CommonDescription.php @@ -0,0 +1,229 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle common bookmark description service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_CommonDescription extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + public function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'commondescription'; + } + + function addTagDescription($tag, $desc, $uId, $time) { + $tag = self::getSortedTag($tag); + + // Check if no modification + $lastDesc = $this->getLastTagDescription($tag); + if($lastDesc['cdDescription'] == $desc) { + return true; + } + + // If modification + $datetime = gmdate('Y-m-d H:i:s', $time); + $values = array('tag'=>$tag, 'cdDescription'=>$desc, 'uId'=>$uId, 'cdDatetime'=>$datetime); + $sql = 'INSERT INTO '. $this->getTableName() .' '. $this->db->sql_build_array('INSERT', $values); + + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add tag description', '', __LINE__, __FILE__, $sql, $this->db); + return false; + } + + return true; + } + + function getLastTagDescription($tag) { + $tag = self::getSortedTag($tag); + + $query = "SELECT *"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= ' WHERE tag=\'' . $this->db->sql_escape($tag) . "'"; + $query.= " ORDER BY cdDatetime DESC"; + + if (!($dbresult = $this->db->sql_query_limit($query, 1, 0))) { + message_die(GENERAL_ERROR, 'Could not get tag description', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + if ($row) { + return $row; + } else { + return false; + } + } + + function getAllTagsDescription($tag) { + $query = "SELECT *"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= ' WHERE tag=\'' . $this->db->sql_escape($tag) . "'"; + $query.= " ORDER BY cdDatetime DESC"; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tag descriptions', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $res = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $res; + } + + function getDescriptionById($cdId) { + $query = "SELECT *"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= ' WHERE cdId=\'' . $this->db->sql_escape($cdId) . "'"; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tag descriptions', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + if ($row = $this->db->sql_fetchrow($dbresult)) { + $this->db->sql_freeresult($dbresult); + return $row; + } else { + return false; + } + + } + + function addBookmarkDescription($bHash, $title, $desc, $uId, $time) { + // Check if no modification + $lastDesc = $this->getLastBookmarkDescription($bHash); + if($lastDesc['cdTitle'] == $title && $lastDesc['cdDescription'] == $desc) { + return true; + } + + // If modification + $datetime = gmdate('Y-m-d H:i:s', $time); + $values = array('bHash'=>$bHash, 'cdTitle'=>$title, 'cdDescription'=>$desc, 'uId'=>$uId, 'cdDatetime'=>$datetime); + $sql = 'INSERT INTO '. $this->getTableName() .' '. $this->db->sql_build_array('INSERT', $values); + + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add bookmark description', '', __LINE__, __FILE__, $sql, $this->db); + return false; + } + return true; + } + + function getLastBookmarkDescription($bHash) { + $query = "SELECT *"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= ' WHERE bHash=\'' . $this->db->sql_escape($bHash) . "'"; + $query.= " ORDER BY cdDatetime DESC"; + + if (!($dbresult = $this->db->sql_query_limit($query, 1, 0))) { + message_die(GENERAL_ERROR, 'Could not get bookmark description', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + if ($row) { + return $row; + } else { + return false; + } + } + + function getAllBookmarksDescription($bHash) { + $query = "SELECT *"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= ' WHERE bHash=\'' . $this->db->sql_escape($bHash) . "'"; + $query.= " ORDER BY cdDatetime DESC"; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get bookmark descriptions', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $rowset = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $rowset; + } + + function deleteDescriptionsForUser($uId){ + $query = 'DELETE FROM '. $this->getTableName() . ' WHERE uId = '. intval($uId); + + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not delete user descriptions', '', + __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + } + + + function deleteAll() { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + + + + /** + * Sorts a tag combination. + * + * Multiple tags are separated by "+" signs. Semantically, + * tag combinations like "blue+flower" and "flower+blue" are identical. + * This method sorts the single tags so i.e. the common bookmark description + * of "blue+flower" is also shown for "flower+blue". + * + * @param string $tag Single tag or tag combination ("+") + * + * @return string Sorted tag combination + */ + protected static function getSortedTag($tag) + { + $tags = explode('+', $tag); + sort($tags); + return implode('+', $tags); + } + +} +?> diff --git a/src/SemanticScuttle/Service/Factory.php b/src/SemanticScuttle/Service/Factory.php new file mode 100644 index 0000000..b661cdb --- /dev/null +++ b/src/SemanticScuttle/Service/Factory.php @@ -0,0 +1,154 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Connect to the database and build services. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Factory +{ + /** + * Array of service instances. + * Key is service name (i.e. "Bookmark") + * + * @var array + */ + protected static $instances = array(); + + /** + * Database connection + * + * @var sql_qb + */ + protected static $db = null; + + /** + * Array of service names -> new service class + * in case you want to overwrite the services. + * + * Key is the old service name (i.e. "Bookmark"), + * value the new class name, e.g. + * "My_Cool_Own_BookmarkService" + * + * @var array + */ + public static $serviceoverrides = array(); + + + + /** + * Returns the service for the given name. + * + * @param string $name Service name (i.e. "Bookmark") + * + * @return SemanticScuttle_Service Service object + */ + public static function get($name) + { + self::loadDb(); + self::loadService($name); + return self::$instances[$name]; + } + + + + /** + * Loads service with the given name into + * self::$instances[$name]. + * + * @param string $name Service name (i.e. 'Bookmark') + * + * @return void + */ + protected static function loadService($name) + { + if (isset(self::$instances[$name])) { + return; + } + + if (isset(self::$serviceoverrides[$name])) { + $class = self::$serviceoverrides[$name]; + } else { + $class = 'SemanticScuttle_Service_' . $name; + } + + if (!class_exists($class)) { + //PEAR classname to filename rule + $file = str_replace('_', '/', $class) . '.php'; + include_once $file; + } + + self::$instances[$name] = call_user_func( + array($class, 'getInstance'), + self::$db + ); + } + + + + /** + * Loads self::$db if it is not loaded already. + * Dies if the connection could not be established. + * + * @return void + */ + protected static function loadDb() + { + global $dbhost, $dbuser, $dbpass, $dbname, + $dbport, $dbpersist, $dbtype, $dbneedssetnames; + + if (self::$db !== null) { + return; + } + include_once 'SemanticScuttle/db/'. $dbtype .'.php'; + $db = new sql_db(); + $db->sql_connect( + $dbhost, $dbuser, $dbpass, $dbname, $dbport, $dbpersist + ); + if (!$db->db_connect_id) { + message_die( + CRITICAL_ERROR, + 'Could not connect to the database', + self::$db + ); + } + + $dbneedssetnames && $db->sql_query('SET NAMES UTF8'); + + self::$db = $db; + } + + + + /** + * Returns sql database object + * + * @return sql_db Database instance + */ + public static function getDb() + { + self::loadDb(); + return self::$db; + } + +} +?> diff --git a/src/SemanticScuttle/Service/SearchHistory.php b/src/SemanticScuttle/Service/SearchHistory.php new file mode 100644 index 0000000..ac0b1c7 --- /dev/null +++ b/src/SemanticScuttle/Service/SearchHistory.php @@ -0,0 +1,280 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle search history service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_SearchHistory extends SemanticScuttle_DbService +{ + /** + * Size of the search history. + * If the number of logged searches is larger than this, + * adding a new search will delete the oldest one automatically. + * + * Use -1 to deactivate automatic deletion. + * + * @var integer + */ + public $sizeSearchHistory; + + + + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + + /** + * Creates a new instance. + * + * Sets $this->sizeSearchHistory to $GLOBALS['sizeSearchHistory'] or 10 + * if the global variable is not defined. + * + * @param DB $db Database object + */ + public function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] . 'searchhistory'; + if (isset($GLOBALS['sizeSearchHistory'])) { + $this->sizeSearchHistory = $GLOBALS['sizeSearchHistory']; + } else { + $this->sizeSearchHistory = 10; + } + } + + + + /** + * Adds a new search to the search history. + * Automatically deletes the oldest search when the number of + * searches is larger than $sizeSearchHistory. + * + * @param string $terms Search terms separated by spaces + * @param string $range - 'all' - search was in all bookmarks + * - 'watchlist' - searched in watchlist + * - any username to show that the search happened + * in his own bookmarks. + * @param integer $nbResults Number of search result rows + * @param integer $uId ID of user that searched + * + * @return boolean True if it has been added, false if not + */ + public function addSearch($terms, $range, $nbResults, $uId = 0) + { + if (strlen($terms) == 0) { + return false; + } + $datetime = gmdate('Y-m-d H:i:s', time()); + + //Insert values + $values = array( + 'shTerms' => $terms, + 'shRange' => $range, + 'shDatetime' => $datetime, + 'shNbResults' => $nbResults, + 'uId' => $uId + ); + $sql = 'INSERT INTO ' . $this->getTableName() + . ' ' . $this->db->sql_build_array('INSERT', $values); + + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not insert search history', + '', __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + + if ($this->sizeSearchHistory != -1 + && $this->countSearches() > $this->sizeSearchHistory + ) { + $this->deleteOldestSearch(); + } + + return true; + } + + + + /** + * Returns searches with the given features. + * + * @param string $range - 'all' - search was in all bookmarks + * - 'watchlist' - searched in watchlist + * - any username to show that the search happened + * in his own bookmarks. + * @param integer $uId Id of the user who searched. null for any users + * @param integer $nb Number of bookmarks to retrieve (paging) + * @param integer $start Number of bookmark to begin with (paging) + * @param boolean $distinct If the search terms shall be distinct + * @param boolean $withResults Only return searches that had at least one result + * + * @return array Array of search history database rows + */ + public function getAllSearches( + $range = null, $uId = null, $nb = null, + $start = null, $distinct = false, $withResults = false + ) { + $sql = 'SELECT DISTINCT(shTerms),' + . ' shId, shRange, shNbResults, shDatetime, uId'; + $sql.= ' FROM '. $this->getTableName(); + $sql.= ' WHERE 1=1'; + if ($range != null) { + $sql.= ' AND shRange = "'.$range.'"'; + } else { + $sql.= ' AND shRange = "all"'; + } + if ($uId != null) { + $sql.= ' AND uId = '.$uId; + } + if ($withResults == true) { + $sql.= ' AND shNbResults > 0'; + } + if ($distinct) { + $sql.= ' GROUP BY shTerms'; + } + $sql.= ' ORDER BY shId DESC'; + + if (!($dbresult = $this->db->sql_query_limit($sql, $nb, $start))) { + message_die( + GENERAL_ERROR, 'Could not get searches', + '', __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + + $searches = array(); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $searches[] = $row; + } + $this->db->sql_freeresult($dbresult); + return $searches; + } + + + + /** + * Counts the number of searches that have been made in total. + * + * @return integer Number of searches + */ + public function countSearches() + { + $sql = 'SELECT COUNT(*) AS `total` FROM '. $this->getTableName(); + if (!($dbresult = $this->db->sql_query($sql)) + || (!($row = $this->db->sql_fetchrow($dbresult))) + ) { + message_die( + GENERAL_ERROR, 'Could not get total searches', + '', __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + $this->db->sql_freeresult($dbresult); + return $row['total']; + } + + + + /** + * This function allows to limit the number of saved searches + * by deleting the oldest one + * + * @return boolean True when all went well, false in case of an error + */ + public function deleteOldestSearch() + { + $sql = 'DELETE FROM '.$this->getTableName(); + // warning: here the limit is important + $sql .= ' ORDER BY shId ASC LIMIT 1'; + + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not delete bookmarks', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + return true; + } + + + + /** + * Deletes all search history entries that have been made by the user + * with the given ID. + * + * @param integer $uId ID of the user + * + * @return boolean True when all went well, false in case of an error + */ + public function deleteSearchHistoryForUser($uId) + { + $query = 'DELETE FROM '. $this->getTableName() + . ' WHERE uId = ' . intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not delete search history', '', + __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + return true; + } + + + + /** + * Deletes all search history entries. + * + * @return void + */ + public function deleteAll() + { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} +?> diff --git a/src/SemanticScuttle/Service/Tag.php b/src/SemanticScuttle/Service/Tag.php new file mode 100644 index 0000000..bd6bf70 --- /dev/null +++ b/src/SemanticScuttle/Service/Tag.php @@ -0,0 +1,160 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle tag management service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Tag extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + public function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'tags'; + } + + function getDescription($tag, $uId) { + $query = 'SELECT tag, uId, tDescription'; + $query.= ' FROM '.$this->getTableName(); + $query.= ' WHERE tag = \''. $this->db->sql_escape($tag) . "'"; + $query.= ' AND uId = ' . intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tag description', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + if ($row) { + return $row; + } else { + return array('tDescription'=>''); + } + } + + function existsDescription($tag, $uId) { + $query = 'SELECT tag, uId, tDescription'; + $query.= ' FROM '.$this->getTableName(); + $query.= ' WHERE tag = \'' . $this->db->sql_escape($tag) . "'"; + $query.= ' AND uId = "' . intval($uId) . '"'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tag description', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + if ($row) { + return true; + } else { + return false; + } + } + + function getAllDescriptions($tag) { + $query = 'SELECT tag, uId, tDescription'; + $query.= ' FROM '.$this->getTableName(); + $query.= ' WHERE tag = \''. $this->db->sql_escape($tag) . "'"; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not get tag description', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $rowset = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $rowset; + } + + function updateDescription($tag, $uId, $desc) { + if($this->existsDescription($tag, $uId)) { + $query = 'UPDATE '.$this->getTableName(); + $query.= ' SET tDescription= \'' . $this->db->sql_escape($desc) . "'"; + $query.= ' WHERE tag=\'' . $this->db->sql_escape($tag) . "' AND uId=" . intval($uId); + } else { + $values = array('tag'=>$tag, 'uId'=>$uId, 'tDescription'=>$desc); + $query = 'INSERT INTO '. $this->getTableName() .' '. $this->db->sql_build_array('INSERT', $values); + } + + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not delete bookmarks', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $this->db->sql_transaction('commit'); + return true; + } + + function renameTag($uId, $oldName, $newName) { + $newname = $this->normalize($newName); + + $query = 'UPDATE `'. $this->getTableName() .'`'; + $query.= ' SET tag=\'' . $this->db->sql_escape($newName) . "'"; + $query.= ' WHERE tag=\'' . $this->db->sql_escape($oldName) . "'"; + $query.= ' AND uId=' . intval($uId); + $this->db->sql_query($query); + return true; + } + + /* normalize the input tags which could be a string or an array*/ + function normalize($tags) { + //clean tags from strange characters + $tags = str_replace(array('"', '\'', '/'), "_", $tags); + + //normalize + if(!is_array($tags)) { + $tags = utf8_strtolower(trim($tags)); + } else { + $tags = array_filter($tags);//remove empty values + foreach($tags as $i => $tag) { + $tags[$i] = utf8_strtolower(trim($tags[$i])); + } + } + return $tags; + } + + function deleteAll() { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} +?> diff --git a/src/SemanticScuttle/Service/Tag2Tag.php b/src/SemanticScuttle/Service/Tag2Tag.php new file mode 100644 index 0000000..9dddc44 --- /dev/null +++ b/src/SemanticScuttle/Service/Tag2Tag.php @@ -0,0 +1,470 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle tag-to-tag service which works with tag relations. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Tag2Tag extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + function __construct($db) + { + $this->db =$db; + $this->tablename = $GLOBALS['tableprefix'] .'tags2tags'; + } + + /** + * Add a tag-to-tag link relation + * + * @param string $tag1 First tag + * @param string $tag2 Second tag + * @param string $relationType Relation: + * "=" (both tags are equal) + * ">" (tag2 is part of tag1) + * @param integer $uId User ID + * + * @return boolean True when it was created, false in case of + * an error or when the link already existed. + */ + public function addLinkedTags($tag1, $tag2, $relationType, $uId) + { + $tagservice = SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + $tag2 = $tagservice->normalize($tag2); + + if ($tag1 == $tag2 || strlen($tag1) == 0 || strlen($tag2) == 0 + || ($relationType != '>' && $relationType != '=') + || !is_numeric($uId) || $uId<=0 + || ($this->existsLinkedTags($tag1, $tag2, $relationType, $uId)) + ) { + return false; + } + + $values = array( + 'tag1' => $tag1, + 'tag2' => $tag2, + 'relationType' => $relationType, + 'uId' => $uId + ); + $query = 'INSERT INTO ' . $this->getTableName() + . ' ' . $this->db->sql_build_array('INSERT', $values); + + //die($query); + if (!($dbresult = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not attach tag to tag', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + $this->db->sql_transaction('commit'); + + // Update stats and cache + $this->update($tag1, $tag2, $relationType, $uId); + + return true; + } + + + + /** + * Same as getLinkedTags(), but only tags that have been created + * by admin users are returned. + * + * @param string $tag Tag to get related tags for + * @param string $relationType Type of tag-to-tag relation: >, < or = + * @param boolean $inverseRelation Reverse relation (parent -> child) + * @param array $stopList Array of tags that shall not be returned + * + * @return array Array of tag names + * + * @see getLinkedTags() + */ + public function getAdminLinkedTags( + $tag, $relationType, $inverseRelation = false, $stopList = array() + ) { + // look for admin ids + $userservice = SemanticScuttle_Service_Factory :: get('User'); + $adminIds = $userservice->getAdminIds(); + + //ask for their linked tags + return $this->getLinkedTags( + $tag, $relationType, $adminIds, $inverseRelation, $stopList + ); + } + + + + /** + * Returns an array of tags that are in relation to the given $tag. + * + * @param string $tag Tag to get related tags for + * @param string $relationType Type of tag-to-tag relation: >, < or = + * @param boolean $inverseRelation Reverse relation (parent -> child) + * @param array $stopList Array of tags that shall not be returned + * + * @return array Array of tag names + */ + public function getLinkedTags( + $tag, $relationType, $uId = null, $inverseRelation = false, $stopList = array() + ) { + // Set up the SQL query. + if($inverseRelation) { + $queriedTag = "tag1"; + $givenTag = "tag2"; + } else { + $queriedTag = "tag2"; + $givenTag = "tag1"; + } + + $query = "SELECT DISTINCT ". $queriedTag ." as 'tag'"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= " WHERE 1=1"; + if($tag !=null) { + $query.= " AND ". $givenTag ." = '". $this->db->sql_escape($tag) ."'"; + } + if($relationType) { + $query.= " AND relationType = '". $this->db->sql_escape($relationType) ."'"; + } + if(is_array($uId)) { + $query.= " AND ( 1=0 "; //tricks always false + foreach($uId as $u) { + $query.= " OR uId = '".intval($u)."'"; + } + $query.= " ) "; + } elseif($uId != null) { + $query.= " AND uId = '".intval($uId)."'"; + } + //die($query); + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not get related tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $rowset = $this->db->sql_fetchrowset($dbresult); + $output = array(); + foreach($rowset as $row) { + if(!in_array($row['tag'], $stopList)) { + $output[] = $row['tag']; + } + } + + //bijective case for '=' + if($relationType == '=' && $inverseRelation == false) { + //$stopList[] = $tag; + $bijectiveOutput = $this->getLinkedTags($tag, $relationType, $uId, true, $stopList); + $output = array_merge($output, $bijectiveOutput); + //$output = array_unique($output); // remove duplication + } + + $this->db->sql_freeresult($dbresult); + return $output; + } + + /* + * Returns all linked tags (all descendants if relation is >, + * all synonyms if relation is = ) + * $stopList allows to avoid cycle (a > b > a) between tags + */ + function getAllLinkedTags($tag1, $relationType, $uId, $stopList=array()) { + if(in_array($tag1, $stopList) || $tag1 == '') { + return array(); + } + + // try to find data in cache + $tcs = SemanticScuttle_Service_Factory::get('TagCache'); + if(count($stopList) == 0) { + $activatedCache = true; + } else { + $activatedCache = false; + } + + // look for existing links + $stopList[] = $tag1; + $linkedTags = $this->getLinkedTags($tag1, $relationType, $uId, false, $stopList); + if($relationType != '=') { + $linkedTags = array_merge($linkedTags, $this->getLinkedTags($tag1, '=', $uId, false, $stopList)); + } + + if(count($linkedTags) == 0) { + return array(); + + } else { + // use cache if possible + if($activatedCache) { + if($relationType == '>') { + $output = $tcs->getChildren($tag1, $uId); + } elseif($relationType == '=') { + $output = $tcs->getSynonyms($tag1, $uId); + } + if(count($output)>0) { + return $output; + } + } + + // else compute the links + $output = array(); + + foreach($linkedTags as $linkedTag) { + $allLinkedTags = $this->getAllLinkedTags($linkedTag, $relationType, $uId, $stopList); + $output[] = $linkedTag; + if(is_array($allLinkedTags)) { + $output = array_merge($output, $allLinkedTags); + } else { + $output[] = $allLinkedTags; + } + } + + // and save in cache + if($activatedCache == true && $uId>0) { + $tcs->updateTag($tag1, $relationType, $output, $uId); + } + + //$output = array_unique($output); // remove duplication + return $output; + + } + } + + function getOrphewTags($relationType, $uId = 0, $limit = null, $orderBy = null) { + $query = "SELECT DISTINCT tts.tag1 as tag"; + $query.= " FROM `". $this->getTableName() ."` tts"; + if($orderBy != null) { + $tsts =SemanticScuttle_Service_Factory::get('TagStat'); + $query.= ", ".$tsts->getTableName() ." tsts"; + } + $query.= " WHERE tts.tag1 <> ALL"; + $query.= " (SELECT DISTINCT tag2 FROM `". $this->getTableName() ."`"; + $query.= " WHERE relationType = '" . $this->db->sql_escape($relationType) . "'"; + if($uId > 0) { + $query.= " AND uId = '".intval($uId)."'"; + } + $query.= ")"; + if($uId > 0) { + $query.= " AND tts.uId = '".intval($uId)."'"; + } + + switch($orderBy) { + case "nb": + $query.= " AND tts.tag1 = tsts.tag1"; + $query.= " AND tsts.relationType = '" . $this->db->sql_escape($relationType) . "'"; + if($uId > 0) { + $query.= " AND tsts.uId = " . intval($uId); + } + $query.= " ORDER BY tsts.nb DESC"; + break; + case "depth": // by nb of descendants + $query.= " AND tts.tag1 = tsts.tag1"; + $query.= " AND tsts.relationType = '" . $this->db->sql_escape($relationType) . "'"; + if($uId > 0) { + $query.= " AND tsts.uId = " . intval($uId); + } + $query.= " ORDER BY tsts.depth DESC"; + break; + case "nbupdate": + $query.= " AND tts.tag1 = tsts.tag1"; + $query.= " AND tsts.relationType = '" . $this->db->sql_escape($relationType) . "'"; + if($uId > 0) { + $query.= " AND tsts.uId = " . intval($uId); + } + $query.= " ORDER BY tsts.nbupdate DESC"; + break; + } + + if($limit != null) { + $query.= " LIMIT 0," . intval($limit); + } + + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not get linked tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + + function getMenuTags($uId) { + if(strlen($GLOBALS['menuTag']) < 1) { + return array(); + } else { + // we don't use the getAllLinkedTags function in order to improve performance + $query = "SELECT tag2 as 'tag', COUNT(tag2) as 'count'"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" . $this->db->sql_escape($GLOBALS['menuTag']) . "'"; + $query.= " AND relationType = '>'"; + if($uId > 0) { + $query.= " AND uId = " . intval($uId); + } + $query.= " GROUP BY tag2"; + $query.= " ORDER BY count DESC"; + $query.= " LIMIT 0, " . intval($GLOBALS['maxSizeMenuBlock']); + + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not get linked tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $output = $this->db->sql_fetchrowset($dbresult); + $this->db->sql_freeresult($dbresult); + return $output; + } + } + + + function existsLinkedTags($tag1, $tag2, $relationType, $uId) { + + //$tag1 = mysql_real_escape_string($tag1); + //$tag2 = mysql_real_escape_string($tag2); + + $query = "SELECT tag1, tag2, relationType, uId FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" . $this->db->sql_escape($tag1) . "'"; + $query.= " AND tag2 = '" . $this->db->sql_escape($tag2) . "'"; + $query.= " AND relationType = '" . $this->db->sql_escape($relationType) . "'"; + $query.= " AND uId = " . intval($uId); + + //echo($query."<br>\n"); + + $dbres = $this->db->sql_query($query); + $hasTags = $this->db->sql_numrows($dbres) > 0; + $this->db->sql_freeresult($dbres); + return $hasTags; + } + + function getLinks($uId) { + $query = "SELECT tag1, tag2, relationType, uId FROM `". $this->getTableName() ."`"; + $query.= " WHERE 1=1"; + if($uId > 0) { + $query.= " AND uId = " . intval($uId); + } + + $dbres = $this->db->sql_query($query); + $rowset = $this->db->sql_fetchrowset($dbres); + $this->db->sql_freeresult($dbres); + return $rowset; + } + + function removeLinkedTags($tag1, $tag2, $relationType, $uId) { + if(($tag1 != '' && $tag1 == $tag2) || + ($relationType != ">" && $relationType != "=" && $relationType != "") || + ($tag1 == '' && $tag2 == '')) { + return false; + } + $query = 'DELETE FROM '. $this->getTableName(); + $query.= ' WHERE 1=1'; + $query.= strlen($tag1)>0 ? ' AND tag1 = \''. $this->db->sql_escape($tag1) . "'" : ''; + $query.= strlen($tag2)>0 ? ' AND tag2 = \''. $this->db->sql_escape($tag2) . "'" : ''; + $query.= strlen($relationType)>0 ? ' AND relationType = \''. $this->db->sql_escape($relationType) . "'" : ''; + $query.= strlen($uId)>0 ? ' AND uId = '. intval($uId) : ''; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not remove tag relation', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + + // Update stats and cache + $this->update($tag1, $tag2, $relationType, $uId); + + $this->db->sql_freeresult($dbresult); + return true; + } + + function removeLinkedTagsForUser($uId) { + $query = 'DELETE FROM '. $this->getTableName(); + $query.= ' WHERE uId = '. intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not remove tag relation', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + + // Update stats and cache + $this->update('', '', '', $uId); + + $this->db->sql_freeresult($dbresult); + return true; + } + + function renameTag($uId, $oldName, $newName) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $newName = $tagservice->normalize($newName); + + $query = 'UPDATE `'. $this->getTableName() .'`'; + $query.= ' SET tag1=\'' . $this->db->sql_escape($newName) ."'"; + $query.= ' WHERE tag1=\'' . $this->db->sql_escape($oldName) . "'"; + $query.= ' AND uId=' . intval($uId); + $this->db->sql_query($query); + + $query = 'UPDATE `'. $this->getTableName() .'`'; + $query.= ' SET tag2=\'' . $this->db->sql_escape($newName) . "'"; + $query.= ' WHERE tag2=\'' . $this->db->sql_escape($oldName) . "'"; + $query.= ' AND uId=' . intval($uId); + $this->db->sql_query($query); + + + // Update stats and cache + $this->update($oldName, NULL, '=', $uId); + $this->update($oldName, NULL, '>', $uId); + $this->update($newName, NULL, '=', $uId); + $this->update($newName, NULL, '>', $uId); + + return true; + + } + + function update($tag1, $tag2, $relationType, $uId) { + $tsts =SemanticScuttle_Service_Factory::get('TagStat'); + $tsts->updateStat($tag1, $relationType, $uId); + + $tcs = SemanticScuttle_Service_Factory::get('TagCache'); + $tcs->deleteByUser($uId); + } + + function deleteAll() { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + + $tsts =SemanticScuttle_Service_Factory::get('TagStat'); + $tsts->deleteAll(); + } + +} +?> diff --git a/src/SemanticScuttle/Service/TagCache.php b/src/SemanticScuttle/Service/TagCache.php new file mode 100644 index 0000000..f8a28af --- /dev/null +++ b/src/SemanticScuttle/Service/TagCache.php @@ -0,0 +1,397 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle tag caching service. + * + * This class infers on relation between tags by storing all + * the including tags or synonymous tag. + * For example, if the user creates: tag1>tag2>tag3, the system + * can infer that tag is included into tag1. + * Instead of computing this relation several times, it is saved + * into this current table. + * For synonymy, this table stores also the group of synonymous tags. + * The table must be updated for each modification of + * the relations between tags. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_TagCache extends SemanticScuttle_DbService +{ + + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + protected function __construct($db) + { + $this->db =$db; + $this->tablename = $GLOBALS['tableprefix'] .'tagscache'; + } + + function getChildren($tag1, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + + if($tag1 == '') return false; + + $query = "SELECT DISTINCT tag2 as 'tag'"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= " WHERE relationType = '>'"; + $query.= " AND tag1 = '" . $this->db->sql_escape($tag1) . "'"; + $query.= " AND uId = " . intval($uId); + + //die($query); + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not get related tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + + $rowset = $this->db->sql_fetchrowset($dbresult); + $output = array(); + foreach($rowset as $row) { + $output[] = $row['tag']; + } + + $this->db->sql_freeresult($dbresult); + return $output; + } + + function addChild($tag1, $tag2, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + $tag2 = $tagservice->normalize($tag2); + + if($tag1 == $tag2 || strlen($tag1) == 0 || strlen($tag2) == 0 + || ($this->existsChild($tag1, $tag2, $uId))) { + return false; + } + + $values = array('tag1' => $tag1, 'tag2' => $tag2, 'relationType'=> '>', 'uId'=> $uId); + $query = 'INSERT INTO '. $this->getTableName() .' '. $this->db->sql_build_array('INSERT', $values); + //die($query); + if (!($dbresult = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add tag cache inference', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $this->db->sql_transaction('commit'); + } + + function removeChild($tag1, $tag2, $uId) { + if(($tag1 != '' && $tag1 == $tag2) || + ($tag1 == '' && $tag2 == '' && $uId == '')) { + return false; + } + + $query = 'DELETE FROM '. $this->getTableName(); + $query.= ' WHERE 1=1'; + $query.= strlen($tag1)>0 ? ' AND tag1 = \''. $this->db->sql_escape($tag1) . "'" : ''; + $query.= strlen($tag2)>0 ? ' AND tag2 = \''. $this->db->sql_escape($tag2) . "'" : ''; + $query.= ' AND relationType = ">"'; + $query.= strlen($uId)>0 ? ' AND uId = ' . intval($uId) : ''; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not remove tag cache inference', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + } + + function removeChildren($tag1, $uId) { + $this->removeChild($tag1, NULL, $uId); + } + + function existsChild($tag1, $tag2, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + $tag2 = $tagservice->normalize($tag2); + + $query = "SELECT tag1, tag2, relationType, uId FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" . $this->db->sql_escape($tag1) . "'"; + $query.= " AND tag2 = '" . $this->db->sql_escape($tag2) . "'"; + $query.= " AND relationType = '>'"; + $query.= " AND uId = " . intval($uId); + + //echo($query."<br>\n"); + + $dbres = $this->db->sql_query($query); + $rows = $this->db->sql_numrows($dbres); + $this->db->sql_freeresult($dbres); + return $rows > 0; + } + + /* + * Synonyms of a same concept are a group. A group has one main synonym called key + * and a list of synonyms called values. + */ + function addSynonym($tag1, $tag2, $uId) { + + if($tag1 == $tag2 || strlen($tag1) == 0 || strlen($tag2) == 0 + || ($this->existsSynonym($tag1, $tag2, $uId))) { + return false; + } + + $case1 = '0'; // not in DB + if($this->_isSynonymKey($tag1, $uId)) { + $case1 = 'key'; + } elseif($this->_isSynonymValue($tag1, $uId)) { + $case1 = 'value'; + } + + $case2 = '0'; // not in DB + if($this->_isSynonymKey($tag2, $uId)) { + $case2 = 'key'; + } elseif($this->_isSynonymValue($tag2, $uId)) { + $case2 = 'value'; + } + $case = $case1.$case2; + + // all the possible cases + switch ($case) { + case 'keykey': + $values = $this->_getSynonymValues($tag2, $uId); + $this->removeSynonymGroup($tag2, $uId); + foreach($values as $value) { + $this->addSynonym($tag1, $value, $uId); + } + $this->addSynonym($tag1, $tag2, $uId); + break; + + case 'valuekey': + $key = $this->_getSynonymKey($tag1, $uId); + $this->addSynonym($key, $tag2, $uId); + break; + + case 'keyvalue': + $this->addSynonym($tag2, $tag1, $uId); + break; + case 'valuevalue': + $key1 = $this->_getSynonymKey($tag1, $uId); + $key2 = $this->_getSynonymKey($tag2, $uId); + $this->addSynonym($key1, $key2, $uId); + break; + case '0value': + $key = $this->_getSynonymKey($tag2, $uId); + $this->addSynonym($key, $tag1, $uId); + break; + case 'value0': + $this->addSynonym($tag2, $tag1, $uId); + break; + case '0key': + $this->addSynonym($tag2, $tag1, $uId); + break; + default: + $values = array('tag1' => $tag1, 'tag2' => $tag2, 'relationType'=> '=', 'uId'=> $uId); + $query = 'INSERT INTO '. $this->getTableName() .' '. $this->db->sql_build_array('INSERT', $values); + //die($query); + if (!($dbresult = $this->db->sql_query($query))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add tag cache synonymy', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + $this->db->sql_transaction('commit'); + break; + } + } + + function removeSynonymGroup($tag1, $uId) { + $query = 'DELETE FROM '. $this->getTableName(); + $query.= ' WHERE 1=1'; + $query.= ' AND tag1 = \''. $this->db->sql_escape($tag1) . "'"; + $query.= ' AND relationType = "="'; + $query.= ' AND uId = ' . intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not remove tag cache inference', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + } + + function _isSynonymKey($tag1, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + + $query = "SELECT tag1 FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" . $this->db->sql_escape($tag1) ."'"; + $query.= " AND relationType = '='"; + $query.= " AND uId = " . intval($uId); + + $dbres = $this->db->sql_query($query); + $rows = $this->db->sql_numrows($dbres); + $this->db->sql_freeresult($dbres); + return $rows > 0; + } + + function _isSynonymValue($tag2, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag2 = $tagservice->normalize($tag2); + + $query = "SELECT tag2 FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag2 = '" . $this->db->sql_escape($tag2) . "'"; + $query.= " AND relationType = '='"; + $query.= " AND uId = " . intval($uId); + + $dbres = $this->db->sql_query($query); + $rows = $this->db->sql_numrows($dbres); + $this->db->sql_freeresult($dbres); + return $rows > 0; + } + + function getSynonyms($tag1, $uId) { + $values = array(); + if($this->_isSynonymKey($tag1, $uId)) { + $values = $this->_getSynonymValues($tag1, $uId); + } elseif($this->_isSynonymValue($tag1, $uId)) { + $key = $this->_getSynonymKey($tag1, $uId); + $values = $this->_getSynonymValues($key, $uId, $tag1); + $values[] = $key; + } + return $values; + } + + function _getSynonymKey($tag2, $uId) { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag2 = $tagservice->normalize($tag2); + + if($this->_isSynonymKey($tag2, $uId)) return $tag2; + + if($tag2 == '') return false; + + $query = "SELECT DISTINCT tag1 as 'tag'"; + $query.= " FROM `". $this->getTableName() ."`"; + $query.= " WHERE relationType = '='"; + $query.= " AND tag2 = '" . $this->db->sql_escape($tag2) . "'"; + $query.= " AND uId = " . intval($uId); + + //die($query); + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not get related tags', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + return $row['tag']; + } + + /* + * Return values associated with a key. + * $tagExcepted allows to hide a value. + * + * @return array Array with tag names + */ + protected function _getSynonymValues($tag1, $uId, $tagExcepted = NULL) + { + $tagservice =SemanticScuttle_Service_Factory::get('Tag'); + $tag1 = $tagservice->normalize($tag1); + $tagExcepted = $tagservice->normalize($tagExcepted); + + if($tag1 == '') return false; + + $query = "SELECT DISTINCT tag2 as 'tag'"; + $query .= " FROM `". $this->getTableName() ."`"; + $query .= " WHERE relationType = '='"; + $query .= " AND tag1 = '" . $this->db->sql_escape($tag1) . "'"; + $query .= " AND uId = " . intval($uId); + $query .= $tagExcepted != '' + ? " AND tag2!='" . $this->db->sql_escape($tagExcepted) . "'" + : ''; + + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die( + GENERAL_ERROR, 'Could not get related tags', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $rowset = $this->db->sql_fetchrowset($dbresult); + + $output = array(); + foreach($rowset as $row) { + $output[] = $row['tag']; + } + + $this->db->sql_freeresult($dbresult); + return $output; + } + + function existsSynonym($tag1, $tag2, $uId) { + if($this->_getSynonymKey($tag1, $uId) == $tag2 || $this->_getSynonymKey($tag2, $uId) == $tag1) { + return true; + } else { + return false; + } + } + + + function updateTag($tag1, $relationType, $otherTags, $uId) { + if($relationType == '=') { + if($this->getSynonyms($tag1, $uId)) { // remove previous data avoiding unconstistency + $this->removeSynonymGroup($tag1, $uId); + } + + foreach($otherTags as $tag2) { + $this->addSynonym($tag1, $tag2, $uId); + } + } elseif($relationType == '>') { + if(count($this->getChildren($tag1, $uId))>0) { // remove previous data avoiding unconstistency + $this->removeChildren($tag1); + } + + foreach($otherTags as $tag2) { + $this->addChild($tag1, $tag2, $uId); + } + } + } + + function deleteByUser($uId) { + $query = 'DELETE FROM '. $this->getTableName() .' WHERE uId = '. intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete user tags cache', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + + } + + function deleteAll() { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} +?> diff --git a/src/SemanticScuttle/Service/TagStat.php b/src/SemanticScuttle/Service/TagStat.php new file mode 100644 index 0000000..af8d110 --- /dev/null +++ b/src/SemanticScuttle/Service/TagStat.php @@ -0,0 +1,233 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle tag statistics service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_TagStat extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + protected function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'tagsstats'; + } + + function getNbChildren($tag1, $relationType, $uId) { + $tts =SemanticScuttle_Service_Factory::get('Tag2Tag'); + $query = "SELECT tag1, relationType, uId FROM `". $tts->getTableName() ."`"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + $dbres = $this->db->sql_query($query); + $rows = $this->db->sql_numrows($dbres); + $this->db->sql_freeresult($dbres); + return $rows; + } + + function getNbDescendants($tag1, $relationType, $uId) { + $query = "SELECT nb FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + $dbresults = $this->db->sql_query($query); + $row = $this->db->sql_fetchrow($dbresults); + $this->db->sql_freeresult($dbresults); + if($row['nb'] == null) { + return 0; + } else { + return (int) $row['nb']; + } + } + + function getMaxDepth($tag1, $relationType, $uId) { + $query = "SELECT depth FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + $dbresults = $this->db->sql_query($query); + $row = $this->db->sql_fetchrow($dbresults); + $this->db->sql_freeresult($dbresults); + if($row['depth'] == null) { + return 0; + } else { + return (int) $row['depth']; + }; + } + + function getNbUpdates($tag1, $relationType, $uId) { + $query = "SELECT nbupdate FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + $dbresults = $this->db->sql_query($query); + $row = $this->db->sql_fetchrow($dbresults); + $this->db->sql_freeresult($dbresults); + if($row['nbupdate'] == null) { + return 0; + } else { + return (int) $row['nbupdate']; + } + } + + function existStat($tag1, $relationType, $uId) { + $query = "SELECT tag1, relationType, uId FROM `". $this->getTableName() ."`"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + $dbres = $this->db->sql_query($query); + $rows = $this->db->sql_numrows($dbres); + $this->db->sql_freeresult($dbres); + return $rows > 0; + } + + function createStat($tag1, $relationType, $uId) { + $query = "INSERT INTO `". $this->getTableName() ."`"; + $query.= "(tag1, relationType, uId)"; + $query.= " VALUES ('".$tag1."','".$relationType."','".$uId."')"; + $this->db->sql_query($query); + } + + function updateStat($tag1, $relationType, $uId=null, $stoplist=array()) { + if(in_array($tag1, $stoplist)) { + return false; + } + + $tts =SemanticScuttle_Service_Factory::get('Tag2Tag'); + $linkedTags = $tts->getLinkedTags($tag1, $relationType, $uId); + $nbDescendants = 0; + $maxDepth = 0; + foreach($linkedTags as $linkedTag) { + $nbDescendants+= 1 + $this->getNbDescendants($linkedTag, $relationType, $uId); + $maxDepth = max($maxDepth, 1 + $this->getMaxDepth($linkedTag, $relationType, $uId)); + } + $this->setNbDescendants($tag1, $relationType, $uId, $nbDescendants); + $this->setMaxDepth($tag1, $relationType, $uId, $maxDepth); + $this->increaseNbUpdate($tag1, $relationType, $uId); + + // propagation to the precedent tags + $linkedTags = $tts->getLinkedTags($tag1, $relationType, $uId, true); + $stoplist[] = $tag1; + foreach($linkedTags as $linkedTag) { + $this->updateStat($linkedTag, $relationType, $uId, $stoplist); + } + } + + function updateAllStat() { + $tts =SemanticScuttle_Service_Factory::get('Tag2Tag'); + + $query = "SELECT tag1, uId FROM `". $tts->getTableName() ."`"; + $query.= " WHERE relationType = '>'"; + + //die($query); + + if (! ($dbresult = $this->db->sql_query($query)) ){ + message_die(GENERAL_ERROR, 'Could not update stats', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $rowset = $this->db->sql_fetchrowset($dbresult); + foreach($rowset as $row) { + $this->updateStat($row['tag1'], '>', $row['uId']); + } + $this->db->sql_freeresult($dbresult); + } + + function setNbDescendants($tag1, $relationType, $uId, $nb) { + if(!$this->existStat($tag1, $relationType, $uId)) { + $this->createStat($tag1, $relationType, $uId); + } + $query = "UPDATE `". $this->getTableName() ."`"; + $query.= " SET nb = ". $nb; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + $this->db->sql_freeresult($this->db->sql_query($query)); + } + + function setMaxDepth($tag1, $relationType, $uId, $depth) { + if(!$this->existStat($tag1, $relationType, $uId)) { + $this->createStat($tag1, $relationType, $uId); + } + $query = "UPDATE `". $this->getTableName() ."`"; + $query.= " SET depth = ". $depth; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + $this->db->sql_query($query); + } + + function increaseNbUpdate($tag1, $relationType, $uId) { + if(!$this->existStat($tag1, $relationType, $uId)) { + $this->createStat($tag1, $relationType, $uId); + } + $query = "UPDATE `". $this->getTableName() ."`"; + $query.= " SET nbupdate = nbupdate + 1"; + $query.= " WHERE tag1 = '" .$tag1 ."'"; + $query.= " AND relationType = '". $relationType ."'"; + $query.= " AND uId = '".$uId."'"; + + //die($query); + + $this->db->sql_query($query); + } + + function deleteTagStatForUser($uId) { + $query = 'DELETE FROM '. $this->getTableName() .' WHERE uId = '. intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete tag stats', '', __LINE__, + __FILE__, $query, $this->db); + return false; + } + + return true; + } + + function deleteAll() { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + +} +?> diff --git a/src/SemanticScuttle/Service/Template.php b/src/SemanticScuttle/Service/Template.php new file mode 100644 index 0000000..b5d4cfa --- /dev/null +++ b/src/SemanticScuttle/Service/Template.php @@ -0,0 +1,119 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once 'SemanticScuttle/Model/Template.php'; +require_once 'SemanticScuttle/Model/Theme.php'; + +/** + * SemanticScuttle template service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Template extends SemanticScuttle_Service +{ + /** + * Full path to template directory. + * + * Set in constructor to + * $GLOBALS['TEMPLATES_DIR'] + * + * @var string + */ + protected $basedir; + + /** + * The template theme to use. + * Set in constructor based on $GLOBALS['theme'] + * + * @var SemanticScuttle_Model_Theme + */ + protected $theme; + + + + /** + * Returns the single service instance + * + * @param DB $db Database object + * + * @return SemanticScuttle_Service + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + + /** + * Create a new instance + */ + protected function __construct() + { + $this->basedir = $GLOBALS['TEMPLATES_DIR']; + $this->theme = new SemanticScuttle_Model_Theme($GLOBALS['theme']); + //FIXME: verify the theme exists + } + + + + /** + * Loads and displays a template file. + * + * @param string $template Template filename relative + * to template dir + * @param array $vars Array of template variables. + * The current theme object will be added + * automatically with name "theme". + * + * @return SemanticScuttle_Model_Template Template object + */ + public function loadTemplate($template, $vars = null) + { + if (substr($template, -4) != '.php') { + $template .= '.php'; + } + + $oldIncPath = get_include_path(); + set_include_path( + $this->basedir . $this->theme->getName() + . PATH_SEPARATOR . $this->basedir . 'default' + //needed since services are instantiated in templates + . PATH_SEPARATOR . $oldIncPath + ); + + $vars['theme'] = $this->theme; + $tpl = new SemanticScuttle_Model_Template( + $template, $vars, $this + ); + $tpl->parse(); + + set_include_path($oldIncPath); + + return $tpl; + } +} + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service/Thumbnails.php b/src/SemanticScuttle/Service/Thumbnails.php new file mode 100644 index 0000000..6151254 --- /dev/null +++ b/src/SemanticScuttle/Service/Thumbnails.php @@ -0,0 +1,59 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Instantiates the configured website thumbnailer object. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Thumbnails extends SemanticScuttle_Service +{ + /** + * Instantiates the configured website thumbnailer object. + * + * @return object Website thumbnailer + */ + public function getThumbnailer() + { + if (!isset($GLOBALS['thumbnailsType']) + || $GLOBALS['thumbnailsType'] == '' + ) { + $class = 'SemanticScuttle_Thumbnailer_Null'; + } else { + $class = 'SemanticScuttle_Thumbnailer_' + . ucfirst($GLOBALS['thumbnailsType']); + } + if (!class_exists($class)) { + //PEAR classname to filename rule + $file = str_replace('_', '/', $class) . '.php'; + include_once $file; + } + + $thumbnailer = new $class(); + + if (!isset($GLOBALS['thumbnailsConfig']) + || $GLOBALS['thumbnailsConfig'] == '' + ) { + $thumbnailer->setConfig(null); + } else { + $thumbnailer->setConfig($GLOBALS['thumbnailsConfig']); + } + + return $thumbnailer; + } +} +?> diff --git a/src/SemanticScuttle/Service/User.php b/src/SemanticScuttle/Service/User.php new file mode 100644 index 0000000..434e6b7 --- /dev/null +++ b/src/SemanticScuttle/Service/User.php @@ -0,0 +1,1090 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once 'SemanticScuttle/Model/User.php'; + +/** + * SemanticScuttle user management service. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_User extends SemanticScuttle_DbService +{ + /** + * The ID of the currently logged on user. + * NULL when not logged in. + * + * @var integer + */ + protected $currentuserId = null; + + /** + * Currently logged on user from database + * + * @var array + * + * @see getCurrentUserId() + * @see getCurrentUser() + * @see setCurrentUserId() + */ + protected $currentuser = null; + + protected $fields = array( + 'primary' => 'uId', + 'username' => 'username', + 'password' => 'password', + 'privateKey' => 'privateKey' + ); + + protected $profileurl; + protected $sessionkey; + protected $cookiekey; + protected $cookietime = 1209600; // 2 weeks + + /** + * Returns the single service instance + * + * @param sql_db $db Database object + * + * @return SemanticScuttle_Service_User + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + protected function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'users'; + $this->sessionkey = INSTALLATION_ID.'-currentuserid'; + $this->cookiekey = INSTALLATION_ID.'-login'; + $this->profileurl = createURL('profile', '%2$s'); + $this->updateSessionStability(); + } + + /** + * Fetches the desired user row from database, specified by column and value + * + * @param string $fieldname Name of database column to identify user + * @param string $value Value of $fieldname + * + * @return array Database row or boolean false + */ + protected function _getuser($fieldname, $value) + { + $query = 'SELECT * FROM '. $this->getTableName() + . ' WHERE ' . $fieldname . ' = "' . $this->db->sql_escape($value) . '"'; + + if (!($dbresult = $this->db->sql_query($query)) ) { + message_die( + GENERAL_ERROR, 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + if ($row) { + return $row; + } else { + return false; + } + } + + function & getUsers($nb=0) { + $query = 'SELECT * FROM '. $this->getTableName() .' ORDER BY `uId` DESC'; + if($nb>0) { + $query .= ' LIMIT 0, '.$nb; + } + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die(GENERAL_ERROR, 'Could not get user', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + while ($row = $this->db->sql_fetchrow($dbresult)) { + $users[] = $row; + } + $this->db->sql_freeresult($dbresult); + return $users; + } + + /** + * Returns an array of user objects. + * Array is in order of uids + * + * @param integer $nb Number of users to fetch. + * + * @return array Array of SemanticScuttle_Model_User objects + */ + public function getObjectUsers($nb = 0) + { + $query = 'SELECT * FROM ' . $this->getTableName() + . ' ORDER BY uId DESC'; + + if ($nb > 0) { + $query .= ' LIMIT 0, ' . intval($nb); + } + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die( + GENERAL_ERROR, 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $users = array(); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $users[] = new SemanticScuttle_Model_User( + $row[$this->getFieldName('primary')], + $row[$this->getFieldName('username')] + ); + } + $this->db->sql_freeresult($dbresult); + return $users; + } + + function _randompassword() { + $seed = (integer) md5(microtime()); + mt_srand($seed); + $password = mt_rand(1, 99999999); + $password = substr(md5($password), mt_rand(0, 19), mt_rand(6, 12)); + return $password; + } + + /** + * Updates a single field in the user's database row + * + * @param integer $uId ID of the user + * @param string $fieldname Name of table column to change + * @param string $value New value + * + * @return boolean True if all was well, false if not + */ + public function _updateuser($uId, $fieldname, $value) + { + $updates = array ($fieldname => $value); + $sql = 'UPDATE '. $this->getTableName() + . ' SET '. $this->db->sql_build_array('UPDATE', $updates) + . ' WHERE '. $this->getFieldName('primary') . '=' . intval($uId); + + // Execute the statement. + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not update user', '', + __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + $this->db->sql_transaction('commit'); + + // Everything worked out, so return true. + return true; + } + + function getProfileUrl($id, $username) { + return sprintf($this->profileurl, urlencode($id), urlencode($username)); + } + + function getUserByUsername($username) { + return $this->_getuser($this->getFieldName('username'), $username); + } + + /** + * Returns user row from database. + * + * @param string $privateKey Private Key + * + * @return array User array from database, false if no user was found + */ + public function getUserByPrivateKey($privateKey) + { + return $this->_getuser($this->getFieldName('privateKey'), $privateKey); + } + + function getObjectUserByUsername($username) { + $user = $this->_getuser($this->getFieldName('username'), $username); + if($user != false) { + return new SemanticScuttle_Model_User( + $user[$this->getFieldName('primary')], $username + ); + } else { + return NULL; + } + } + + /** + * Obtains the ID of the given user name. + * If a user ID is passed, it is returned. + * In case the user does not exist, NULL is returned. + * + * @param string|integer $user User name or user ID + * + * @return integer NULL if not found or the user ID + */ + public function getIdFromUser($user) + { + if (is_int($user)) { + return intval($user); + } else { + $objectUser = $this->getObjectUserByUsername($user); + if ($objectUser != null) { + return $objectUser->getId(); + } + } + return null; + } + + /** + * Returns user row from database. + * + * @param integer $id User ID + * + * @return array User array from database + */ + public function getUser($id) + { + return $this->_getuser($this->getFieldName('primary'), $id); + } + + /** + * Returns user object for given user id + * + * @param integer $id User ID + * + * @return SemanticScuttle_Model_User User object + */ + public function getObjectUser($id) + { + $user = $this->_getuser($this->getFieldName('primary'), $id); + return new SemanticScuttle_Model_User( + $id, $user[$this->getFieldName('username')] + ); + } + + function isLoggedOn() { + return ($this->getCurrentUserId() !== false); + } + + /** + * Tells you if the private key is enabled and valid + * + * @param string $privateKey Private Key + * + * @return boolean True if enabled and valid + */ + public function isPrivateKeyValid($privateKey) + { + // check length of private key + if (strlen($privateKey) == 32) { + return true; + } + return false; + } + + /** + * Returns the current user object + * + * @param boolean $refresh Reload the user from database + * based on current user id + * @param mixed $newval New user value (used internally + * as setter method) + * + * @return array User from database + */ + public function getCurrentUser($refresh = false, $newval = null) + { + if (!is_null($newval)) { + //internal use only: reset currentuser + $this->currentuser = $newval; + } else if ($refresh || !isset($this->currentuser)) { + if ($id = $this->getCurrentUserId()) { + $this->currentuser = $this->getUser($id); + } else { + $this->currentuser = null; + } + } + return $this->currentuser; + } + + /** + * Return current user as object + * + * @param boolean $refresh Reload the user from database + * based on current user id + * @param mixed $newval New user value (used internally + * as setter method) + * + * @return SemanticScuttle_Model_User User object + */ + function getCurrentObjectUser($refresh = false, $newval = null) + { + static $currentObjectUser; + if (!is_null($newval)) { + //internal use only: reset currentuser + $currentObjectUser = $newval; + } else if ($refresh || !isset($currentObjectUser)) { + if ($id = $this->getCurrentUserId()) { + $currentObjectUser = $this->getObjectUser($id); + } else { + $currentObjectUser = null; + } + } + return $currentObjectUser; + } + + function existsUserWithUsername($username) { + if($this->getUserByUsername($username) != '') { + return true; + } else { + return false; + } + } + + function existsUser($id) { + if($this->getUser($id) != '') { + return true; + } else { + return false; + } + } + + /** + * Checks if the given user is an administrator. + * Uses global admin_users property containing admin + * user names. + * + * Passing the user id makes this function load the user + * from database. For efficiency reasons, try to pass + * the user name or database row. + * + * @param integer|array|string $user User ID or user row from DB + * or user name + * + * @return boolean True if the user is admin + */ + function isAdmin($user) + { + if (is_numeric($user)) { + $user = $this->getUser($user); + $user = $user['username']; + } else if (is_array($user)) { + $user = $user['username']; + } + + if (isset($GLOBALS['admin_users']) + && in_array($user, $GLOBALS['admin_users']) + ) { + return true; + } else { + return false; + } + } + + /** + * Return current user id based on session or cookie + * + * @return mixed Integer user id or boolean false when user + * could not be found or is not logged on. + */ + public function getCurrentUserId() + { + if ($this->currentuserId !== null) { + return $this->currentuserId; + } + + if (isset($_SESSION[$this->getSessionKey()])) { + $this->currentuserId = (int)$_SESSION[$this->getSessionKey()]; + return $this->currentuserId; + + } + + if (isset($_COOKIE[$this->getCookieKey()])) { + $cook = explode(':', $_COOKIE[$this->getCookieKey()]); + //cookie looks like this: 'id:md5(username+password)' + $query = 'SELECT * FROM '. $this->getTableName() . + ' WHERE MD5(CONCAT('.$this->getFieldName('username') . + ', '.$this->getFieldName('password') . + ')) = \''.$this->db->sql_escape($cook[1]).'\' AND '. + $this->getFieldName('primary'). ' = '. $this->db->sql_escape($cook[0]); + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die( + GENERAL_ERROR, 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + if ($row = $this->db->sql_fetchrow($dbresult)) { + $this->setCurrentUserId( + (int)$row[$this->getFieldName('primary')], true + ); + $this->db->sql_freeresult($dbresult); + return $this->currentuserId; + } + } + + $ssls = SemanticScuttle_Service_Factory::get('User_SslClientCert'); + if ($ssls->hasValidCert()) { + $id = $ssls->getUserIdFromCert(); + if ($id !== false) { + $this->setCurrentUserId($id, true); + return $this->currentuserId; + } + } + return false; + } + + + + /** + * Set the current user ID (i.e. when logging on) + * + * @internal + * No ID verification is being done. + * + * @param integer $user User ID or null to unset the user + * @param boolean $storeInSession Store the user ID in the session + * + * @return void + */ + public function setCurrentUserId($user, $storeInSession = false) + { + if ($user === null) { + $this->currentuserId = null; + if ($storeInSession) { + unset($_SESSION[$this->getSessionKey()]); + } + } else { + $this->currentuserId = (int)$user; + if ($storeInSession) { + $_SESSION[$this->getSessionKey()] = $this->currentuserId; + } + } + //reload user object + $this->getCurrentUser(true); + $this->getCurrentObjectUser(true); + } + + + + /** + * Try to authenticate and login a user with + * username and password. + * + * @param string $username Name of user + * @param string $password Password + * @param boolean $remember If a long-time cookie shall be set + * + * @return boolean True if the user could be authenticated, + * false if not. + */ + public function login($username, $password, $remember = false) + { + $password = $this->sanitisePassword($password); + $query = 'SELECT '. $this->getFieldName('primary') .' FROM '. $this->getTableName() .' WHERE '. $this->getFieldName('username') .' = "'. $this->db->sql_escape($username) .'" AND '. $this->getFieldName('password') .' = "'. $this->db->sql_escape($password) .'"'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, + 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + + if ($row) { + $this->setCurrentUserId($row[$this->getFieldName('primary')], true); + if ($remember) { + $cookie = $this->currentuserId . ':' . md5($username.$password); + setcookie( + $this->cookiekey, $cookie, + time() + $this->cookietime, '/' + ); + } + return true; + } else { + return false; + } + } + + /** + * Try to authenticate via the privateKey + * + * @param string $privateKey Private Key + * + * @return boolean true if the user could be authenticated, + * false if not. + */ + public function loginPrivateKey($privateKey) + { + /* Check if private key valid and enabled */ + if (!$this->isPrivateKeyValid($privateKey)) { + return false; + } + + $query = 'SELECT '. $this->getFieldName('primary') .' FROM ' + . $this->getTableName() .' WHERE ' + . $this->getFieldName('privateKey') .' = "' + . $this->db->sql_escape($privateKey) .'"'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, + 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + + if ($row) { + $this->setCurrentUserId($row[$this->getFieldName('primary')], false); + return true; + } else { + return false; + } + } + + /** + * Logs the user off + * + * @return void + */ + public function logout() + { + @setcookie($this->getCookiekey(), '', time() - 1, '/'); + unset($_COOKIE[$this->getCookiekey()]); + session_unset(); + $this->currentuserId = null; + $this->currentuser = null; + } + + function getWatchlist($uId) { + // Gets the list of user IDs being watched by the given user. + $query = 'SELECT watched FROM '. $GLOBALS['tableprefix'] .'watched WHERE uId = '. intval($uId); + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die(GENERAL_ERROR, 'Could not get watchlist', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $arrWatch = array(); + if ($this->db->sql_numrows($dbresult) == 0) { + $this->db->sql_freeresult($dbresult); + return $arrWatch; + } + while ($row = $this->db->sql_fetchrow($dbresult)) { + $arrWatch[] = $row['watched']; + } + $this->db->sql_freeresult($dbresult); + return $arrWatch; + } + + + /** + * Gets the list of user names being watched by the given user. + * + * @param integer $uId User ID + * @param boolean $watchedby if false: get the list of users that $uId watches + * if true: get the list of users that watch $uId + * + * @return array Array of user names + */ + public function getWatchNames($uId, $watchedby = false) + { + if ($watchedby) { + $table1 = 'b'; + $table2 = 'a'; + } else { + $table1 = 'a'; + $table2 = 'b'; + } + $primary = $this->getFieldName('primary'); + $userfield = $this->getFieldName('username'); + $query = 'SELECT '. $table1 .'.'. $userfield + . ' FROM '. $GLOBALS['tableprefix'] . 'watched AS W,' + . ' ' . $this->getTableName() .' AS a,' + . ' ' . $this->getTableName() .' AS b' + . ' WHERE W.watched = a.' . $primary + . ' AND W.uId = b.' . $primary + . ' AND ' . $table2 . '.' . $primary . ' = '. intval($uId) + . ' ORDER BY '. $table1 . '.' . $userfield; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not get watchlist', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $arrWatch = array(); + if ($this->db->sql_numrows($dbresult) == 0) { + $this->db->sql_freeresult($dbresult); + return $arrWatch; + } + while ($row = $this->db->sql_fetchrow($dbresult)) { + $arrWatch[] = $row[$this->getFieldName('username')]; + } + $this->db->sql_freeresult($dbresult); + return $arrWatch; + } + + + function getWatchStatus($watcheduser, $currentuser) { + // Returns true if the current user is watching the given user, and false otherwise. + $query = 'SELECT watched FROM '. $GLOBALS['tableprefix'] .'watched AS W INNER JOIN '. $this->getTableName() .' AS U ON U.'. $this->getFieldName('primary') .' = W.watched WHERE U.'. $this->getFieldName('primary') .' = '. intval($watcheduser) .' AND W.uId = '. intval($currentuser); + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die(GENERAL_ERROR, 'Could not get watchstatus', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $retval = true; + if ($this->db->sql_numrows($dbresult) == 0) + $retval = false; + + $this->db->sql_freeresult($dbresult); + return $retval; + } + + function setWatchStatus($subjectUserID) { + if (!is_numeric($subjectUserID)) + return false; + + $currentUserID = $this->getCurrentUserId(); + $watched = $this->getWatchStatus($subjectUserID, $currentUserID); + + if ($watched) { + $sql = 'DELETE FROM '. $GLOBALS['tableprefix'] .'watched WHERE uId = '. intval($currentUserID) .' AND watched = '. intval($subjectUserID); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add user to watch list', '', __LINE__, __FILE__, $sql, $this->db); + return false; + } + } else { + $values = array( + 'uId' => intval($currentUserID), + 'watched' => intval($subjectUserID) + ); + $sql = 'INSERT INTO '. $GLOBALS['tableprefix'] .'watched '. $this->db->sql_build_array('INSERT', $values); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die(GENERAL_ERROR, 'Could not add user to watch list', '', __LINE__, __FILE__, $sql, $this->db); + return false; + } + } + + $this->db->sql_transaction('commit'); + return true; + } + + /** + * Create a new user in database. + * No checks are done in here - you ought to have checked + * everything before calling this method! + * + * @param string $username Username to use + * @param string $password Password to use + * @param string $email Email to use + * @param string $privateKey Key for RSS auth + * + * @return mixed Integer user ID if all is well, + * boolean false if an error occured + */ + public function addUser($username, $password, $email, $privateKey = null) + { + // Set up the SQL UPDATE statement. + $datetime = gmdate('Y-m-d H:i:s', time()); + $password = $this->sanitisePassword($password); + $values = array( + 'username' => $username, + 'password' => $password, + 'email' => $email, + 'uDatetime' => $datetime, + 'uModified' => $datetime, + 'privateKey' => $privateKey + ); + $sql = 'INSERT INTO '. $this->getTableName() + . ' '. $this->db->sql_build_array('INSERT', $values); + + // Execute the statement. + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not insert user', + '', __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + $uId = $this->db->sql_nextid($dbresult); + $this->db->sql_transaction('commit'); + + return $uId; + } + + /** + * Updates the given user + * + * @param integer $uId ID of user to change + * @param string $password Password to use + * @param string $name Realname to use + * @param string $email Email to use + * @param string $homepage User's homepage + * @param string $uContent User note + * @param string $privateKey RSS Private Key + * @param boolean $enablePrivateKey RSS Private Key Flag + * + * @return boolean True when all is well, false if not + */ + public function updateUser( + $uId, $password, $name, $email, $homepage, $uContent, + $privateKey = null, $enablePrivateKey = false + ) { + if (!is_numeric($uId)) { + return false; + } + + // prepend '-' to privateKey if disabled + if ($privateKey != null && strlen($privateKey) == 32 + && $enablePrivateKey == false + ) { + $privateKey = '-' . $privateKey; + } + + // remove '-' from privateKey if enabling + if ($privateKey != null && strlen($privateKey) == 33 + && $enablePrivateKey == true + ) { + $privateKey = substr($privateKey, 1, 32); + } + + // if new user is enabling Private Key, create new key + if ($privateKey == null && $enablePrivateKey == true) { + $privateKey = $this->getNewPrivateKey(); + } + + // Set up the SQL UPDATE statement. + $moddatetime = gmdate('Y-m-d H:i:s', time()); + if ($password == '') { + $updates = array( + 'uModified' => $moddatetime, + 'name' => $name, + 'email' => $email, + 'homepage' => $homepage, + 'uContent' => $uContent, + 'privateKey' => $privateKey + ); + } else { + $updates = array( + 'uModified' => $moddatetime, + 'password' => $this->sanitisePassword($password), + 'name' => $name, + 'email' => $email, + 'homepage' => $homepage, + 'uContent' => $uContent, + 'privateKey' => $privateKey + ); + } + $sql = 'UPDATE '. $this->getTableName() + . ' SET '. $this->db->sql_build_array('UPDATE', $updates) + . ' WHERE '. $this->getFieldName('primary') . '=' . intval($uId); + + // Execute the statement. + $this->db->sql_transaction('begin'); + if (!($dbresult = $this->db->sql_query($sql))) { + $this->db->sql_transaction('rollback'); + message_die( + GENERAL_ERROR, 'Could not update user', '', + __LINE__, __FILE__, $sql, $this->db + ); + return false; + } + $this->db->sql_transaction('commit'); + + // Everything worked out, so return true. + return true; + } + + + + function getAllUsers ( ) { + $query = 'SELECT * FROM '. $this->getTableName(); + + if (! ($dbresult = $this->db->sql_query($query)) ) { + message_die(GENERAL_ERROR, 'Could not get users', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + $rows = array(); + + while ( $row = $this->db->sql_fetchrow($dbresult) ) { + $rows[] = $row; + } + $this->db->sql_freeresult($dbresult); + return $rows; + } + + // Returns an array with admin uIds + function getAdminIds() { + $admins = array(); + foreach($GLOBALS['admin_users'] as $adminName) { + if($this->getIdFromUser($adminName) != NULL) + $admins[] = $this->getIdFromUser($adminName); + } + return $admins; + } + + function deleteUser($uId) { + $query = 'DELETE FROM '. $this->getTableName() .' WHERE uId = '. intval($uId); + + if (!($dbresult = $this->db->sql_query($query))) { + message_die(GENERAL_ERROR, 'Could not delete user', '', __LINE__, __FILE__, $query, $this->db); + return false; + } + + return true; + } + + /** + * Delete all users and their watch states. + * Mainly used in unit tests. + * + * @return void + */ + public function deleteAll() + { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + + $query = 'TRUNCATE TABLE `' . $GLOBALS['tableprefix'] . 'watched' . '`'; + $this->db->sql_query($query); + } + + /** + * Hashes the password for storage in/querying the database. + * + * @param string $password Password to hash + * + * @return string Hashed password + */ + public function sanitisePassword($password) + { + return sha1(trim($password)); + } + + /** + * Changes the password for the given user to a new, random one. + * + * @param integer $uId User ID + * + * @return string New password of false if something went wrong + */ + public function generatePassword($uId) + { + if (!is_numeric($uId)) { + return false; + } + + $password = $this->_randompassword(); + + $ok = $this->_updateuser( + $uId, $this->getFieldName('password'), + $this->sanitisePassword($password) + ); + + if ($ok) { + return $password; + } else { + return false; + } + } + + /** + * Generates a new private key and confirms it isn't being used. + * Private key is 32 characters long, consisting of lowercase and + * numeric characters. + * + * @return string the new key value + */ + public function getNewPrivateKey() + { + do { + $newKey = md5(uniqid('SemanticScuttle', true)); + } while ($this->privateKeyExists($newKey)); + + return $newKey; + } + + /** + * Checks if a private key already exists + * + * @param string $privateKey key that has been generated + * + * @return boolean true when the private key exists, + * False if not. + */ + public function privateKeyExists($privateKey) + { + if (!$privateKey) { + return false; + } + $crit = array('privateKey' => $privateKey); + + $sql = 'SELECT COUNT(*) as "0" FROM ' + . $GLOBALS['tableprefix'] . 'users' + . ' WHERE '. $this->db->sql_build_array('SELECT', $crit); + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get vars', '', + __LINE__, __FILE__, $sql, $this->db + ); + } + if ($this->db->sql_fetchfield(0, 0) > 0) { + $exists = true; + } else { + $exists = false; + } + $this->db->sql_freeresult($dbresult); + return $exists; + } + + function isReserved($username) { + if (in_array($username, $GLOBALS['reservedusers'])) { + return true; + } else { + return false; + } + } + + function isValidUsername($username) { + if (strlen($username) < 4) { + return false; + }elseif (strlen($username) > 24) { + // too long usernames are cut by database and may cause bugs when compared + return false; + } elseif (preg_match('/(\W)/', $username) > 0) { + // forbidden non-alphanumeric characters + return false; + } + return true; + } + + + + /** + * Checks if the given email address is valid + * + * @param string $email Email address + * + * @return boolean True if it is valid, false if not + */ + public function isValidEmail($email) + { + return filter_var($email, FILTER_VALIDATE_EMAIL) !== false; + } + + /** + * Sets a session variable. + * Updates it when it is already set. + * This is used to detect if cookies work. + * + * @return void + * + * @see isSessionStable() + */ + public function updateSessionStability() + { + //find out if we have cookies enabled + if (!isset($_SESSION['sessionStable'])) { + $_SESSION['sessionStable'] = 0; + } else { + $_SESSION['sessionStable'] = 1; + } + } + + /** + * Tells you if the session is fresh or old. + * If the session is fresh, it's the first page + * call with that session id. If the session is old, + * we know that cookies (or session persistance) works + * + * @return boolean True if the + * + * @see updateSessionStability() + */ + public function isSessionStable() + { + return $_SESSION['sessionStable'] == 1; + } + + /** + * Get database column name. + * + * @param string $field Field name like 'primary', 'username' + * and 'password' + * + * @return string Real field name + */ + public function getFieldName($field) + { + return $this->fields[$field]; + } + + /** + * Set field name + * + * @param string $field Field name like 'primary', 'username' + * and 'password' + * @param string $value Real database column name + * + * @return void + */ + public function setFieldName($field, $value) + { + $this->fields[$field] = $value; + } + + function getSessionKey() { return $this->sessionkey; } + function setSessionKey($value) { $this->sessionkey = $value; } + + function getCookieKey() { return $this->cookiekey; } + function setCookieKey($value) { $this->cookiekey = $value; } +} + +?> diff --git a/src/SemanticScuttle/Service/User/SslClientCert.php b/src/SemanticScuttle/Service/User/SslClientCert.php new file mode 100644 index 0000000..a6d43be --- /dev/null +++ b/src/SemanticScuttle/Service/User/SslClientCert.php @@ -0,0 +1,283 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle SSL client certificate management service + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_User_SslClientCert extends SemanticScuttle_DbService +{ + /** + * Creates a new instance, sets database variable and table name. + * + * @param sql_db $db Database object + */ + protected function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] .'users_sslclientcerts'; + } + + /** + * Returns the single service instance + * + * @param sql_db $db Database object + * + * @return SemanticScuttle_Service_User + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + /** + * Determines if the browser provided a valid SSL client certificate + * + * @return boolean True if the client cert is there and is valid + */ + public function hasValidCert() + { + if (!isset($_SERVER['SSL_CLIENT_M_SERIAL']) + || !isset($_SERVER['SSL_CLIENT_V_END']) + || !isset($_SERVER['SSL_CLIENT_VERIFY']) + || $_SERVER['SSL_CLIENT_VERIFY'] !== 'SUCCESS' + || !isset($_SERVER['SSL_CLIENT_I_DN']) + ) { + return false; + } + + if ($_SERVER['SSL_CLIENT_V_REMAIN'] <= 0) { + return false; + } + + return true; + } + + + + /** + * Registers the currently available SSL client certificate + * with the given user. As a result, the user will be able to login + * using the certifiate + * + * @param integer $uId User ID to attach the client cert to. + * + * @return boolean True if registration was well, false if not. + */ + public function registerCurrentCertificate($uId) + { + $serial = $_SERVER['SSL_CLIENT_M_SERIAL']; + $clientIssuerDn = $_SERVER['SSL_CLIENT_I_DN']; + + $query = 'INSERT INTO ' . $this->getTableName() + . ' '. $this->db->sql_build_array( + 'INSERT', array( + 'uId' => $uId, + 'sslSerial' => $serial, + 'sslClientIssuerDn' => $clientIssuerDn, + 'sslName' => $_SERVER['SSL_CLIENT_S_DN_CN'], + 'sslEmail' => $_SERVER['SSL_CLIENT_S_DN_Email'] + ) + ); + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not load user for client certificate', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + return true; + } + + + + /** + * Takes values from the currently available SSL client certificate + * and adds the available profile data to the user. + * + * @param integer $uId User ID to attach the client cert to. + * + * @return array Array of profile data that were registered. + * Database column name as key, new value as value + */ + public function updateProfileFromCurentCert($uId) + { + $arData = array(); + + if (isset($_SERVER['SSL_CLIENT_S_DN_CN']) + && trim($_SERVER['SSL_CLIENT_S_DN_CN']) != '' + ) { + $arData['name'] = trim($_SERVER['SSL_CLIENT_S_DN_CN']); + } + + if (count($arData)) { + $us = SemanticScuttle_Service_Factory::get('User'); + foreach ($arData as $column => $value) { + $us->_updateuser($uId, $column, $value); + } + } + return $arData; + } + + + + /** + * Tries to detect the user ID from the SSL client certificate passed + * to the web server. + * + * @return mixed Integer user ID if the certificate is valid and + * assigned to a user, boolean false otherwise + */ + public function getUserIdFromCert() + { + if (!$this->hasValidCert()) { + return false; + } + + $serial = $_SERVER['SSL_CLIENT_M_SERIAL']; + $clientIssuerDn = $_SERVER['SSL_CLIENT_I_DN']; + + $query = 'SELECT uId' + . ' FROM ' . $this->getTableName() + . ' WHERE sslSerial = \'' . $this->db->sql_escape($serial) . '\'' + . ' AND sslClientIssuerDn = \'' + . $this->db->sql_escape($clientIssuerDn) + . '\''; + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not load user for client certificate', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + + if (!$row) { + return false; + } + return (int)$row['uId']; + } + + + + /** + * Fetches the certificate with the given ID from database. + * + * @param integer $id Certificate ID in database + * + * @return SemanticScuttle_Model_User_SslClientCert Certificate object + * or null if not found + */ + public function getCert($id) + { + $query = 'SELECT * FROM ' . $this->getTableName() + . ' WHERE id = ' . (int)$id; + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not load SSL client certificate', + '', __LINE__, __FILE__, $query, $this->db + ); + return null; + } + + if ($row = $this->db->sql_fetchrow($dbresult)) { + $cert = SemanticScuttle_Model_User_SslClientCert::fromDb($row); + } else { + $cert = null; + } + $this->db->sql_freeresult($dbresult); + return $cert; + } + + + + /** + * Fetches all registered certificates for the user from the database + * and returns it. + * + * @return array Array with all certificates for the user. Empty if + * there are none, SemanticScuttle_Model_User_SslClientCert + * objects otherwise. + */ + public function getUserCerts($uId) + { + $query = 'SELECT * FROM ' . $this->getTableName() + . ' WHERE uId = ' . (int)$uId + . ' ORDER BY sslSerial DESC'; + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not load SSL client certificates', + '', __LINE__, __FILE__, $query, $this->db + ); + return array(); + } + + $certs = array(); + while ($row = $this->db->sql_fetchrow($dbresult)) { + $certs[] = SemanticScuttle_Model_User_SslClientCert::fromDb($row); + } + $this->db->sql_freeresult($dbresult); + return $certs; + } + + + + /** + * Deletes a SSL client certificate. + * No security checks are made here. + * + * @param mixed $cert Certificate object or certificate database id. + * Objects are of type + * SemanticScuttle_Model_User_SslClientCert + * + * @return boolean True if all went well, false if it could not be deleted + */ + public function delete($cert) + { + if ($cert instanceof SemanticScuttle_Model_User_SslClientCert) { + $id = (int)$cert->id; + } else { + $id = (int)$cert; + } + + if ($id === 0) { + return false; + } + + $query = 'DELETE FROM ' . $this->getTableName() + .' WHERE id = ' . $id; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not delete user certificate', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + return true; + } +} +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Service/Vote.php b/src/SemanticScuttle/Service/Vote.php new file mode 100644 index 0000000..85380e7 --- /dev/null +++ b/src/SemanticScuttle/Service/Vote.php @@ -0,0 +1,337 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * SemanticScuttle voting system. + * + * Each registered user in SemanticScuttle may vote + * for or against a bookmark, counting +1 or -1. + * The sum of all votes determines the "voting" of a bookmark. + * Every user is equal in voting. Each vote is tied to a user. + * + * @internal + * Votes are saved in a separate "votes" table. + * Additionally to that, the voting and number of votes + * of a bookmark is also stored in the bookmarks table. + * This is done to make sure lookups are really fast, since + * every bookmark in a list shows its voting. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Service_Vote extends SemanticScuttle_DbService +{ + /** + * Returns the single service instance + * + * @param sql_db $db Database object + * + * @return SemanticScuttle_Service_Vote + */ + public static function getInstance($db) + { + static $instance; + if (!isset($instance)) { + $instance = new self($db); + } + return $instance; + } + + + + /** + * Create a new instance. + * + * @param sql_db $db Database object + */ + protected function __construct($db) + { + $this->db = $db; + $this->tablename = $GLOBALS['tableprefix'] . 'votes'; + } + + + + /** + * Returns the sum of votes for the given bookmark. + * + * @internal + * Uses the "votes" table to retrieve the votes, which + * has high costs. It is more efficient to get the sum of + * all votes for a bookmark from the bookmarks table, + * field bVoting. + * + * @param integer $bookmark Bookmark ID + * + * @return integer Vote (can be positive, 0 or negative) + */ + public function getVoting($bookmark) + { + $query = 'SELECT SUM(vote) as sum FROM ' . $this->getTableName() + . ' WHERE bid = "' . $this->db->sql_escape($bookmark) . '"'; + + if (!($dbres = $this->db->sql_query_limit($query, 1, 0))) { + message_die( + GENERAL_ERROR, 'Could not get voting', + '', __LINE__, __FILE__, $query, $this->db + ); + //FIXME: throw exception + } + + $row = $this->db->sql_fetchrow($dbres); + $this->db->sql_freeresult($dbres); + + return (int)$row['sum']; + }//public function getVoting(..) + + + + /** + * Returns the number of users that voted for or + * against the given bookmark. + * + * @internal + * This method uses the votes table to calculate the + * number of votes for a bookmark. In normal life, it + * is more efficient to use the "bVotes" field in the + * bookmarks table. + * + * @param integer $bookmark Bookmark ID + * + * @return integer Number of votes + */ + public function getVotes($bookmark) + { + $query = 'SELECT COUNT(vote) as count FROM ' + . $this->getTableName() + . ' WHERE bid = "' . $this->db->sql_escape($bookmark) . '"'; + + if (!($dbres = $this->db->sql_query_limit($query, 1, 0))) { + message_die( + GENERAL_ERROR, 'Could not get vote count', + '', __LINE__, __FILE__, $query, $this->db + ); + //FIXME: throw exception + } + + $row = $this->db->sql_fetchrow($dbres); + $this->db->sql_freeresult($dbres); + + return (int)$row['count']; + } + + + + /** + * Returns if the user has already voted for + * the given bookmark. + * + * @param integer $bookmark Bookmark ID + * @param integer $user User ID + * + * @return boolean True if the user has already voted + */ + public function hasVoted($bookmark, $user) + { + $query = 'SELECT COUNT(vote) as count FROM ' + . $this->getTableName() + . ' WHERE' + . ' bid = "' . $this->db->sql_escape($bookmark) . '"' + . ' AND uid = "' . $this->db->sql_escape($user) . '"'; + + if (!($dbres = $this->db->sql_query_limit($query, 1, 0))) { + message_die( + GENERAL_ERROR, 'Could not get vote count', + '', __LINE__, __FILE__, $query, $this->db + ); + //FIXME: throw exception + } + + $row = $this->db->sql_fetchrow($dbres); + $this->db->sql_freeresult($dbres); + + return (int)$row['count'] == 1; + } + + + + /** + * Returns the actual vote the given user + * gave the bookmark. + * + * @param integer $bookmark Bookmark ID + * @param integer $user User ID + * + * @return integer Either 1 or -1, null when not voted. + */ + public function getVote($bookmark, $user) + { + $query = 'SELECT vote FROM ' . $this->getTableName() + . ' WHERE' + . ' bid = "' . $this->db->sql_escape($bookmark) . '"' + . ' AND uid = "' . $this->db->sql_escape($user) . '"'; + + if (!($dbres = $this->db->sql_query_limit($query, 1, 0))) { + message_die( + GENERAL_ERROR, 'Could not get vote count', + '', __LINE__, __FILE__, $query, $this->db + ); + //FIXME: throw exception + } + + $row = $this->db->sql_fetchrow($dbres); + $this->db->sql_freeresult($dbres); + + if (!$row) { + return null; + } + + return $row['vote']; + } + + + + /** + * Let a user vote for the bookmark. + * + * @internal + * We check if voting is enabled or not, + * and if the user has already voted. + * It is up to the calling code to make sure + * the user is authorized to vote. + * + * @param integer $bookmark Bookmark ID + * @param integer $user User ID + * @param integer $vote 1 or -1 + * + * @return boolean True if the vote was saved, + * false if there was a problem + * (i.e. already voted) + */ + public function vote($bookmark, $user, $vote = 1) + { + if ($GLOBALS['enableVoting'] == false) { + return false; + } + + if ($vote != -1 && $vote != 1) { + return false; + } + + if ($this->hasVoted($bookmark, $user)) { + $this->removeVote($bookmark, $user); + } + + $res = $this->db->sql_query( + 'INSERT INTO ' . $this->getTableName() + . ' SET' + . ' bid = ' . (int)$bookmark + . ',uid = ' . (int)$user + . ',vote = ' . (int)$vote + ); + //FIXME: check for sql error + $this->db->sql_freeresult($res); + + //update bookmark table + $bm = SemanticScuttle_Service_Factory::get('Bookmark'); + $res = $this->db->sql_query( + $sql='UPDATE ' . $bm->getTableName() + . ' SET bVoting = bVoting + ' . (int)$vote + . ' , bVotes = bVotes + 1' + . ' WHERE bId = ' . (int)$bookmark + ); + $this->db->sql_freeresult($res); + + return true; + } + + + + /** + * Removes a vote from the database + * + * @param integer $bookmark Bookmark ID + * @param integer $user User ID + * + * @return boolean True if all went well, false if not + */ + protected function removeVote($bookmark, $user) + { + $vote = $this->getVote($bookmark, $user); + if ($vote === null) { + return false; + } + + //remove from votes table + $query = 'DELETE FROM ' . $this->getTableName() + . ' WHERE bId = ' . (int)$bookmark + . ' AND uId = ' . (int)$user; + $this->db->sql_query($query); + + //change voting sum in bookmarks table + $bm = SemanticScuttle_Service_Factory::get('Bookmark'); + $res = $this->db->sql_query( + $sql='UPDATE ' . $bm->getTableName() + . ' SET bVoting = bVoting - ' . (int)$vote + . ' , bVotes = bVotes - 1' + . ' WHERE bId = ' . (int)$bookmark + ); + $this->db->sql_freeresult($res); + + return true; + } + + + + /** + * Re-calculates all votings for all bookmarks + * and updates the voting values in the bookmarks + * table. + * This is mainly meant to be an administrative method + * to fix a broken database. + * + * @return void + */ + public function rewriteVotings() + { + $bm = SemanticScuttle_Service_Factory::get('Bookmark'); + $query = 'UPDATE ' . $bm->getTableName() . ' as B SET bVoting = ' + . '(SELECT SUM(vote) FROM ' . $this->getTableName() . ' as V' + . ' WHERE V.bId = B.bId GROUP BY bid)' + . ', bVotes = ' + . '(SELECT COUNT(vote) FROM ' . $this->getTableName() . ' as V' + . ' WHERE V.bId = B.bId GROUP BY bid)'; + $this->db->sql_query($query); + } + + + + /** + * Delete all votes from the database table. + * Used in unit tests. + * + * @return void + */ + public function deleteAll() + { + $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; + $this->db->sql_query($query); + } + + +} + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/Thumbnailer/Null.php b/src/SemanticScuttle/Thumbnailer/Null.php new file mode 100644 index 0000000..ec12135 --- /dev/null +++ b/src/SemanticScuttle/Thumbnailer/Null.php @@ -0,0 +1,52 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Dummy thumbnailer that never returns a thumbnail URL + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Thumbnailer_Null +{ + /** + * Set dummy configuration + * + * @param array $config Dummy configuration + * + * @return void + */ + public function setConfig($config) + { + } + + /** + * Get the URL for a website thumbnail. + * Always returns false. + * + * @param string $bookmarkUrl URL of website to create thumbnail for + * @param integer $width Screenshot width + * @param integer $height Screenshot height + * + * @return mixed FALSE when no screenshot could be obtained, + * string with the URL otherwise + */ + public function getThumbnailUrl($bookmarkUrl, $width, $height) + { + return false; + } +} +?> diff --git a/src/SemanticScuttle/Thumbnailer/Phancap.php b/src/SemanticScuttle/Thumbnailer/Phancap.php new file mode 100644 index 0000000..1b76849 --- /dev/null +++ b/src/SemanticScuttle/Thumbnailer/Phancap.php @@ -0,0 +1,92 @@ +<?php +/** + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Show website thumbnails/screenshots using phancap + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + * @see http://cweiske.de/phancap.htm + */ +class SemanticScuttle_Thumbnailer_Phancap +{ + /** + * Configuration array. + * Required keys: + * - url + * - token + * - secret + */ + protected $config = array(); + + /** + * Set phancap configuration + * + * @param array $config Phancap configuration + * + * @return void + */ + public function setConfig($config) + { + $this->config = $config; + } + + /** + * Get the URL for a website thumbnail + * + * @param string $bookmarkUrl URL of website to create thumbnail for + * @param integer $width Screenshot width + * @param integer $height Screenshot height + * + * @return mixed FALSE when no screenshot could be obtained, + * string with the URL otherwise + */ + public function getThumbnailUrl($bookmarkUrl, $width, $height) + { + //default parameters for the phancap service + $parameters = array( + 'url' => $bookmarkUrl, + 'swidth' => $width, + 'sheight' => $height, + 'sformat' => 'jpg', + ); + + if (isset($this->config['token']) && $this->config['token'] != '') { + $parameters['atoken'] = $this->config['token']; + $parameters['atimestamp'] = time(); + + //create signature + ksort($parameters); + foreach ($parameters as $key => $value) { + $encparams[] = $key . '=' . rawurlencode($value); + } + $encstring = implode('&', $encparams); + $signature = hash_hmac('sha1', $encstring, $this->config['secret']); + //append signature to parameters + $parameters['asignature'] = $signature; + } + + //url-encode the parameters + $urlParams = array(); + foreach ($parameters as $key => $value) { + $urlParams[] = $key . '=' . urlencode($value); + } + + //final URL + return $this->config['url'] . '?' . implode('&', $urlParams); + } +} +?> diff --git a/src/SemanticScuttle/constants.php b/src/SemanticScuttle/constants.php new file mode 100644 index 0000000..306c32a --- /dev/null +++ b/src/SemanticScuttle/constants.php @@ -0,0 +1,74 @@ +<?php +/** + * Define constants used in all the application. + * Some constants are based on variables from configuration file. + * + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @subcategory Base + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +// Debug managament +if (isset($GLOBALS['debugMode'])) { + define('DEBUG_MODE', $GLOBALS['debugMode']); + // Constant used exclusively into db/ directory + define('DEBUG_EXTRA', $GLOBALS['debugMode']); +} + +// Determine the base URL as ROOT +if (!isset($GLOBALS['root'])) { + $pieces = explode('/', $_SERVER['SCRIPT_NAME']); + + $rootTmp = '/'; + foreach ($pieces as $piece) { + //we eliminate possible sscuttle subfolders (like gsearch for example) + if ($piece != '' && !strstr($piece, '.php') + && $piece != 'gsearch' && $piece != 'ajax' + ) { + $rootTmp .= $piece .'/'; + } + } + if (($rootTmp != '/') && (substr($rootTmp, -1, 1) != '/')) { + $rootTmp .= '/'; + } + + //we do not prepend http since we also want to support https connections + // "http" is not required; it's automatically determined by the browser + // depending on the current connection. + define('ROOT', '//'. $_SERVER['HTTP_HOST'] . $rootTmp); +} else { + define('ROOT', $GLOBALS['root']); +} +define('ROOT_JS', ROOT . 'js/jstree-1.0-rc2/'); + +// Error codes +define('GENERAL_MESSAGE', 200); +define('GENERAL_ERROR', 202); +define('CRITICAL_MESSAGE', 203); +define('CRITICAL_ERROR', 204); + +// Page name +define('PAGE_INDEX', "index"); +define('PAGE_BOOKMARKS', "bookmarks"); +define('PAGE_WATCHLIST', "watchlist"); + + +// Miscellanous + +// INSTALLATION_ID is based on directory DB and used as prefix +// (in session and cookie) to prevent mutual login for different +// installations on the same host server +define('INSTALLATION_ID', md5($GLOBALS['dbname'].$GLOBALS['tableprefix'])); + +//fix PATH_INFO on certain hosts +$_SERVER['PATH_INFO'] = SemanticScuttle_Environment::getServerPathInfo(); +?> diff --git a/src/SemanticScuttle/db/db2.php b/src/SemanticScuttle/db/db2.php new file mode 100644 index 0000000..b1abf1a --- /dev/null +++ b/src/SemanticScuttle/db/db2.php @@ -0,0 +1,417 @@ +<?php +/** +* +* @package dbal_db2 +* @version $Id: db2.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if(!defined("SQL_LAYER")) +{ + +define("SQL_LAYER","db2"); + +/** +* @package dbal_db2 +* DB2 Database Abstraction Layer +*/ +class sql_db +{ + + var $db_connect_id; + var $query_result; + var $query_resultset; + var $query_numrows; + var $next_id; + var $row = array(); + var $rowset = array(); + var $row_index; + var $num_queries = 0; + + // + // Constructor + // + function sql_db($sqlserver, $sqluser, $sqlpassword, $database, $persistency = true) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->password = $sqlpassword; + $this->dbname = $database; + + $this->server = $sqlserver; + + if($this->persistency) + { + $this->db_connect_id = odbc_pconnect($this->server, "", ""); + } + else + { + $this->db_connect_id = odbc_connect($this->server, "", ""); + } + + if($this->db_connect_id) + { + @odbc_autocommit($this->db_connect_id, off); + + return $this->db_connect_id; + } + else + { + return false; + } + } + // + // Other base methods + // + function sql_close() + { + if($this->db_connect_id) + { + if($this->query_result) + { + @odbc_free_result($this->query_result); + } + $result = @odbc_close($this->db_connect_id); + return $result; + } + else + { + return false; + } + } + + + // + // Query method + // + function sql_query($query = "", $transaction = FALSE) + { + // + // Remove any pre-existing queries + // + unset($this->query_result); + unset($this->row); + if($query != "") + { + $this->num_queries++; + + if(!eregi("^INSERT ",$query)) + { + if(eregi("LIMIT", $query)) + { + preg_match("/^(.*)LIMIT ([0-9]+)[, ]*([0-9]+)*/s", $query, $limits); + + $query = $limits[1]; + if($limits[3]) + { + $row_offset = $limits[2]; + $num_rows = $limits[3]; + } + else + { + $row_offset = 0; + $num_rows = $limits[2]; + } + + $query .= " FETCH FIRST ".($row_offset+$num_rows)." ROWS ONLY OPTIMIZE FOR ".($row_offset+$num_rows)." ROWS"; + + $this->query_result = odbc_exec($this->db_connect_id, $query); + + $query_limit_offset = $row_offset; + $this->result_numrows[$this->query_result] = $num_rows; + } + else + { + $this->query_result = odbc_exec($this->db_connect_id, $query); + + $row_offset = 0; + $this->result_numrows[$this->query_result] = 5E6; + } + + $result_id = $this->query_result; + if($this->query_result && eregi("^SELECT", $query)) + { + + for($i = 1; $i < odbc_num_fields($result_id)+1; $i++) + { + $this->result_field_names[$result_id][] = odbc_field_name($result_id, $i); + } + + $i = $row_offset + 1; + $k = 0; + while(odbc_fetch_row($result_id, $i) && $k < $this->result_numrows[$result_id]) + { + + for($j = 1; $j < count($this->result_field_names[$result_id])+1; $j++) + { + $this->result_rowset[$result_id][$k][$this->result_field_names[$result_id][$j-1]] = odbc_result($result_id, $j); + } + $i++; + $k++; + } + + $this->result_numrows[$result_id] = $k; + $this->row_index[$result_id] = 0; + } + else + { + $this->result_numrows[$result_id] = @odbc_num_rows($result_id); + $this->row_index[$result_id] = 0; + } + } + else + { + if(eregi("^(INSERT|UPDATE) ", $query)) + { + $query = preg_replace("/\\\'/s", "''", $query); + } + + $this->query_result = odbc_exec($this->db_connect_id, $query); + + if($this->query_result) + { + $sql_id = "VALUES(IDENTITY_VAL_LOCAL())"; + + $id_result = odbc_exec($this->db_connect_id, $sql_id); + if($id_result) + { + $row_result = odbc_fetch_row($id_result); + if($row_result) + { + $this->next_id[$this->query_result] = odbc_result($id_result, 1); + } + } + } + + odbc_commit($this->db_connect_id); + + $this->query_limit_offset[$this->query_result] = 0; + $this->result_numrows[$this->query_result] = 0; + } + + return $this->query_result; + } + else + { + return false; + } + } + + // + // Other query methods + // + function sql_numrows($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + return $this->result_numrows[$query_id]; + } + else + { + return false; + } + } + function sql_affectedrows($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + return $this->result_numrows[$query_id]; + } + else + { + return false; + } + } + function sql_numfields($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = count($this->result_field_names[$query_id]); + return $result; + } + else + { + return false; + } + } + function sql_fieldname($offset, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = $this->result_field_names[$query_id][$offset]; + return $result; + } + else + { + return false; + } + } + function sql_fieldtype($offset, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @odbc_field_type($query_id, $offset); + return $result; + } + else + { + return false; + } + } + function sql_fetchrow($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + if($this->row_index[$query_id] < $this->result_numrows[$query_id]) + { + $result = $this->result_rowset[$query_id][$this->row_index[$query_id]]; + $this->row_index[$query_id]++; + return $result; + } + else + { + return false; + } + } + else + { + return false; + } + } + function sql_fetchrowset($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $this->row_index[$query_id] = $this->result_numrows[$query_id]; + return $this->result_rowset[$query_id]; + } + else + { + return false; + } + } + function sql_fetchfield($field, $row = -1, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + if($row < $this->result_numrows[$query_id]) + { + if($row == -1) + { + $getrow = $this->row_index[$query_id]-1; + } + else + { + $getrow = $row; + } + + return $this->result_rowset[$query_id][$getrow][$this->result_field_names[$query_id][$field]]; + + } + else + { + return false; + } + } + else + { + return false; + } + } + function sql_rowseek($offset, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $this->row_index[$query_id] = 0; + return true; + } + else + { + return false; + } + } + function sql_nextid($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + return $this->next_id[$query_id]; + } + else + { + return false; + } + } + function sql_freeresult($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @odbc_free_result($query_id); + return $result; + } + else + { + return false; + } + } + function sql_error($query_id = 0) + { +// $result['code'] = @odbc_error($this->db_connect_id); +// $result['message'] = @odbc_errormsg($this->db_connect_id); + + return ""; + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/firebird.php b/src/SemanticScuttle/db/firebird.php new file mode 100644 index 0000000..58a7d07 --- /dev/null +++ b/src/SemanticScuttle/db/firebird.php @@ -0,0 +1,527 @@ +<?php +/** +* +* @package dbal_firebird +* @version $Id: firebird.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'firebird'); + +/** +* @package dbal_firebird +* Firebird/Interbase Database Abstraction Layer +* Minimum Requirement is Firebird 1.5+/Interbase 7.1+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + var $last_query_text = ''; + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @ibase_pconnect($this->server . ':' . $this->dbname, $this->user, $sqlpassword, false, false, 3) : @ibase_connect($this->server . ':' . $this->dbname, $this->user, $sqlpassword, false, false, 3); + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + @ibase_commit($this->db_connect_id); + } + + if (sizeof($this->open_queries)) + { + foreach ($this->open_queries as $i_query_id => $query_id) + { + @ibase_free_query($query_id); + } + } + + return @ibase_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $this->transaction = true; + break; + + case 'commit': + $result = @ibase_commit(); + $this->transaction = false; + + if (!$result) + { + @ibase_rollback(); + } + break; + + case 'rollback': + $result = @ibase_rollback(); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + $this->last_query_text = $query; + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = @ibase_query($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + // TODO: have to debug the commit states in firebird + if (!$this->transaction) + { + @ibase_commit_ret(); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $cache->sql_save($query, $this->query_result, $cache_ttl); + } + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + if ($query != '') + { + $this->query_result = false; + + $query = 'SELECT FIRST ' . $total . ((!empty($offset)) ? ' SKIP ' . $offset : '') . substr($query, 6); + + return $this->sql_query($query, $cache_ttl); + } + else + { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + return FALSE; + } + + function sql_affectedrows() + { + // TODO: hmm, maybe doing something similar as in mssql-odbc.php? + return ($this->query_result) ? true : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + $row = array(); + $cur_row = @ibase_fetch_object($query_id, IBASE_TEXT); + + if (!$cur_row) + { + return false; + } + + foreach (get_object_vars($cur_row) as $key => $value) + { + $row[strtolower($key)] = trim(str_replace("\\0", "\0", str_replace("\\n", "\n", $value))); + } + return ($query_id) ? $row : false; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + $result = array(); + while ($this->rowset[$query_id] = get_object_vars(@ibase_fetch_object($query_id, IBASE_TEXT))) + { + $result[] = $this->rowset[$query_id]; + } + + return $result; + } + else + { + return false; + } + } + + function sql_fetchfield($field, $rownum = -1, $query_id = 0) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { + // erm... ok, my bad, we always use zero. :/ + for ($i = 0; $i <= $rownum; $i++) + { + $row = $this->sql_fetchrow($query_id); + } + + return $row[$field]; + } + else + { + if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) + { + if ($this->sql_fetchrow($query_id)) + { + $result = $this->row[$query_id][$field]; + } + } + else + { + if ($this->rowset[$query_id]) + { + $result = $this->rowset[$query_id][$field]; + } + else if ($this->row[$query_id]) + { + $result = $this->row[$query_id][$field]; + } + } + } + return $result; + } + else + { + return false; + } + } + + function sql_rowseek($rownum, $query_id = 0) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + for($i = 1; $i < $rownum; $i++) + { + if (!$this->sql_fetchrow($query_id)) + { + return false; + } + } + + return true; + } + + function sql_nextid() + { + if ($this->query_result && preg_match('#^INSERT[\t\n ]+INTO[\t\n ]+([a-z0-9\_\-]+)#is', $this->last_query_text, $tablename)) + { + $query = "SELECT GEN_ID('" . $tablename[1] . "_gen', 0) AS new_id + FROM RDB\$DATABASE"; + if (!($temp_q_id = @ibase_query($this->db_connect_id, $query))) + { + return false; + } + + $temp_result = @ibase_fetch_object($temp_q_id); + $this->sql_freeresult($temp_q_id); + + return ($temp_result) ? $temp_result->last_value : false; + } + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (!$this->transaction && $query_id) + { + @ibase_commit(); + } + + return ($query_id) ? @ibase_free_result($query_id) : false; + } + + function sql_escape($msg) + { + return (@ini_get('magic_quotes_sybase') || strtolower(@ini_get('magic_quotes_sybase')) == 'on') ? str_replace('\\\'', '\'', addslashes($msg)) : str_replace('\'', '\'\'', stripslashes($msg)); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page =(!empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' .((!empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : $_ENV['QUERY_STRING']); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @ibase_errmsg() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . $this_page .(($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result['message'] = @ibase_errmsg(); + $result['code'] = ''; + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $this->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @ibase_query($this->db_connect_id, $query); + while ($void = @ibase_fetch_object($result, IBASE_TEXT)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + @ibase_freeresult($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - ' . SQL_LAYER . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/mssql-odbc.php b/src/SemanticScuttle/db/mssql-odbc.php new file mode 100644 index 0000000..a2d3d02 --- /dev/null +++ b/src/SemanticScuttle/db/mssql-odbc.php @@ -0,0 +1,576 @@ +<?php +/** +* +* @package dbal_odbc_mssql +* @version $Id: mssql-odbc.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'mssql-odbc'); + +/** +* @package dbal_odbc_mssql +* MSSQL ODBC Database Abstraction Layer for MSSQL +* Minimum Requirement is Version 2000+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + var $result_rowset = array(); + var $field_names = array(); + var $field_types = array(); + var $num_rows = array(); + var $current_row = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @odbc_pconnect($this->server, $this->user, $sqlpassword) : @odbc_connect($this->server, $this->user, $sqlpassword); + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + @odbc_commit($this->db_connect_id); + } + + if (sizeof($this->result_rowset)) + { + unset($this->result_rowset); + unset($this->field_names); + unset($this->field_types); + unset($this->num_rows); + unset($this->current_row); + } + + if (sizeof($this->open_queries)) + { + foreach ($this->open_queries as $i_query_id => $query_id) + { + @odbc_free_result($query_id); + } + } + + return @odbc_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @odbc_autocommit($this->db_connect_id, false); + $this->transaction = true; + break; + + case 'commit': + $result = @odbc_commit($this->db_connect_id); + @odbc_autocommit($this->db_connect_id, true); + $this->transaction = false; + + if (!$result) + { + @odbc_rollback($this->db_connect_id); + @odbc_autocommit($this->db_connect_id, true); + } + break; + + case 'rollback': + $result = @odbc_rollback($this->db_connect_id); + @odbc_autocommit($this->db_connect_id, true); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = $this->_odbc_execute_query($query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + $cache->sql_save($query, $this->query_result, $cache_ttl); + // odbc_free_result called within sql_save() + } + else if (strpos($query, 'SELECT') !== false && $this->query_result) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function _odbc_execute_query($query) + { + $result = false; + + if (eregi("^SELECT ", $query)) + { + $result = @odbc_exec($this->db_connect_id, $query); + + if ($result) + { + if (empty($this->field_names[$result])) + { + for ($i = 1, $j = @odbc_num_fields($result) + 1; $i < $j; $i++) + { + $this->field_names[$result][] = @odbc_field_name($result, $i); + $this->field_types[$result][] = @odbc_field_type($result, $i); + } + } + + $this->current_row[$result] = 0; + $this->result_rowset[$result] = array(); + + $row_outer = (isset($row_offset)) ? $row_offset + 1 : 1; + $row_outer_max = (isset($num_rows)) ? $row_offset + $num_rows + 1 : 1E9; + $row_inner = 0; + + while (@odbc_fetch_row($result, $row_outer) && $row_outer < $row_outer_max) + { + for ($i = 0, $j = sizeof($this->field_names[$result]); $i < $j; $i++) + { + $this->result_rowset[$result][$row_inner][$this->field_names[$result][$i]] = stripslashes(@odbc_result($result, $i + 1)); + } + + $row_outer++; + $row_inner++; + } + + $this->num_rows[$result] = sizeof($this->result_rowset[$result]); + } + } + else if (eregi("^INSERT ", $query)) + { + $result = @odbc_exec($this->db_connect_id, $query); + + if ($result) + { + $result_id = @odbc_exec($this->db_connect_id, 'SELECT @@IDENTITY'); + if ($result_id) + { + if (@odbc_fetch_row($result_id)) + { + $this->next_id[$this->db_connect_id] = @odbc_result($result_id, 1); + $this->affected_rows[$this->db_connect_id] = @odbc_num_rows($result); + } + } + } + } + else + { + $result = @odbc_exec($this->db_connect_id, $query); + + if ($result) + { + $this->affected_rows[$this->db_connect_id] = @odbc_num_rows($result); + } + } + + return $result; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + if ($query != '') + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = -1; + } + + $row_offset = ($total) ? $offset : ''; + $num_rows = ($total) ? $total : $offset; + + $query = 'SELECT TOP ' . ($row_offset + $num_rows) . ' ' . substr($query, 6); + + return $this->sql_query($query, $cache_ttl); + } + else + { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @$this->num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->affected_rows[$this->db_connect_id]) ? $this->affected_rows[$this->db_connect_id] : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + return ($this->num_rows[$query_id] && $this->current_row[$query_id] < $this->num_rows[$query_id]) ? $this->result_rowset[$query_id][$this->current_row[$query_id]++] : false; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($this->num_rows[$query_id]) ? $this->result_rowset[$query_id] : false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum < $this->num_rows[$query_id]) + { + $getrow = ($rownum == -1) ? $this->current_row[$query_id] - 1 : $rownum; + + return $this->result_rowset[$query_id][$getrow][$this->field_names[$query_id][$field]]; + } + } + + return false; + } + + function sql_rowseek($rownum, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->current_row[$query_id])) + { + $this->current_row[$query_id] = $rownum; + return true; + } + + return false; + } + + function sql_nextid() + { + return ($this->next_id[$this->db_connect_id]) ? $this->next_id[$this->db_connect_id] : false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + unset($this->num_rows[$query_id]); + unset($this->current_row[$query_id]); + unset($this->result_rowset[$query_id]); + unset($this->field_names[$query_id]); + unset($this->field_types[$query_id]); + + return @odbc_free_result($query_id); + } + + return false; + } + + function sql_escape($msg) + { + return str_replace("'", "''", str_replace('\\', '\\\\', $msg)); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @odbc_errormsg() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @odbc_errormsg(), + 'code' => @odbc_error() + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $this->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = $this->_odbc_execute_query($query); + + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + @odbc_free_result($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - ' . SQL_LAYER . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/mssql.php b/src/SemanticScuttle/db/mssql.php new file mode 100644 index 0000000..2b17b9e --- /dev/null +++ b/src/SemanticScuttle/db/mssql.php @@ -0,0 +1,551 @@ +<?php +/** +* +* @package dbal_mssql +* @version $Id: mssql.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'mssql'); + +/** +* @package dbal_mssql +* MSSQL Database Abstraction Layer +* Minimum Requirement is MSSQL 2000+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @mssql_pconnect($this->server, $this->user, $sqlpassword) : @mssql_connect($this->server, $this->user, $sqlpassword); + + if ($this->db_connect_id && $this->dbname != '') + { + if (!@mssql_select_db($this->dbname, $this->db_connect_id)) + { + @mssql_close($this->db_connect_id); + return false; + } + } + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + @mssql_query('COMMIT', $this->db_connect_id); + } + + if (sizeof($this->open_queries)) + { + foreach ($this->open_queries as $i_query_id => $query_id) + { + @mssql_free_result($query_id); + } + } + + return @mssql_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @mssql_query('BEGIN TRANSACTION', $this->db_connect_id); + $this->transaction = true; + break; + + case 'commit': + $result = @mssql_query('commit', $this->db_connect_id); + $this->transaction = false; + + if (!$result) + { + @mssql_query('ROLLBACK', $this->db_connect_id); + } + break; + + case 'rollback': + $result = @mssql_query('ROLLBACK', $this->db_connect_id); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = @mssql_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + $cache->sql_save($query, $this->query_result, $cache_ttl); + // sql_freeresult called within sql_save() + } + else if (strpos($query, 'SELECT') !== false && $this->query_result) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + if ($query != '') + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = -1; + } + + $row_offset = ($total) ? $offset : ''; + $num_rows = ($total) ? $total : $offset; + + $query = 'SELECT TOP ' . ($row_offset + $num_rows) . ' ' . substr($query, 6); + + return $this->sql_query($query, $cache_ttl); + } + else + { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + +// return (isset($this->limit_offset[$query_id])) ? @mssql_num_rows($query_id) - $this->limit_offset[$query_id] : @mssql_num_rows($query_id); + return ($query_id) ? @mssql_num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mssql_rows_affected($this->db_connect_id) : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + $row = @mssql_fetch_array($query_id, MSSQL_ASSOC); + + if ($row) + { + foreach ($row as $key => $value) + { + $row[$key] = ($value === ' ') ? trim($value) : $value; + } + } + + return $row; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + $result = array(); + while ($this->rowset[$query_id] = $this->sql_fetchrow($query_id)) + { + $result[] = $this->rowset[$query_id]; + } + return $result; + } + + return false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { +// (!empty($this->limit_offset[$query_id])) ? @mssql_data_seek($query_id, ($this->limit_offset[$query_id] + $rownum)) : @mssql_data_seek($query_id, $rownum); + @mssql_data_seek($query_id, $rownum); + $row = @mssql_fetch_array($query_id, MSSQL_ASSOC); + $result = isset($row[$field]) ? $row[$field] : false; + } + else + { + if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) + { + if ($this->sql_fetchrow($query_id)) + { + $result = $this->row[$query_id][$field]; + } + } + else + { + if ($this->rowset[$query_id]) + { + $result = $this->rowset[$query_id][$field]; + } + elseif ($this->row[$query_id]) + { + $result = $this->row[$query_id][$field]; + } + } + } + + return $result; + } + + return false; + } + + function sql_rowseek($rownum, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->current_row[$query_id])) + { +// (!empty($this->limit_offset[$query_id])) ? @mssql_data_seek($query_id, ($this->limit_offset[$query_id] + $rownum)) : @mssql_data_seek($query_id, $rownum); + @mssql_data_seek($query_id, $rownum); + return true; + } + + return false; + } + + function sql_nextid() + { + $result_id = @mssql_query('SELECT @@IDENTITY', $this->db_connect_id); + if ($result_id) + { + if (@mssql_fetch_array($result_id, MSSQL_ASSOC)) + { + return @mssql_result($result_id, 1); + } + } + + return false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->open_queries[$query_id])) + { + unset($this->open_queries[$query_id]); + unset($this->result_rowset[$query_id]); + + return @mssql_free_result($query_id); + } + + return false; + } + + function sql_escape($msg) + { + return str_replace("'", "''", str_replace('\\', '\\\\', $msg)); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @mssql_get_last_message() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @mssql_get_last_message($this->db_connect_id), + 'code' => '' + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $this->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @mssql_query($query, $this->db_connect_id); + while ($void = @mssql_fetch_array($result, MSSQL_ASSOC)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + @mssql_free_result($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - ' . SQL_LAYER . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/mysql.php b/src/SemanticScuttle/db/mysql.php new file mode 100644 index 0000000..a646e0d --- /dev/null +++ b/src/SemanticScuttle/db/mysql.php @@ -0,0 +1,552 @@ +<?php +/** +* +* @package dbal_mysql +* @version $Id: mysql.php,v 1.5 2006/02/10 01:30:19 scronide Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'mysql'); + +/** +* @package dbal_mysql +* MySQL Database Abstraction Layer +* Minimum Requirement is 3.23+/4.0+/4.1+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @mysql_pconnect($this->server, $this->user, $sqlpassword) : @mysql_connect($this->server, $this->user, $sqlpassword); + + if ($this->db_connect_id && $this->dbname != '') + { + if (@mysql_select_db($this->dbname)) + { + return $this->db_connect_id; + } + } + + return $this->sql_error(''); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if (sizeof($this->open_queries)) + { + foreach ($this->open_queries as $i_query_id => $query_id) + { + @mysql_free_result($query_id); + } + } + + return @mysql_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @mysql_query('BEGIN', $this->db_connect_id); + $this->transaction = true; + break; + + case 'commit': + $result = @mysql_query('COMMIT', $this->db_connect_id); + $this->transaction = false; + + if (!$result) + { + @mysql_query('ROLLBACK', $this->db_connect_id); + } + break; + + case 'rollback': + $result = @mysql_query('ROLLBACK', $this->db_connect_id); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = @mysql_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + $cache->sql_save($query, $this->query_result, $cache_ttl); + // mysql_free_result called within sql_save() + } + else if (strpos($query, 'SELECT') !== false && $this->query_result) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) { + if ($query != '') { + $this->query_result = false; + + // only limit the number of rows if $total is greater than 0 + if ($total > 0) + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } else { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysql_num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mysql_affected_rows($this->db_connect_id) : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id) ? @mysql_fetch_assoc($query_id) : false; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + $result = array(); + while ($this->rowset[$query_id] = $this->sql_fetchrow($query_id)) + { + $result[] = $this->rowset[$query_id]; + } + return $result; + } + + return false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { + $result = @mysql_result($query_id, $rownum, $field); + } + else + { + if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) + { + if ($this->sql_fetchrow($query_id)) + { + $result = $this->row[$query_id][$field]; + } + } + else + { + if ($this->rowset[$query_id]) + { + $result = $this->rowset[$query_id][$field]; + } + elseif ($this->row[$query_id]) + { + $result = $this->row[$query_id][$field]; + } + } + } + return $result; + } + return false; + } + + function sql_rowseek($rownum, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysql_data_seek($query_id, $rownum) : false; + } + + function sql_nextid() + { + return ($this->db_connect_id) ? @mysql_insert_id($this->db_connect_id) : false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @mysql_free_result($query_id); + } + + return false; + } + + function sql_escape($msg) { + if (function_exists('mysql_real_escape_string')) { + return @mysql_real_escape_string($msg, $this->db_connect_id); + } else { + return mysql_escape_string($msg); + } + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @mysql_error() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @mysql_error(), + 'code' => @mysql_errno() + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $db, $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $db->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + elseif (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = FALSE; + + if ($result = mysql_query("EXPLAIN $explain_query", $this->db_connect_id)) + { + while ($row = mysql_fetch_assoc($result)) + { + if (!$html_table && sizeof($row)) + { + $html_table = TRUE; + $html_hold .= '<table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0" align="center"><tr>'; + + foreach (array_keys($row) as $val) + { + $html_hold .= '<th nowrap="nowrap">' . (($val) ? ucwords(str_replace('_', ' ', $val)) : ' ') . '</th>'; + } + $html_hold .= '</tr>'; + } + $html_hold .= '<tr>'; + + $class = 'row1'; + foreach (array_values($row) as $val) + { + $class = ($class == 'row1') ? 'row2' : 'row1'; + $html_hold .= '<td class="' . $class . '">' . (($val) ? $val : ' ') . '</td>'; + } + $html_hold .= '</tr>'; + } + } + + if ($html_table) + { + $html_hold .= '</table>'; + } + } + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = mysql_query($query, $this->db_connect_id); + while ($void = mysql_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + mysql_free_result($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - MySQL Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/mysql4.php b/src/SemanticScuttle/db/mysql4.php new file mode 100644 index 0000000..0639518 --- /dev/null +++ b/src/SemanticScuttle/db/mysql4.php @@ -0,0 +1,552 @@ +<?php +/** +* +* @package dbal_mysql4 +* @version $Id: mysql4.php,v 1.4 2006/02/10 01:30:19 scronide Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'mysql4'); + +/** +* @package dbal_mysql4 +* MySQL4 Database Abstraction Layer +* Minimum Requirement is 4.0+/4.1+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @mysql_pconnect($this->server, $this->user, $sqlpassword) : @mysql_connect($this->server, $this->user, $sqlpassword); + + if ($this->db_connect_id && $this->dbname != '') + { + if (@mysql_select_db($this->dbname)) + { + return $this->db_connect_id; + } + } + + return $this->sql_error(''); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if (sizeof($this->open_queries)) + { + foreach ($this->open_queries as $i_query_id => $query_id) + { + @mysql_free_result($query_id); + } + } + + return @mysql_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @mysql_query('BEGIN', $this->db_connect_id); + $this->transaction = true; + break; + + case 'commit': + $result = @mysql_query('COMMIT', $this->db_connect_id); + $this->transaction = false; + + if (!$result) + { + @mysql_query('ROLLBACK', $this->db_connect_id); + } + break; + + case 'rollback': + $result = @mysql_query('ROLLBACK', $this->db_connect_id); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = @mysql_query($query, $this->db_connect_id)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + $cache->sql_save($query, $this->query_result, $cache_ttl); + // mysql_free_result called within sql_save() + } + else if (strpos($query, 'SELECT') !== false && $this->query_result) + { + $this->open_queries[(int) $this->query_result] = $this->query_result; + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) { + if ($query != '') { + $this->query_result = false; + + // only limit the number of rows if $total is greater than 0 + if ($total > 0) + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } else { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysql_num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mysql_affected_rows($this->db_connect_id) : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id) ? @mysql_fetch_assoc($query_id) : false; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + $result = array(); + while ($this->rowset[$query_id] = $this->sql_fetchrow($query_id)) + { + $result[] = $this->rowset[$query_id]; + } + return $result; + } + + return false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { + $result = @mysql_result($query_id, $rownum, $field); + } + else + { + if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) + { + if ($this->sql_fetchrow($query_id)) + { + $result = $this->row[$query_id][$field]; + } + } + else + { + if ($this->rowset[$query_id]) + { + $result = $this->rowset[$query_id][$field]; + } + elseif ($this->row[$query_id]) + { + $result = $this->row[$query_id][$field]; + } + } + } + return $result; + } + return false; + } + + function sql_rowseek($rownum, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysql_data_seek($query_id, $rownum) : false; + } + + function sql_nextid() + { + return ($this->db_connect_id) ? @mysql_insert_id($this->db_connect_id) : false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (isset($this->open_queries[(int) $query_id])) + { + unset($this->open_queries[(int) $query_id]); + return @mysql_free_result($query_id); + } + + return false; + } + + function sql_escape($msg) { + if (function_exists('mysql_real_escape_string')) { + return @mysql_real_escape_string($msg, $this->db_connect_id); + } else { + return mysql_escape_string($msg); + } + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @mysql_error() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @mysql_error(), + 'code' => @mysql_errno() + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $db, $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $db->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + elseif (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = FALSE; + + if ($result = mysql_query("EXPLAIN $explain_query", $this->db_connect_id)) + { + while ($row = mysql_fetch_assoc($result)) + { + if (!$html_table && sizeof($row)) + { + $html_table = TRUE; + $html_hold .= '<table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0" align="center"><tr>'; + + foreach (array_keys($row) as $val) + { + $html_hold .= '<th nowrap="nowrap">' . (($val) ? ucwords(str_replace('_', ' ', $val)) : ' ') . '</th>'; + } + $html_hold .= '</tr>'; + } + $html_hold .= '<tr>'; + + $class = 'row1'; + foreach (array_values($row) as $val) + { + $class = ($class == 'row1') ? 'row2' : 'row1'; + $html_hold .= '<td class="' . $class . '">' . (($val) ? $val : ' ') . '</td>'; + } + $html_hold .= '</tr>'; + } + } + + if ($html_table) + { + $html_hold .= '</table>'; + } + } + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = mysql_query($query, $this->db_connect_id); + while ($void = mysql_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + mysql_free_result($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - MySQL Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/mysqli.php b/src/SemanticScuttle/db/mysqli.php new file mode 100644 index 0000000..9a2709a --- /dev/null +++ b/src/SemanticScuttle/db/mysqli.php @@ -0,0 +1,562 @@ +<?php +/** +* +* @package dbal_mysqli +* @version $Id: mysqli.php,v 1.4 2006/02/10 01:30:19 scronide Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'mysqli'); + +/** +* @package dbal_mysqli +* MySQLi Database Abstraction Layer +* Minimum Requirement is MySQL 4.1+ and the mysqli-extension +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + var $indexed = 0; + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @mysqli_pconnect($this->server, $this->user, $sqlpassword) : @mysqli_connect($this->server, $this->user, $sqlpassword); + + if ($this->db_connect_id && $this->dbname != '') + { + if (@mysqli_select_db($this->db_connect_id, $this->dbname)) + { + return $this->db_connect_id; + } + } + + return $this->sql_error(mysqli_connect_error()); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + @mysqli_commit($this->db_connect_id); + } + + return @mysqli_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @mysqli_autocommit($this->db_connect_id, false); + $this->transaction = true; + break; + + case 'commit': + $result = @mysqli_commit($this->db_connect_id); + @mysqli_autocommit($this->db_connect_id, true); + $this->transaction = false; + + if (!$result) + { + @mysqli_rollback($this->db_connect_id); + @mysqli_autocommit($this->db_connect_id, true); + } + break; + + case 'rollback': + $result = @mysqli_rollback($this->db_connect_id); + @mysqli_autocommit($this->db_connect_id, true); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + + if (($this->query_result = @mysqli_query($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + if (is_object($this->query_result)) + { + $this->query_result->cur_index = $this->indexed++; + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $cache->sql_save($query, $this->query_result, $cache_ttl); + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) { + if ($query != '') { + $this->query_result = false; + + // only limit the number of rows if $total is greater than 0 + if ($total > 0) + $query .= "\n LIMIT " . ((!empty($offset)) ? $offset . ', ' . $total : $total); + + return $this->sql_query($query, $cache_ttl); + } else { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysqli_num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->db_connect_id) ? @mysqli_affected_rows($this->db_connect_id) : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (!is_object($query_id) && isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id) ? @mysqli_fetch_assoc($query_id) : false; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + $cur_index = (is_object($query_id)) ? $query_id->cur_index : $query_id; + + unset($this->rowset[$cur_index]); + unset($this->row[$cur_index]); + + $result = array(); + while ($this->rowset[$cur_index] = $this->sql_fetchrow($query_id)) + { + $result[] = $this->rowset[$cur_index]; + } + return $result; + } + + return false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { + @mysqli_data_seek($query_id, $rownum); + $row = @mysqli_fetch_assoc($query_id); + $result = isset($row[$field]) ? $row[$field] : false; + } + else + { + $cur_index = (is_object($query_id)) ? $query_id->cur_index : $query_id; + + if (empty($this->row[$cur_index]) && empty($this->rowset[$cur_index])) + { + if ($this->row[$cur_index] = $this->sql_fetchrow($query_id)) + { + $result = $this->row[$cur_index][$field]; + } + } + else + { + if ($this->rowset[$cur_index]) + { + $result = $this->rowset[$cur_index][$field]; + } + elseif ($this->row[$cur_index]) + { + $result = $this->row[$cur_index][$field]; + } + } + } + return $result; + } + return false; + } + + function sql_rowseek($rownum, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @mysqli_data_seek($query_id, $rownum) : false; + } + + function sql_nextid() + { + return ($this->db_connect_id) ? @mysqli_insert_id($this->db_connect_id) : false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + $cur_index = (is_object($query_id)) ? $query_id->cur_index : $query_id; + + unset($this->rowset[$cur_index]); + unset($this->row[$cur_index]); + + if (is_object($query_id)) + { + $this->indexed--; + return @mysqli_free_result($query_id); + } + else + { + return false; + } + } + + function sql_escape($msg) { + return mysqli_real_escape_string($this->db_connect_id, $msg); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @mysqli_error($this->db_connect_id) . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @mysqli_error($this->db_connect_id), + 'code' => @mysqli_errno($this->db_connect_id) + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $db, $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $db->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>DB query explanation</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $explain_query = $query; + if (preg_match('/UPDATE ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + elseif (preg_match('/DELETE FROM ([a-z0-9_]+).*?WHERE(.*)/s', $query, $m)) + { + $explain_query = 'SELECT * FROM ' . $m[1] . ' WHERE ' . $m[2]; + } + + if (preg_match('/^SELECT/', $explain_query)) + { + $html_table = FALSE; + + if ($result = @mysqli_query($this->db_connect_id, "EXPLAIN $explain_query")) + { + while ($row = @mysqli_fetch_assoc($result)) + { + if (!$html_table && sizeof($row)) + { + $html_table = TRUE; + $html_hold .= '<table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0" align="center"><tr>'; + + foreach (array_keys($row) as $val) + { + $html_hold .= '<th nowrap="nowrap">' . (($val) ? ucwords(str_replace('_', ' ', $val)) : ' ') . '</th>'; + } + $html_hold .= '</tr>'; + } + $html_hold .= '<tr>'; + + $class = 'row1'; + foreach (array_values($row) as $val) + { + $class = ($class == 'row1') ? 'row2' : 'row1'; + $html_hold .= '<td class="' . $class . '">' . (($val) ? $val : ' ') . '</td>'; + } + $html_hold .= '</tr>'; + } + } + + if ($html_table) + { + $html_hold .= '</table>'; + } + } + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @mysqli_query($this->db_connect_id, $query); + while ($void = @mysqli_fetch_assoc($result)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + @mysqli_free_result($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - MySQL Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/oracle.php b/src/SemanticScuttle/db/oracle.php new file mode 100644 index 0000000..7ef10e5 --- /dev/null +++ b/src/SemanticScuttle/db/oracle.php @@ -0,0 +1,468 @@ +<?php +/** +* +* @package dbal_oracle +* @version $Id: oracle.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if(!defined("SQL_LAYER")) +{ + +define("SQL_LAYER","oracle"); + +/** +* @package dbal_oracle +* Oracle Database Abstraction Layer +*/ +class sql_db +{ + + var $db_connect_id; + var $query_result; + var $in_transaction = 0; + var $row = array(); + var $rowset = array(); + var $num_queries = 0; + var $last_query_text = ""; + + // + // Constructor + // + function sql_db($sqlserver, $sqluser, $sqlpassword, $database="", $persistency = true) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->password = $sqlpassword; + $this->server = $sqlserver; + $this->dbname = $database; + + if($this->persistency) + { + $this->db_connect_id = @OCIPLogon($this->user, $this->password, $this->server); + } + else + { + $this->db_connect_id = @OCINLogon($this->user, $this->password, $this->server); + } + if($this->db_connect_id) + { + return $this->db_connect_id; + } + else + { + return false; + } + } + + // + // Other base methods + // + function sql_close() + { + if($this->db_connect_id) + { + // Commit outstanding transactions + if($this->in_transaction) + { + OCICommit($this->db_connect_id); + } + + if($this->query_result) + { + @OCIFreeStatement($this->query_result); + } + $result = @OCILogoff($this->db_connect_id); + return $result; + } + else + { + return false; + } + } + + // + // Base query method + // + function sql_query($query = "", $transaction = FALSE) + { + // Remove any pre-existing queries + unset($this->query_result); + + // Put us in transaction mode because with Oracle as soon as you make a query you're in a transaction + $this->in_transaction = TRUE; + + if($query != "") + { + $this->last_query = $query; + $this->num_queries++; + + if(eregi("LIMIT", $query)) + { + preg_match("/^(.*)LIMIT ([0-9]+)[, ]*([0-9]+)*/s", $query, $limits); + + $query = $limits[1]; + if($limits[3]) + { + $row_offset = $limits[2]; + $num_rows = $limits[3]; + } + else + { + $row_offset = 0; + $num_rows = $limits[2]; + } + } + + if(eregi("^(INSERT|UPDATE) ", $query)) + { + $query = preg_replace("/\\\'/s", "''", $query); + } + + $this->query_result = @OCIParse($this->db_connect_id, $query); + $success = @OCIExecute($this->query_result, OCI_DEFAULT); + } + if($success) + { + if($transaction == END_TRANSACTION) + { + OCICommit($this->db_connect_id); + $this->in_transaction = FALSE; + } + + unset($this->row[$this->query_result]); + unset($this->rowset[$this->query_result]); + $this->last_query_text[$this->query_result] = $query; + + return $this->query_result; + } + else + { + if($this->in_transaction) + { + OCIRollback($this->db_connect_id); + } + return false; + } + } + + // + // Other query methods + // + function sql_numrows($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @OCIFetchStatement($query_id, $this->rowset); + // OCIFetchStatment kills our query result so we have to execute the statment again + // if we ever want to use the query_id again. + @OCIExecute($query_id, OCI_DEFAULT); + return $result; + } + else + { + return false; + } + } + function sql_affectedrows($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @OCIRowCount($query_id); + return $result; + } + else + { + return false; + } + } + function sql_numfields($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @OCINumCols($query_id); + return $result; + } + else + { + return false; + } + } + function sql_fieldname($offset, $query_id = 0) + { + // OCIColumnName uses a 1 based array so we have to up the offset by 1 in here to maintain + // full abstraction compatibitly + $offset += 1; + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = strtolower(@OCIColumnName($query_id, $offset)); + return $result; + } + else + { + return false; + } + } + function sql_fieldtype($offset, $query_id = 0) + { + // This situation is the same as fieldname + $offset += 1; + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @OCIColumnType($query_id, $offset); + return $result; + } + else + { + return false; + } + } + function sql_fetchrow($query_id = 0, $debug = FALSE) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result_row = ""; + $result = @OCIFetchInto($query_id, $result_row, OCI_ASSOC+OCI_RETURN_NULLS); + if($debug) + { + echo "Query was: ".$this->last_query . "<br>"; + echo "Result: $result<br>"; + echo "Query ID: $query_id<br>"; + echo "<pre>"; + var_dump($result_row); + echo "</pre>"; + } + if($result_row == "") + { + return false; + } + + for($i = 0; $i < count($result_row); $i++) + { + list($key, $val) = each($result_row); + $return_arr[strtolower($key)] = $val; + } + $this->row[$query_id] = $return_arr; + + return $this->row[$query_id]; + } + else + { + return false; + } + } + // This function probably isn't as efficant is it could be but any other way I do it + // I end up losing 1 row... + function sql_fetchrowset($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $rows = @OCIFetchStatement($query_id, $results); + @OCIExecute($query_id, OCI_DEFAULT); + for($i = 0; $i <= $rows; $i++) + { + @OCIFetchInto($query_id, $tmp_result, OCI_ASSOC+OCI_RETURN_NULLS); + + for($j = 0; $j < count($tmp_result); $j++) + { + list($key, $val) = each($tmp_result); + $return_arr[strtolower($key)] = $val; + } + $result[] = $return_arr; + } + return $result; + } + else + { + return false; + } + } + function sql_fetchfield($field, $rownum = -1, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + if($rownum > -1) + { + // Reset the internal rownum pointer. + @OCIExecute($query_id, OCI_DEFAULT); + for($i = 0; $i < $rownum; $i++) + { + // Move the interal pointer to the row we want + @OCIFetch($query_id); + } + // Get the field data. + $result = @OCIResult($query_id, strtoupper($field)); + } + else + { + // The internal pointer should be where we want it + // so we just grab the field out of the current row. + $result = @OCIResult($query_id, strtoupper($field)); + } + return $result; + } + else + { + return false; + } + } + function sql_rowseek($rownum, $query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + @OCIExecute($query_id, OCI_DEFAULT); + for($i = 0; $i < $rownum; $i++) + { + @OCIFetch($query_id); + } + $result = @OCIFetch($query_id); + return $result; + } + else + { + return false; + } + } + function sql_nextid($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id && $this->last_query_text[$query_id] != "") + { + if( eregi("^(INSERT{1}|^INSERT INTO{1})[[:space:]][\"]?([a-zA-Z0-9\_\-]+)[\"]?", $this->last_query_text[$query_id], $tablename)) + { + $query = "SELECT ".$tablename[2]."_id_seq.currval FROM DUAL"; + $stmt = @OCIParse($this->db_connect_id, $query); + @OCIExecute($stmt,OCI_DEFAULT ); + $temp_result = @OCIFetchInto($stmt, $temp_result, OCI_ASSOC+OCI_RETURN_NULLS); + if($temp_result) + { + return $temp_result['CURRVAL']; + } + else + { + return false; + } + } + else + { + return false; + } + } + else + { + return false; + } + } + + function sql_nextid($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id && $this->last_query_text[$query_id] != "") + { + if( eregi("^(INSERT{1}|^INSERT INTO{1})[[:space:]][\"]?([a-zA-Z0-9\_\-]+)[\"]?", $this->last_query_text[$query_id], $tablename)) + { + $query = "SELECT ".$tablename[2]."_id_seq.CURRVAL FROM DUAL"; + $temp_q_id = @OCIParse($this->db_connect_id, $query); + @OCIExecute($temp_q_id, OCI_DEFAULT); + @OCIFetchInto($temp_q_id, $temp_result, OCI_ASSOC+OCI_RETURN_NULLS); + + if($temp_result) + { + return $temp_result['CURRVAL']; + } + else + { + return false; + } + } + else + { + return false; + } + } + else + { + return false; + } + } + + + + function sql_freeresult($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + if($query_id) + { + $result = @OCIFreeStatement($query_id); + return $result; + } + else + { + return false; + } + } + function sql_error($query_id = 0) + { + if(!$query_id) + { + $query_id = $this->query_result; + } + $result = @OCIError($query_id); + return $result; + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/postgres.php b/src/SemanticScuttle/db/postgres.php new file mode 100644 index 0000000..b5bad20 --- /dev/null +++ b/src/SemanticScuttle/db/postgres.php @@ -0,0 +1,597 @@ +<?php +/** +* +* @package dbal_postgres +* @version $Id: postgres.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined('SQL_LAYER')) +{ + +define('SQL_LAYER', 'postgresql'); + +/** +* @package dbal_postgres +* PostgreSQL Database Abstraction Layer +* Minimum Requirement is Version 7.3+ +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port = false, $persistency = false) + { + $this->connect_string = ''; + + if ($sqluser) + { + $this->connect_string .= "user=$sqluser "; + } + + if ($sqlpassword) + { + $this->connect_string .= "password=$sqlpassword "; + } + + if ($sqlserver) + { + if (ereg(":", $sqlserver)) + { + list($sqlserver, $sqlport) = split(":", $sqlserver); + $this->connect_string .= "host=$sqlserver port=$sqlport "; + } + else + { + if ($sqlserver != "localhost") + { + $this->connect_string .= "host=$sqlserver "; + } + + if ($port) + { + $this->connect_string .= "port=$port "; + } + } + } + + if ($database) + { + $this->dbname = $database; + $this->connect_string .= "dbname=$database"; + } + + $this->persistency = $persistency; + + $this->db_connect_id = ($this->persistency) ? @pg_pconnect($this->connect_string) : @pg_connect($this->connect_string); + + return ($this->db_connect_id) ? $this->db_connect_id : $this->sql_error(''); + } + + // + // Other base methods + // + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + if ($this->transaction) + { + @pg_exec($this->db_connect_id, 'COMMIT'); + } + + return @pg_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $result = @pg_exec($this->db_connect_id, 'BEGIN'); + $this->transaction = true; + break; + + case 'commit': + $result = @pg_exec($this->db_connect_id, 'COMMIT'); + $this->transaction = false; + + if (!$result) + { + @pg_exec($this->db_connect_id, 'ROLLBACK'); + } + break; + + case 'rollback': + $result = @pg_exec($this->db_connect_id, 'ROLLBACK'); + $this->transaction = false; + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $cache_ttl = 0) + { + if ($query != '') + { + global $cache; + + // EXPLAIN only in extra debug mode + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('start', $query); + } + + $this->query_result = ($cache_ttl && method_exists($cache, 'sql_load')) ? $cache->sql_load($query) : false; + + if (!$this->query_result) + { + $this->num_queries++; + $this->last_query_text = $query; + + if (($this->query_result = @pg_exec($this->db_connect_id, $query)) === false) + { + $this->sql_error($query); + } + + if (defined('DEBUG_EXTRA')) + { + $this->sql_report('stop', $query); + } + + if ($cache_ttl && method_exists($cache, 'sql_save')) + { + $cache->sql_save($query, $this->query_result, $cache_ttl); + } + } + else if (defined('DEBUG_EXTRA')) + { + $this->sql_report('fromcache', $query); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $cache_ttl = 0) + { + if ($query != '') + { + $this->query_result = false; + + // if $total is set to 0 we do not want to limit the number of rows + if ($total == 0) + { + $total = -1; + } + + $query .= "\n LIMIT $total OFFSET $offset"; + + return $this->sql_query($query, $cache_ttl); + } + else + { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE' || $query == 'SELECT') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(($query == 'UPDATE') ? ', ' : ' AND ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @pg_numrows($query_id) : false; + } + + function sql_affectedrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @pg_cmdtuples($query_id) : false; + } + + function sql_fetchrow($query_id = false) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if (!isset($this->rownum[$query_id])) + { + $this->rownum[$query_id] = 0; + } + + if (isset($cache->sql_rowset[$query_id])) + { + return $cache->sql_fetchrow($query_id); + } + + $result = @pg_fetch_array($query_id, NULL, PGSQL_ASSOC); + + if ($result) + { + $this->rownum[$query_id]++; + } + + return $result; + } + + function sql_fetchrowset($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + $result = array(); + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + + $result = array(); + while ($this->rowset[$query_id] = $this->sql_fetchrow($query_id)) + { + $result[] = $this->rowset[$query_id]; + } + return $result; + } + + return false; + } + + function sql_fetchfield($field, $rownum = -1, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($rownum > -1) + { + if (@function_exists('pg_result_seek')) + { + @pg_result_seek($query_id, $rownum); + $row = @pg_fetch_assoc($query_id); + $result = isset($row[$field]) ? $row[$field] : false; + } + else + { + $this->sql_rowseek($offset, $query_id); + $row = $this->sql_fetchrow($query_id); + $result = isset($row[$field]) ? $row[$field] : false; + } + } + else + { + if (empty($this->row[$query_id]) && empty($this->rowset[$query_id])) + { + if ($this->sql_fetchrow($query_id)) + { + $result = $this->row[$query_id][$field]; + } + } + else + { + if ($this->rowset[$query_id]) + { + $result = $this->rowset[$query_id][$field]; + } + elseif ($this->row[$query_id]) + { + $result = $this->row[$query_id][$field]; + } + } + } + return $result; + } + return false; + } + + function sql_rowseek($offset, $query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + if ($offset > -1) + { + if (@function_exists('pg_result_seek')) + { + @pg_result_seek($query_id, $rownum); + } + else + { + for ($i = $this->rownum[$query_id]; $i < $offset; $i++) + { + $this->sql_fetchrow($query_id); + } + } + return true; + } + else + { + return false; + } + } + + return false; + } + + function sql_nextid() + { + $query_id = $this->query_result; + + if ($query_id && $this->last_query_text != '') + { + if (preg_match("/^INSERT[\t\n ]+INTO[\t\n ]+([a-z0-9\_\-]+)/is", $this->last_query_text, $tablename)) + { + $query = "SELECT currval('" . $tablename[1] . "_id_seq') AS last_value"; + $temp_q_id = @pg_exec($this->db_connect_id, $query); + if (!$temp_q_id) + { + return false; + } + + $temp_result = @pg_fetch_array($temp_q_id, NULL, PGSQL_ASSOC); + + return ($temp_result) ? $temp_result['last_value'] : false; + } + } + + return false; + } + + function sql_freeresult($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return (is_resource($query_id)) ? @pg_freeresult($query_id) : false; + } + + function sql_escape($msg) + { + return str_replace("'", "''", str_replace('\\', '\\\\', $msg)); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (isset($_SERVER['PHP_SELF']) && !empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((isset($_SERVER['QUERY_STRING']) && !empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : (isset($_ENV['QUERY_STRING']) ? $_ENV['QUERY_STRING'] : '')); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @pg_errormessage() . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @pg_errormessage(), + 'code' => '' + ); + + return $result; + } + + function sql_report($mode, $query = '') + { + if (empty($_GET['explain'])) + { + return; + } + + global $cache, $starttime, $phpbb_root_path; + static $curtime, $query_hold, $html_hold; + static $sql_report = ''; + static $cache_num_queries = 0; + + if (!$query && !empty($query_hold)) + { + $query = $query_hold; + } + + switch ($mode) + { + case 'display': + if (!empty($cache)) + { + $cache->unload(); + } + $this->sql_close(); + + $mtime = explode(' ', microtime()); + $totaltime = $mtime[0] + $mtime[1] - $starttime; + + echo '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8869-1"><meta http-equiv="Content-Style-Type" content="text/css"><link rel="stylesheet" href="' . $phpbb_root_path . 'adm/subSilver.css" type="text/css"><style type="text/css">' . "\n"; + echo 'th { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic3.gif\') }' . "\n"; + echo 'td.cat { background-image: url(\'' . $phpbb_root_path . 'adm/images/cellpic1.gif\') }' . "\n"; + echo '</style><title>' . $msg_title . '</title></head><body>'; + echo '<table width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td><a href="' . htmlspecialchars(preg_replace('/&explain=([^&]*)/', '', $_SERVER['REQUEST_URI'])) . '"><img src="' . $phpbb_root_path . 'adm/images/header_left.jpg" width="200" height="60" alt="phpBB Logo" title="phpBB Logo" border="0"/></a></td><td width="100%" background="' . $phpbb_root_path . 'adm/images/header_bg.jpg" height="60" align="right" nowrap="nowrap"><span class="maintitle">SQL Report</span> </td></tr></table><br clear="all"/><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td height="40" align="center" valign="middle"><b>Page generated in ' . round($totaltime, 4) . " seconds with {$this->num_queries} queries" . (($cache_num_queries) ? " + $cache_num_queries " . (($cache_num_queries == 1) ? 'query' : 'queries') . ' returning data from cache' : '') . '</b></td></tr><tr><td align="center" nowrap="nowrap">Time spent on MySQL queries: <b>' . round($this->sql_time, 5) . 's</b> | Time spent on PHP: <b>' . round($totaltime - $this->sql_time, 5) . 's</b></td></tr></table><table width="95%" cellspacing="1" cellpadding="4" border="0" align="center"><tr><td>'; + echo $sql_report; + echo '</td></tr></table><br /></body></html>'; + exit; + break; + + case 'start': + $query_hold = $query; + $html_hold = ''; + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1]; + break; + + case 'fromcache': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $result = @pg_exec($this->db_connect_id, $query); + while ($void = @pg_fetch_array($result, NULL, PGSQL_ASSOC)) + { + // Take the time spent on parsing rows into account + } + $splittime = explode(' ', microtime()); + $splittime = $splittime[0] + $splittime[1]; + + $time_cache = $endtime - $curtime; + $time_db = $splittime - $endtime; + $color = ($time_db > $time_cache) ? 'green' : 'red'; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query results obtained from the cache</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table><p align="center">'; + + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed [cache]: <b style="color: ' . $color . '">' . sprintf('%.5f', ($time_cache)) . 's</b> | Elapsed [db]: <b>' . sprintf('%.5f', $time_db) . 's</b></p>'; + + // Pad the start time to not interfere with page timing + $starttime += $time_db; + + @pg_freeresult($result); + $cache_num_queries++; + break; + + case 'stop': + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1]; + + $sql_report .= '<hr width="100%"/><br /><table class="bg" width="100%" cellspacing="1" cellpadding="4" border="0"><tr><th>Query #' . $this->num_queries . '</th></tr><tr><td class="row1"><textarea style="font-family:\'Courier New\',monospace;width:100%" rows="5">' . preg_replace('/\t(AND|OR)(\W)/', "\$1\$2", htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n", $query))) . '</textarea></td></tr></table> ' . $html_hold . '<p align="center">'; + + if ($this->query_result) + { + if (preg_match('/^(UPDATE|DELETE|REPLACE)/', $query)) + { + $sql_report .= "Affected rows: <b>" . $this->sql_affectedrows($this->query_result) . '</b> | '; + } + $sql_report .= 'Before: ' . sprintf('%.5f', $curtime - $starttime) . 's | After: ' . sprintf('%.5f', $endtime - $starttime) . 's | Elapsed: <b>' . sprintf('%.5f', $endtime - $curtime) . 's</b>'; + } + else + { + $error = $this->sql_error(); + $sql_report .= '<b style="color: red">FAILED</b> - ' . SQL_LAYER . ' Error ' . $error['code'] . ': ' . htmlspecialchars($error['message']); + } + + $sql_report .= '</p>'; + + $this->sql_time += $endtime - $curtime; + break; + } + } + +} // class ... db_sql + +} // if ... defined + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/db/sqlite.php b/src/SemanticScuttle/db/sqlite.php new file mode 100644 index 0000000..1591396 --- /dev/null +++ b/src/SemanticScuttle/db/sqlite.php @@ -0,0 +1,387 @@ +<?php +/** +* +* @package dbal_sqlite +* @version $Id: sqlite.php,v 1.2 2005/06/10 08:52:03 devalley Exp $ +* @copyright (c) 2005 phpBB Group +* @license http://opensource.org/licenses/gpl-license.php GNU Public License +* +*/ + +/** +* @ignore +*/ +if (!defined("SQL_LAYER")) +{ + +define("SQL_LAYER","sqlite"); + +/** +* @package dbal_sqlite +* Sqlite Database Abstraction Layer +*/ +class sql_db +{ + var $db_connect_id; + var $query_result; + var $return_on_error = false; + var $transaction = false; + var $sql_report = ''; + var $sql_time = 0; + var $num_queries = 0; + var $open_queries = array(); + + function sql_connect($sqlserver, $sqluser, $sqlpassword, $database, $port, $persistency = false) + { + $this->persistency = $persistency; + $this->user = $sqluser; + $this->server = $sqlserver . (($port) ? ':' . $port : ''); + $this->dbname = $database; + + $this->db_connect_id = ($this->persistency) ? @sqlite_popen($this->server, 0, $error) : @sqlite_open($this->server, 0, $error); + + return ($this->db_connect_id) ? true : $error; + } + + // Other base methods + function sql_close() + { + if (!$this->db_connect_id) + { + return false; + } + + return @sqlite_close($this->db_connect_id); + } + + function sql_return_on_error($fail = false) + { + $this->return_on_error = $fail; + } + + function sql_num_queries() + { + return $this->num_queries; + } + + function sql_transaction($status = 'begin') + { + switch ($status) + { + case 'begin': + $this->transaction = true; + $result = @sqlite_query('BEGIN', $this->db_connect_id); + break; + + case 'commit': + $this->transaction = false; + $result = @sqlite_query('COMMIT', $this->db_connect_id); + break; + + case 'rollback': + $this->transaction = false; + $result = @sqlite_query('ROLLBACK', $this->db_connect_id); + break; + + default: + $result = true; + } + + return $result; + } + + // Base query method + function sql_query($query = '', $expire_time = 0) + { + if ($query != '') + { + global $cache; + + $query = preg_replace('#FROM \((.*?)\)(,|[\n\t ]+?WHERE) #s', 'FROM \1\2 ', $query); + + if (!$expire_time || !$cache->sql_load($query, $expire_time)) + { + if ($expire_time) + { + $cache_result = true; + } + + $this->query_result = false; + $this->num_queries++; + + if (!empty($_GET['explain'])) + { + global $starttime; + + $curtime = explode(' ', microtime()); + $curtime = $curtime[0] + $curtime[1] - $starttime; + } + + if (!($this->query_result = @sqlite_query($query, $this->db_connect_id))) + { + $this->sql_error($query); + } + + if (!empty($_GET['explain'])) + { + $endtime = explode(' ', microtime()); + $endtime = $endtime[0] + $endtime[1] - $starttime; + + $this->sql_report .= "<pre>Query:\t" . htmlspecialchars(preg_replace('/[\s]*[\n\r\t]+[\n\r\s\t]*/', "\n\t", $query)) . "\n\n"; + + if ($this->query_result) + { + $this->sql_report .= "Time before: $curtime\nTime after: $endtime\nElapsed time: <b>" . ($endtime - $curtime) . "</b>\n</pre>"; + } + else + { + $error = $this->sql_error(); + $this->sql_report .= '<b>FAILED</b> - SQLite ' . $error['code'] . ': ' . htmlspecialchars($error['message']) . '<br><br><pre>'; + } + + $this->sql_time += $endtime - $curtime; + + if (preg_match('#^SELECT#', $query)) + { + $html_table = FALSE; + if ($result = @sqlite_query("EXPLAIN $query", $this->db_connect_id)) + { + while ($row = @sqlite_fetch_array($result, @sqlite_ASSOC)) + { + if (!$html_table && sizeof($row)) + { + $html_table = TRUE; + $this->sql_report .= "<table width=100% border=1 cellpadding=2 cellspacing=1>\n"; + $this->sql_report .= "<tr>\n<td><b>" . implode("</b></td>\n<td><b>", array_keys($row)) . "</b></td>\n</tr>\n"; + } + $this->sql_report .= "<tr>\n<td>" . implode(" </td>\n<td>", array_values($row)) . " </td>\n</tr>\n"; + } + } + + if ($html_table) + { + $this->sql_report .= '</table><br>'; + } + } + + $this->sql_report .= "<hr>\n"; + } + + if (preg_match('#^SELECT#', $query)) + { + $this->open_queries[] = $this->query_result; + } + } + + if (!empty($cache_result)) + { + $cache->sql_save($query, $this->query_result); + } + } + else + { + return false; + } + + return ($this->query_result) ? $this->query_result : false; + } + + function sql_query_limit($query, $total, $offset = 0, $expire_time = 0) + { + if ($query != '') + { + $this->query_result = false; + + $query .= ' LIMIT ' . ((!empty($offset)) ? $total . ' OFFSET ' . $offset : $total); + + return $this->sql_query($query, $expire_time); + } + else + { + return false; + } + } + + // Idea for this from Ikonboard + function sql_build_array($query, $assoc_ary = false) + { + if (!is_array($assoc_ary)) + { + return false; + } + + $fields = array(); + $values = array(); + if ($query == 'INSERT') + { + foreach ($assoc_ary as $key => $var) + { + $fields[] = $key; + + if (is_null($var)) + { + $values[] = 'NULL'; + } + elseif (is_string($var)) + { + $values[] = "'" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? intval($var) : $var; + } + } + + $query = ' (' . implode(', ', $fields) . ') VALUES (' . implode(', ', $values) . ')'; + } + else if ($query == 'UPDATE') + { + $values = array(); + foreach ($assoc_ary as $key => $var) + { + if (is_null($var)) + { + $values[] = "$key = NULL"; + } + elseif (is_string($var)) + { + $values[] = "$key = '" . $this->sql_escape($var) . "'"; + } + else + { + $values[] = (is_bool($var)) ? "$key = " . intval($var) : "$key = $var"; + } + } + $query = implode(', ', $values); + } + + return $query; + } + + // Other query methods + // + // NOTE :: Want to remove _ALL_ reliance on sql_numrows from core code ... + // don't want this here by a middle Milestone + function sql_numrows($query_id = false) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @sqlite_num_rows($query_id) : false; + } + + function sql_affectedrows() + { + return ($this->db_connect_id) ? @sqlite_changes($this->db_connect_id) : false; + } + + function sql_fetchrow($query_id = 0) + { + global $cache; + + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($cache->sql_exists($query_id)) + { + return $cache->sql_fetchrow($query_id); + } + + return ($query_id) ? @sqlite_fetch_array($query_id, @sqlite_ASSOC) : false; + } + + function sql_fetchrowset($query_id = 0) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + unset($this->rowset[$query_id]); + unset($this->row[$query_id]); + while ($this->rowset[$query_id] = @sqlite_fetch_array($query_id, @sqlite_ASSOC)) + { + $result[] = $this->rowset[$query_id]; + } + return $result; + } + else + { + return false; + } + } + + function sql_fetchfield($field, $rownum = -1, $query_id = 0) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + if ($query_id) + { + return ($rownum > -1) ? ((@sqlite_seek($query_id, $rownum)) ? @sqlite_column($query_id, $field) : false) : @sqlite_column($query_id, $field); + } + } + + function sql_rowseek($rownum, $query_id = 0) + { + if (!$query_id) + { + $query_id = $this->query_result; + } + + return ($query_id) ? @sqlite_seek($query_id, $rownum) : false; + } + + function sql_nextid() + { + return ($this->db_connect_id) ? @sqlite_last_insert_rowid($this->db_connect_id) : false; + } + + function sql_freeresult($query_id = false) + { + return true; + } + + function sql_escape($msg) + { + return @sqlite_escape_string(stripslashes($msg)); + } + + function sql_error($sql = '') + { + if (!$this->return_on_error) + { + $this_page = (!empty($_SERVER['PHP_SELF'])) ? $_SERVER['PHP_SELF'] : $_ENV['PHP_SELF']; + $this_page .= '&' . ((!empty($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : $_ENV['QUERY_STRING']); + + $message = '<u>SQL ERROR</u> [ ' . SQL_LAYER . ' ]<br /><br />' . @sqlite_error_string(@sqlite_last_error($this->db_connect_id)) . '<br /><br /><u>CALLING PAGE</u><br /><br />' . htmlspecialchars($this_page) . (($sql != '') ? '<br /><br /><u>SQL</u><br /><br />' . $sql : '') . '<br />'; + + if ($this->transaction) + { + $this->sql_transaction('rollback'); + } + + trigger_error($message, E_USER_ERROR); + } + + $result = array( + 'message' => @sqlite_error_string(@sqlite_last_error($this->db_connect_id)), + 'code' => @sqlite_last_error($this->db_connect_id) + ); + + return $result; + } + +} // class sql_db + +} // if ... define + +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/functions.php b/src/SemanticScuttle/functions.php new file mode 100644 index 0000000..802a7f1 --- /dev/null +++ b/src/SemanticScuttle/functions.php @@ -0,0 +1,292 @@ +<?php +/** + * Defines some commonly used functions. + * + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +// Converts tags: +// - direction = out: convert spaces to underscores; +// - direction = in: convert underscores to spaces. +function convertTag($tag, $direction = 'out') { + if ($direction == 'out') { + $tag = str_replace(' ', '_', $tag); + } else { + $tag = str_replace('_', ' ', $tag); + } + return $tag; +} + +function filter($data, $type = NULL) { + if (is_string($data)) { + $data = trim($data); + $data = stripslashes($data); + switch ($type) { + case 'url': + $data = rawurlencode($data); + break; + default: + $data = htmlspecialchars($data); + break; + } + } else if (is_array($data)) { + foreach(array_keys($data) as $key) { + $row =& $data[$key]; + $row = filter($row, $type); + } + } + return $data; +} + +/** + * Returns the number of bookmarks that shall be displayed on one page. + * + * @param SemanticScuttle_Model_User $userObject Object of the current user + * + * @return integer Number of bookmarks per page + * + * @uses $defaultPerPage + * @uses $defaultPerPageForAdmins + */ +function getPerPageCount($userObject = null) +{ + global $defaultPerPage, $defaultPerPageForAdmins; + + if (isset($defaultPerPageForAdmins) + && $userObject != null && $userObject->isAdmin() + ) { + return $defaultPerPageForAdmins; + } else { + return $defaultPerPage; + } +} + +function getSortOrder($override = NULL) { + global $defaultOrderBy; + + if (isset($_GET['sort'])) { + return preg_replace('/[^a-z_]/', '', $_GET['sort']); + } else if (isset($override)) { + return $override; + } else { + return $defaultOrderBy; + } +} + +function multi_array_search($needle, $haystack) { + if (is_array($haystack)) { + foreach(array_keys($haystack) as $key) { + $value =& $haystack[$key]; + $result = multi_array_search($needle, $value); + if (is_array($result)) { + $return = $result; + array_unshift($return, $key); + return $return; + } elseif ($result == true) { + $return[] = $key; + return $return; + } + } + return false; + } else { + if ($needle === $haystack) { + return true; + } else { + return false; + } + } +} + +function createURL($page = '', $ending = '') { + global $cleanurls; + if (!$cleanurls && $page != '') { + $page .= '.php'; + } + if(strlen($ending)>0) { + return ROOT . $page .'/'. $ending; + } else { + return ROOT . $page; + } +} + +/** + * Adds the protocol to the URL if it's missing. + * If the current URL is served via HTTPS, https will be used as protocol. + * + * Useful to fix ROOT urls of SemanticScuttle when it's needed, e.g. + * in the bookmarklet or the feed. + * + * @param string $url Url with or without the protocol ("//example.org/foo") + * + * @return string URL with a HTTP protocol + */ +function addProtocolToUrl($url) +{ + if (substr($url, 0, 2) != '//') { + return $url; + } + + if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS']) { + $protocol = 'https:'; + } else { + $protocol = 'http:'; + } + return $protocol . $url; +} +/** + * Creates a "vote for/against this bookmark" URL. + * Also runs htmlspecialchars() on them to prevent XSS. + * + * @param boolean $for For the bookmark (true) or against (false) + * @param integer $bId Bookmark ID + * + * @return string URL to use + */ +function createVoteURL($for, $bId) +{ + return htmlspecialchars( + createURL( + 'vote', + ($for ? 'for' : 'against') . '/' . $bId + ), + ENT_QUOTES + ); +} + +/* Shorten a string like a URL for example by cutting the middle of it */ +function shortenString($string, $maxSize=75) { + $output = ''; + if(strlen($string) > $maxSize) { + $output = substr($string, 0, $maxSize/2).'...'.substr($string, -$maxSize/2); + } else { + $output = $string; + } + return $output; +} + +/* Check url format and check online if the url is a valid page (Not a 404 error for example) */ +function checkUrl($url, $checkOnline = true) { + //check format + if(!preg_match("#(ht|f)tp(s?)\://\S+\.\S+#i",$url)) { + return false; + } + + if($checkOnline) { + //look if the page doesn't return a void or 40X or 50X HTTP code error + $h = @get_headers($url); + if(is_array($h) && strpos($h[0], '40') === false && strpos($h[0], '50') === false) { + return true; + } else { + return false; + } + } else { + return true; + } +} + +/* Returns a concatenated String + * including all the tags from the array $arrayTags (excepted of the $exceptedTag) + * separated by the $separator. + * */ +function aggregateTags($arrayTags, $separator = ' + ', $exceptedTag = '') { + $output = ''; + + for($i = 0; $i<count($arrayTags); $i++) { + if($arrayTags[$i] != $exceptedTag) { + $output.= $arrayTags[$i] . $separator; + } + } + return substr($output, 0, strlen($output) - strlen($separator) ); +} + +function message_die($msg_code, $msg_text = '', $msg_title = '', $err_line = '', $err_file = '', $sql = '', $db = NULL) { + if(defined('HAS_DIED')) + die(T_('message_die() was called multiple times.')); + define('HAS_DIED', 1); + + $sql_store = $sql; + + // Get SQL error if we are debugging. Do this as soon as possible to prevent + // subsequent queries from overwriting the status of sql_error() + if (DEBUG_MODE && ($msg_code == GENERAL_ERROR || $msg_code == CRITICAL_ERROR)) { + $sql_error = is_null($db) ? '' : $db->sql_error(); + $debug_text = ''; + + if ($sql_error['message'] != '') + $debug_text .= '<br /><br />'. T_('SQL Error') .' : '. $sql_error['code'] .' '. $sql_error['message']; + + if ($sql_store != '') + $debug_text .= '<br /><br />'. $sql_store; + + if ($err_line != '' && $err_file != '') + $debug_text .= '</br /><br />'. T_('Line') .' : '. $err_line .'<br />'. T_('File') .' :'. $err_file; + + debug_print_backtrace(); + } + + switch($msg_code) { + case GENERAL_MESSAGE: + if ($msg_title == '') + $msg_title = T_('Information'); + break; + + case CRITICAL_MESSAGE: + if ($msg_title == '') + $msg_title = T_('Critical Information'); + break; + + case GENERAL_ERROR: + if ($msg_text == '') + $msg_text = T_('An error occured'); + + if ($msg_title == '') + $msg_title = T_('General Error'); + break; + + case CRITICAL_ERROR: + // Critical errors mean we cannot rely on _ANY_ DB information being + // available so we're going to dump out a simple echo'd statement + + if ($msg_text == '') + $msg_text = T_('An critical error occured'); + + if ($msg_title == '') + $msg_title = T_('Critical Error'); + break; + } + + // Add on DEBUG_MODE info if we've enabled debug mode and this is an error. This + // prevents debug info being output for general messages should DEBUG_MODE be + // set TRUE by accident (preventing confusion for the end user!) + if (DEBUG_MODE && ($msg_code == GENERAL_ERROR || $msg_code == CRITICAL_ERROR)) { + if ($debug_text != '') + $msg_text = $msg_text . '<br /><br /><strong>'. T_('DEBUG MODE') .'</strong>'. $debug_text; + } + + echo "<html>\n<body>\n". $msg_title ."\n<br /><br />\n". $msg_text ."</body>\n</html>"; + exit; +} + +/** + * Calls reset() on the given arg, without the E_STRICT error + * "Only variables should be passed by reference" + * + * @param array $arg Array to return first element of + * + * @return mixed First element of the array + */ +function rreset($array) +{ + return reset($array); +} +?> diff --git a/src/SemanticScuttle/header-standalone.php b/src/SemanticScuttle/header-standalone.php new file mode 100644 index 0000000..3755ea1 --- /dev/null +++ b/src/SemanticScuttle/header-standalone.php @@ -0,0 +1,19 @@ +<?php +/** + * Load this file instead of header.php if you + * are using it in a standalone non-webserver script. + * + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske <cweiske@cweiske.de> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +$_SERVER['HTTP_HOST'] = 'http://localhost/'; + +require_once dirname(__FILE__) . '/header.php'; +?>
\ No newline at end of file diff --git a/src/SemanticScuttle/header.php b/src/SemanticScuttle/header.php new file mode 100644 index 0000000..1f2f12c --- /dev/null +++ b/src/SemanticScuttle/header.php @@ -0,0 +1,174 @@ +<?php +/** + * Base file that is used by shell scripts and www/www-header.php. + * + * SemanticScuttle - your social bookmark manager. + * + * PHP version 5. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang <mensonge@users.sourceforge.net> + * @author Christian Weiske <cweiske@cweiske.de> + * @author Eric Dane <ericdane@users.sourceforge.net> + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +if ('@data_dir@' == '@' . 'data_dir@') { + //non pear-install + $datadir = dirname(__FILE__) . '/../../data/'; + $wwwdir = dirname(__FILE__) . '/../../www/'; +} else { + //pear installation; files are in include path + $datadir = '@data_dir@/SemanticScuttle/'; + //FIXME: when you have multiple installations, the www_dir will be wrong + $wwwdir = '@www_dir@/SemanticScuttle/'; +} +require_once dirname(__FILE__) . '/Environment.php'; +require_once dirname(__FILE__) . '/Config.php'; + +$cfg = new SemanticScuttle_Config(); +list($configfile, $defaultfile) = $cfg->findFiles(); +if ($defaultfile === null) { + header('HTTP/1.0 500 Internal Server Error'); + echo 'No default configuration file config.default.php found.' + . ' This is really, really strange' + . "\n"; + exit(1); +} +if ($configfile === null) { + header('HTTP/1.0 500 Internal Server Error'); + echo 'Please copy "config.php.dist" to "config.php" in data/ folder.' . "\n"; + exit(1); +} +set_include_path( + get_include_path() . PATH_SEPARATOR + . dirname(__FILE__) . '/../' +); + +// 1 // First requirements part (before debug management) +require_once $defaultfile; +require_once $configfile; + +if (isset($_GET['unittestMode']) && $_GET['unittestMode'] == 1 +) { + if ($allowUnittestMode !== true) { + header('HTTP/1.0 400 Bad Request'); + echo "Unittestmode is not allowed\n"; + exit(2); + } + + define('HTTP_UNIT_TEST_MODE', true); + define('UNIT_TEST_MODE', true); +} +if (defined('UNIT_TEST_MODE')) { + //load configuration for unit tests + $testingConfigFile = $datadir . '/config.testing.php'; + if (file_exists($testingConfigFile)) { + require_once $testingConfigFile; + } + //test-specific configuration file + $unittestConfigFile = $datadir . '/config.testing-tmp.php'; + if (file_exists($unittestConfigFile)) { + require_once $unittestConfigFile; + } + + //make local config vars global - needed for unit tests + //run with phpunit + foreach (get_defined_vars() as $var => $value) { + $GLOBALS[$var] = $value; + } +} + +// some constants are based on variables from config file +require_once 'SemanticScuttle/constants.php'; + + +// Debug Management using constants +if (DEBUG_MODE) { + ini_set('display_errors', '1'); + ini_set('mysql.trace_mode', '1'); + error_reporting(E_ALL); +} else { + ini_set('display_errors', '0'); + ini_set('mysql.trace_mode', '0'); + error_reporting(0); +} + +// 2 // Second requirements part which could display bugs +// (must come after debug management) +require_once 'SemanticScuttle/Service.php'; +require_once 'SemanticScuttle/DbService.php'; +require_once 'SemanticScuttle/Service/Factory.php'; +require_once 'SemanticScuttle/functions.php'; +require_once 'SemanticScuttle/Model/Bookmark.php'; +require_once 'SemanticScuttle/Model/UserArray.php'; +require_once 'SemanticScuttle/Model/User/SslClientCert.php'; + +if (count($GLOBALS['serviceoverrides']) > 0 + && !defined('UNIT_TEST_MODE') +) { + SemanticScuttle_Service_Factory::$serviceoverrides + = $GLOBALS['serviceoverrides']; +} + +// 3 // Third requirements part which import functions from includes/ directory + +// UTF-8 functions +require_once 'SemanticScuttle/utf8.php'; + +// Translation +require_once 'php-gettext/gettext.inc'; +$domain = 'messages'; +T_setlocale(LC_MESSAGES, $locale); +T_bindtextdomain($domain, realpath($datadir . 'locales/')); +T_bind_textdomain_codeset($domain, 'UTF-8'); +T_textdomain($domain); + +// 4 // Session +if (isset($_SERVER['REMOTE_ADDR'])) { + session_start(); + if ($GLOBALS['enableVoting']) { + if (isset($_SESSION['lastUrl'])) { + $GLOBALS['lastUrl'] = $_SESSION['lastUrl']; + } + //this here is hacky, but currently the only way to + // differentiate between css/js php files and normal + // http files + if (!isset($GLOBALS['saveInLastUrl']) + || $GLOBALS['saveInLastUrl'] + ) { + $_SESSION['lastUrl'] = $_SERVER['REQUEST_URI']; + } + } +} + +// 5 // Create mandatory services and objects +$userservice = SemanticScuttle_Service_Factory::get('User'); +$currentUser = $userservice->getCurrentObjectUser(); + +$templateservice = SemanticScuttle_Service_Factory::get('Template'); +$tplVars = array(); +$tplVars['currentUser'] = $currentUser; +$tplVars['userservice'] = $userservice; + +// 6 // Force UTF-8 behaviour for server (cannot be moved into top.inc.php which is not included into every file) +if (!defined('UNIT_TEST_MODE') || defined('HTTP_UNIT_TEST_MODE')) { + //API files define that, so we need a way to support both of them + if (!isset($httpContentType)) { + if (DEBUG_MODE) { + //using that mime type makes all javascript nice in Chromium + // it also serves as test base if the pages really validate + $httpContentType = 'application/xhtml+xml'; + } else { + //until we are sure that all pages validate, we + // keep the non-strict mode on for normal installations + $httpContentType = 'text/html'; + } + } + if ($httpContentType !== false) { + header('Content-Type: ' . $httpContentType . '; charset=utf-8'); + } +} +?> diff --git a/src/SemanticScuttle/utf8.php b/src/SemanticScuttle/utf8.php new file mode 100644 index 0000000..9ef8113 --- /dev/null +++ b/src/SemanticScuttle/utf8.php @@ -0,0 +1,478 @@ +<?php +/** + * UTF8 helper functions + * + * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) + * @author Andreas Gohr <andi@splitbrain.org> + */ + +/** + * URL-Encode a filename to allow unicodecharacters + * + * Slashes are not encoded + * + * When the second parameter is true the string will + * be encoded only if non ASCII characters are detected - + * This makes it safe to run it multiple times on the + * same string (default is true) + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see urlencode + */ +function utf8_encodeFN($file,$safe=true){ + if($safe && preg_match('#^[a-zA-Z0-9/_\-.%]+$#',$file)){ + return $file; + } + $file = urlencode($file); + $file = str_replace('%2F','/',$file); + return $file; +} + +/** + * URL-Decode a filename + * + * This is just a wrapper around urldecode + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see urldecode + */ +function utf8_decodeFN($file){ + $file = urldecode($file); + return $file; +} + +/** + * Checks if a string contains 7bit ASCII only + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +function utf8_isASCII($str){ + for($i=0; $i<strlen($str); $i++){ + if(ord($str{$i}) >127) return false; + } + return true; +} + +/** + * Tries to detect if a string is in Unicode encoding + * + * @author <bmorel@ssi.fr> + * @link http://www.php.net/manual/en/function.utf8-encode.php + */ +function utf8_check($Str) { + for ($i=0; $i<strlen($Str); $i++) { + if (ord($Str[$i]) < 0x80) continue; # 0bbbbbbb + elseif ((ord($Str[$i]) & 0xE0) == 0xC0) $n=1; # 110bbbbb + elseif ((ord($Str[$i]) & 0xF0) == 0xE0) $n=2; # 1110bbbb + elseif ((ord($Str[$i]) & 0xF8) == 0xF0) $n=3; # 11110bbb + elseif ((ord($Str[$i]) & 0xFC) == 0xF8) $n=4; # 111110bb + elseif ((ord($Str[$i]) & 0xFE) == 0xFC) $n=5; # 1111110b + else return false; # Does not match any model + for ($j=0; $j<$n; $j++) { # n bytes matching 10bbbbbb follow ? + if ((++$i == strlen($Str)) || ((ord($Str[$i]) & 0xC0) != 0x80)) + return false; + } + } + return true; +} + +/** + * This is a unicode aware replacement for strlen() + * + * Uses mb_string extension if available + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see strlen() + */ +function utf8_strlen($string){ + if(!defined('UTF8_NOMBSTRING') && function_exists('mb_strlen')) + return mb_strlen($string,'utf-8'); + + $uni = utf8_to_unicode($string); + return count($uni); +} + +/** + * This is a unicode aware replacement for substr() + * + * Uses mb_string extension if available + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see substr() + */ +function utf8_substr($str, $start, $length=null){ + if(!defined('UTF8_NOMBSTRING') && function_exists('mb_substr')) + return mb_substr($str,$start,$length,'utf-8'); + + $uni = utf8_to_unicode($str); + return unicode_to_utf8(array_slice($uni,$start,$length)); +} + +/** + * This is a unicode aware replacement for strtolower() + * + * Uses mb_string extension if available + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see strtolower() + * @see utf8_strtoupper() + */ +function utf8_strtolower($string){ + if(!defined('UTF8_NOMBSTRING') && function_exists('mb_strtolower')) + return mb_strtolower($string,'utf-8'); + + global $UTF8_UPPER_TO_LOWER; + $uni = utf8_to_unicode($string); + for ($i=0; $i < count($uni); $i++){ + if($UTF8_UPPER_TO_LOWER[$uni[$i]]){ + $uni[$i] = $UTF8_UPPER_TO_LOWER[$uni[$i]]; + } + } + return unicode_to_utf8($uni); +} + +/** + * This is a unicode aware replacement for strtoupper() + * + * Uses mb_string extension if available + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see strtoupper() + * @see utf8_strtoupper() + */ +function utf8_strtoupper($string){ + if(!defined('UTF8_NOMBSTRING') && function_exists('mb_strtolower')) + return mb_strtolower($string,'utf-8'); + + global $UTF8_LOWER_TO_UPPER; + $uni = utf8_to_unicode($string); + for ($i=0; $i < count($uni); $i++){ + if($UTF8_LOWER_TO_UPPER[$uni[$i]]){ + $uni[$i] = $UTF8_LOWER_TO_UPPER[$uni[$i]]; + } + } + return unicode_to_utf8($uni); +} + +/** + * Replace accented UTF-8 characters by unaccented ASCII-7 equivalents + * + * Use the optional parameter to just deaccent lower ($case = -1) or upper ($case = 1) + * letters. Default is to deaccent both cases ($case = 0) + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +function utf8_deaccent($string,$case=0){ + if($case <= 0){ + global $UTF8_LOWER_ACCENTS; + $string = str_replace(array_keys($UTF8_LOWER_ACCENTS),array_values($UTF8_LOWER_ACCENTS),$string); + } + if($case >= 0){ + global $UTF8_UPPER_ACCENTS; + $string = str_replace(array_keys($UTF8_UPPER_ACCENTS),array_values($UTF8_UPPER_ACCENTS),$string); + } + return $string; +} + +/** + * Removes special characters (nonalphanumeric) from a UTF-8 string + * + * Be sure to specify all specialchars you give in $repl in $keep, too + * or it won't work. + * + * This function adds the controlchars 0x00 to 0x19 to the array of + * stripped chars (they are not included in $UTF8_SPECIAL_CHARS) + * + * @author Andreas Gohr <andi@splitbrain.org> + * @param string $string The UTF8 string to strip of special chars + * @param string $repl Replace special with this string + * @param string $keep Special chars to keep (in UTF8) + */ +function utf8_stripspecials($string,$repl='',$keep=''){ + global $UTF8_SPECIAL_CHARS; + if($keep != ''){ + $specials = array_diff($UTF8_SPECIAL_CHARS, utf8_to_unicode($keep)); + }else{ + $specials = $UTF8_SPECIAL_CHARS; + } + + $specials = unicode_to_utf8($specials); + $specials = preg_quote($specials, '/'); + + return preg_replace('/[\x00-\x19'.$specials.']/u',$repl,$string); +} + +/** + * This is an Unicode aware replacement for strpos + * + * Uses mb_string extension if available + * + * @author Scott Michael Reynen <scott@randomchaos.com> + * @author Andreas Gohr <andi@splitbrain.org> + * @link http://www.randomchaos.com/document.php?source=php_and_unicode + * @see strpos() + */ +function utf8_strpos($haystack, $needle,$offset=0) { + if(!defined('UTF8_NOMBSTRING') && function_exists('mb_strpos')) + return mb_strpos($haystack,$needle,$offset,'utf-8'); + + $haystack = utf8_to_unicode($haystack); + $needle = utf8_to_unicode($needle); + $position = $offset; + $found = false; + + while( (! $found ) && ( $position < count( $haystack ) ) ) { + if ( $needle[0] == $haystack[$position] ) { + for ($i = 1; $i < count( $needle ); $i++ ) { + if ( $needle[$i] != $haystack[ $position + $i ] ) break; + } + if ( $i == count( $needle ) ) { + $found = true; + $position--; + } + } + $position++; + } + return ( $found == true ) ? $position : false; +} + +/** + * This function will any UTF-8 encoded text and return it as + * a list of Unicode values: + * + * @author Scott Michael Reynen <scott@randomchaos.com> + * @link http://www.randomchaos.com/document.php?source=php_and_unicode + * @see unicode_to_utf8() + */ +function utf8_to_unicode( $str ) { + $unicode = array(); + $values = array(); + $lookingFor = 1; + + for ($i = 0; $i < strlen( $str ); $i++ ) { + $thisValue = ord( $str[ $i ] ); + if ( $thisValue < 128 ) $unicode[] = $thisValue; + else { + if ( count( $values ) == 0 ) $lookingFor = ( $thisValue < 224 ) ? 2 : 3; + $values[] = $thisValue; + if ( count( $values ) == $lookingFor ) { + $number = ( $lookingFor == 3 ) ? + ( ( $values[0] % 16 ) * 4096 ) + ( ( $values[1] % 64 ) * 64 ) + ( $values[2] % 64 ): + ( ( $values[0] % 32 ) * 64 ) + ( $values[1] % 64 ); + $unicode[] = $number; + $values = array(); + $lookingFor = 1; + } + } + } + return $unicode; +} + +/** + * This function will convert a Unicode array back to its UTF-8 representation + * + * @author Scott Michael Reynen <scott@randomchaos.com> + * @link http://www.randomchaos.com/document.php?source=php_and_unicode + * @see utf8_to_unicode() + */ +function unicode_to_utf8( $str ) { + $utf8 = ''; + foreach( $str as $unicode ) { + if ( $unicode < 128 ) { + $utf8.= chr( $unicode ); + } elseif ( $unicode < 2048 ) { + $utf8.= chr( 192 + ( ( $unicode - ( $unicode % 64 ) ) / 64 ) ); + $utf8.= chr( 128 + ( $unicode % 64 ) ); + } else { + $utf8.= chr( 224 + ( ( $unicode - ( $unicode % 4096 ) ) / 4096 ) ); + $utf8.= chr( 128 + ( ( ( $unicode % 4096 ) - ( $unicode % 64 ) ) / 64 ) ); + $utf8.= chr( 128 + ( $unicode % 64 ) ); + } + } + return $utf8; +} + +/** + * UTF-8 Case lookup table + * + * This lookuptable defines the upper case letters to their correspponding + * lower case letter in UTF-8 + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +$UTF8_LOWER_TO_UPPER = array( + 0x0061=>0x0041, 0x03C6=>0x03A6, 0x0163=>0x0162, 0x00E5=>0x00C5, 0x0062=>0x0042, + 0x013A=>0x0139, 0x00E1=>0x00C1, 0x0142=>0x0141, 0x03CD=>0x038E, 0x0101=>0x0100, + 0x0491=>0x0490, 0x03B4=>0x0394, 0x015B=>0x015A, 0x0064=>0x0044, 0x03B3=>0x0393, + 0x00F4=>0x00D4, 0x044A=>0x042A, 0x0439=>0x0419, 0x0113=>0x0112, 0x043C=>0x041C, + 0x015F=>0x015E, 0x0144=>0x0143, 0x00EE=>0x00CE, 0x045E=>0x040E, 0x044F=>0x042F, + 0x03BA=>0x039A, 0x0155=>0x0154, 0x0069=>0x0049, 0x0073=>0x0053, 0x1E1F=>0x1E1E, + 0x0135=>0x0134, 0x0447=>0x0427, 0x03C0=>0x03A0, 0x0438=>0x0418, 0x00F3=>0x00D3, + 0x0440=>0x0420, 0x0454=>0x0404, 0x0435=>0x0415, 0x0449=>0x0429, 0x014B=>0x014A, + 0x0431=>0x0411, 0x0459=>0x0409, 0x1E03=>0x1E02, 0x00F6=>0x00D6, 0x00F9=>0x00D9, + 0x006E=>0x004E, 0x0451=>0x0401, 0x03C4=>0x03A4, 0x0443=>0x0423, 0x015D=>0x015C, + 0x0453=>0x0403, 0x03C8=>0x03A8, 0x0159=>0x0158, 0x0067=>0x0047, 0x00E4=>0x00C4, + 0x03AC=>0x0386, 0x03AE=>0x0389, 0x0167=>0x0166, 0x03BE=>0x039E, 0x0165=>0x0164, + 0x0117=>0x0116, 0x0109=>0x0108, 0x0076=>0x0056, 0x00FE=>0x00DE, 0x0157=>0x0156, + 0x00FA=>0x00DA, 0x1E61=>0x1E60, 0x1E83=>0x1E82, 0x00E2=>0x00C2, 0x0119=>0x0118, + 0x0146=>0x0145, 0x0070=>0x0050, 0x0151=>0x0150, 0x044E=>0x042E, 0x0129=>0x0128, + 0x03C7=>0x03A7, 0x013E=>0x013D, 0x0442=>0x0422, 0x007A=>0x005A, 0x0448=>0x0428, + 0x03C1=>0x03A1, 0x1E81=>0x1E80, 0x016D=>0x016C, 0x00F5=>0x00D5, 0x0075=>0x0055, + 0x0177=>0x0176, 0x00FC=>0x00DC, 0x1E57=>0x1E56, 0x03C3=>0x03A3, 0x043A=>0x041A, + 0x006D=>0x004D, 0x016B=>0x016A, 0x0171=>0x0170, 0x0444=>0x0424, 0x00EC=>0x00CC, + 0x0169=>0x0168, 0x03BF=>0x039F, 0x006B=>0x004B, 0x00F2=>0x00D2, 0x00E0=>0x00C0, + 0x0434=>0x0414, 0x03C9=>0x03A9, 0x1E6B=>0x1E6A, 0x00E3=>0x00C3, 0x044D=>0x042D, + 0x0436=>0x0416, 0x01A1=>0x01A0, 0x010D=>0x010C, 0x011D=>0x011C, 0x00F0=>0x00D0, + 0x013C=>0x013B, 0x045F=>0x040F, 0x045A=>0x040A, 0x00E8=>0x00C8, 0x03C5=>0x03A5, + 0x0066=>0x0046, 0x00FD=>0x00DD, 0x0063=>0x0043, 0x021B=>0x021A, 0x00EA=>0x00CA, + 0x03B9=>0x0399, 0x017A=>0x0179, 0x00EF=>0x00CF, 0x01B0=>0x01AF, 0x0065=>0x0045, + 0x03BB=>0x039B, 0x03B8=>0x0398, 0x03BC=>0x039C, 0x045C=>0x040C, 0x043F=>0x041F, + 0x044C=>0x042C, 0x00FE=>0x00DE, 0x00F0=>0x00D0, 0x1EF3=>0x1EF2, 0x0068=>0x0048, + 0x00EB=>0x00CB, 0x0111=>0x0110, 0x0433=>0x0413, 0x012F=>0x012E, 0x00E6=>0x00C6, + 0x0078=>0x0058, 0x0161=>0x0160, 0x016F=>0x016E, 0x03B1=>0x0391, 0x0457=>0x0407, + 0x0173=>0x0172, 0x00FF=>0x0178, 0x006F=>0x004F, 0x043B=>0x041B, 0x03B5=>0x0395, + 0x0445=>0x0425, 0x0121=>0x0120, 0x017E=>0x017D, 0x017C=>0x017B, 0x03B6=>0x0396, + 0x03B2=>0x0392, 0x03AD=>0x0388, 0x1E85=>0x1E84, 0x0175=>0x0174, 0x0071=>0x0051, + 0x0437=>0x0417, 0x1E0B=>0x1E0A, 0x0148=>0x0147, 0x0105=>0x0104, 0x0458=>0x0408, + 0x014D=>0x014C, 0x00ED=>0x00CD, 0x0079=>0x0059, 0x010B=>0x010A, 0x03CE=>0x038F, + 0x0072=>0x0052, 0x0430=>0x0410, 0x0455=>0x0405, 0x0452=>0x0402, 0x0127=>0x0126, + 0x0137=>0x0136, 0x012B=>0x012A, 0x03AF=>0x038A, 0x044B=>0x042B, 0x006C=>0x004C, + 0x03B7=>0x0397, 0x0125=>0x0124, 0x0219=>0x0218, 0x00FB=>0x00DB, 0x011F=>0x011E, + 0x043E=>0x041E, 0x1E41=>0x1E40, 0x03BD=>0x039D, 0x0107=>0x0106, 0x03CB=>0x03AB, + 0x0446=>0x0426, 0x00FE=>0x00DE, 0x00E7=>0x00C7, 0x03CA=>0x03AA, 0x0441=>0x0421, + 0x0432=>0x0412, 0x010F=>0x010E, 0x00F8=>0x00D8, 0x0077=>0x0057, 0x011B=>0x011A, + 0x0074=>0x0054, 0x006A=>0x004A, 0x045B=>0x040B, 0x0456=>0x0406, 0x0103=>0x0102, + 0x03BB=>0x039B, 0x00F1=>0x00D1, 0x043D=>0x041D, 0x03CC=>0x038C, 0x00E9=>0x00C9, + 0x00F0=>0x00D0, 0x0457=>0x0407, 0x0123=>0x0122, +); + +/** + * UTF-8 Case lookup table + * + * This lookuptable defines the lower case letters to their correspponding + * upper case letter in UTF-8 (it does so by flipping $UTF8_LOWER_TO_UPPER) + * + * @author Andreas Gohr <andi@splitbrain.org> + */ +$UTF8_UPPER_TO_LOWER = @array_flip($UTF8_LOWER_TO_UPPER); + +/** + * UTF-8 lookup table for lower case accented letters + * + * This lookuptable defines replacements for accented characters from the ASCII-7 + * range. This are lower case letters only. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see utf8_deaccent() + */ +$UTF8_LOWER_ACCENTS = array( + 'à ' => 'a', 'ô' => 'o', 'Ä' => 'd', 'ḟ' => 'f', 'ë' => 'e', 'Å¡' => 's', 'Æ¡' => 'o', + 'ß' => 'ss', 'ă' => 'a', 'Å™' => 'r', 'È›' => 't', 'ň' => 'n', 'Ä' => 'a', 'Ä·' => 'k', + 'Å' => 's', 'ỳ' => 'y', 'ņ' => 'n', 'ĺ' => 'l', 'ħ' => 'h', 'á¹—' => 'p', 'ó' => 'o', + 'ú' => 'u', 'Ä›' => 'e', 'é' => 'e', 'ç' => 'c', 'áº' => 'w', 'Ä‹' => 'c', 'õ' => 'o', + 'ṡ' => 's', 'ø' => 'o', 'Ä£' => 'g', 'ŧ' => 't', 'È™' => 's', 'Ä—' => 'e', 'ĉ' => 'c', + 'Å›' => 's', 'î' => 'i', 'ű' => 'u', 'ć' => 'c', 'Ä™' => 'e', 'ŵ' => 'w', 'ṫ' => 't', + 'Å«' => 'u', 'Ä' => 'c', 'ö' => 'oe', 'è' => 'e', 'Å·' => 'y', 'Ä…' => 'a', 'Å‚' => 'l', + 'ų' => 'u', 'ů' => 'u', 'ÅŸ' => 's', 'ÄŸ' => 'g', 'ļ' => 'l', 'Æ’' => 'f', 'ž' => 'z', + 'ẃ' => 'w', 'ḃ' => 'b', 'Ã¥' => 'a', 'ì' => 'i', 'ï' => 'i', 'ḋ' => 'd', 'Å¥' => 't', + 'Å—' => 'r', 'ä' => 'ae', 'Ã' => 'i', 'Å•' => 'r', 'ê' => 'e', 'ü' => 'ue', 'ò' => 'o', + 'Ä“' => 'e', 'ñ' => 'n', 'Å„' => 'n', 'Ä¥' => 'h', 'Ä' => 'g', 'Ä‘' => 'd', 'ĵ' => 'j', + 'ÿ' => 'y', 'Å©' => 'u', 'Å' => 'u', 'Æ°' => 'u', 'Å£' => 't', 'ý' => 'y', 'Å‘' => 'o', + 'â' => 'a', 'ľ' => 'l', 'ẅ' => 'w', 'ż' => 'z', 'Ä«' => 'i', 'ã' => 'a', 'Ä¡' => 'g', + 'á¹' => 'm', 'Å' => 'o', 'Ä©' => 'i', 'ù' => 'u', 'į' => 'i', 'ź' => 'z', 'á' => 'a', + 'û' => 'u', 'þ' => 'th', 'ð' => 'dh', 'æ' => 'ae', 'µ' => 'u', +); + +/** + * UTF-8 lookup table for upper case accented letters + * + * This lookuptable defines replacements for accented characters from the ASCII-7 + * range. This are upper case letters only. + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see utf8_deaccent() + */ +$UTF8_UPPER_ACCENTS = array( + 'à ' => 'A', 'ô' => 'O', 'Ä' => 'D', 'ḟ' => 'F', 'ë' => 'E', 'Å¡' => 'S', 'Æ¡' => 'O', + 'ß' => 'Ss', 'ă' => 'A', 'Å™' => 'R', 'È›' => 'T', 'ň' => 'N', 'Ä' => 'A', 'Ä·' => 'K', + 'Å' => 'S', 'ỳ' => 'Y', 'ņ' => 'N', 'ĺ' => 'L', 'ħ' => 'H', 'á¹—' => 'P', 'ó' => 'O', + 'ú' => 'U', 'Ä›' => 'E', 'é' => 'E', 'ç' => 'C', 'áº' => 'W', 'Ä‹' => 'C', 'õ' => 'O', + 'ṡ' => 'S', 'ø' => 'O', 'Ä£' => 'G', 'ŧ' => 'T', 'È™' => 'S', 'Ä—' => 'E', 'ĉ' => 'C', + 'Å›' => 'S', 'î' => 'I', 'ű' => 'U', 'ć' => 'C', 'Ä™' => 'E', 'ŵ' => 'W', 'ṫ' => 'T', + 'Å«' => 'U', 'Ä' => 'C', 'ö' => 'Oe', 'è' => 'E', 'Å·' => 'Y', 'Ä…' => 'A', 'Å‚' => 'L', + 'ų' => 'U', 'ů' => 'U', 'ÅŸ' => 'S', 'ÄŸ' => 'G', 'ļ' => 'L', 'Æ’' => 'F', 'ž' => 'Z', + 'ẃ' => 'W', 'ḃ' => 'B', 'Ã¥' => 'A', 'ì' => 'I', 'ï' => 'I', 'ḋ' => 'D', 'Å¥' => 'T', + 'Å—' => 'R', 'ä' => 'Ae', 'Ã' => 'I', 'Å•' => 'R', 'ê' => 'E', 'ü' => 'Ue', 'ò' => 'O', + 'Ä“' => 'E', 'ñ' => 'N', 'Å„' => 'N', 'Ä¥' => 'H', 'Ä' => 'G', 'Ä‘' => 'D', 'ĵ' => 'J', + 'ÿ' => 'Y', 'Å©' => 'U', 'Å' => 'U', 'Æ°' => 'U', 'Å£' => 'T', 'ý' => 'Y', 'Å‘' => 'O', + 'â' => 'A', 'ľ' => 'L', 'ẅ' => 'W', 'ż' => 'Z', 'Ä«' => 'I', 'ã' => 'A', 'Ä¡' => 'G', + 'á¹' => 'M', 'Å' => 'O', 'Ä©' => 'I', 'ù' => 'U', 'į' => 'I', 'ź' => 'Z', 'á' => 'A', + 'û' => 'U', 'Þ' => 'Th', 'Ã' => 'Dh', 'Æ' => 'Ae', +); + +/** + * UTF-8 array of common special characters + * + * This array should contain all special characters (not a letter or digit) + * defined in the various local charsets - it's not a complete list of non-alphanum + * characters in UTF-8. It's not perfect but should match most cases of special + * chars. + * + * The controlchars 0x00 to 0x19 are _not_ included in this array. The space 0x20 is! + * + * @author Andreas Gohr <andi@splitbrain.org> + * @see utf8_stripspecials() + */ +$UTF8_SPECIAL_CHARS = array( + 0x001a, 0x001b, 0x001c, 0x001d, 0x001e, 0x001f, 0x0020, 0x0021, 0x0022, 0x0023, + 0x0024, 0x0025, 0x0026, 0x0027, 0x0028, 0x0029, 0x002a, 0x002b, 0x002c, 0x002d, + 0x002e, 0x002f, 0x003a, 0x003b, 0x003c, 0x003d, 0x003e, 0x003f, 0x0040, 0x005b, + 0x005c, 0x005d, 0x005e, 0x005f, 0x0060, 0x0142, 0x007b, 0x007c, 0x007d, 0x007e, + 0x007f, 0x0080, 0x0081, 0x0082, 0x0083, 0x0084, 0x0085, 0x0086, 0x0087, 0x0088, + 0x0089, 0x008a, 0x008b, 0x008c, 0x008d, 0x008e, 0x008f, 0x0090, 0x0091, 0x0092, + 0x0093, 0x0094, 0x0095, 0x0096, 0x0097, 0x0098, 0x0099, 0x009a, 0x009b, 0x009c, + 0x009d, 0x009e, 0x009f, 0x00a0, 0x00a1, 0x00a2, 0x00a3, 0x00a4, 0x00a5, 0x00a6, + 0x00a7, 0x00a8, 0x00a9, 0x00aa, 0x00ab, 0x00ac, 0x00ad, 0x00ae, 0x00af, 0x00b0, + 0x00b1, 0x00b2, 0x00b3, 0x00b4, 0x00b5, 0x00b6, 0x00b7, 0x00b8, 0x00b9, 0x00ba, + 0x00bb, 0x00bc, 0x00bd, 0x00be, 0x00bf, 0x00d7, 0x00f7, 0x02c7, 0x02d8, 0x02d9, + 0x02da, 0x02db, 0x02dc, 0x02dd, 0x0300, 0x0301, 0x0303, 0x0309, 0x0323, 0x0384, + 0x0385, 0x0387, 0x03b2, 0x03c6, 0x03d1, 0x03d2, 0x03d5, 0x03d6, 0x05b0, 0x05b1, + 0x05b2, 0x05b3, 0x05b4, 0x05b5, 0x05b6, 0x05b7, 0x05b8, 0x05b9, 0x05bb, 0x05bc, + 0x05bd, 0x05be, 0x05bf, 0x05c0, 0x05c1, 0x05c2, 0x05c3, 0x05f3, 0x05f4, 0x060c, + 0x061b, 0x061f, 0x0640, 0x064b, 0x064c, 0x064d, 0x064e, 0x064f, 0x0650, 0x0651, + 0x0652, 0x066a, 0x0e3f, 0x200c, 0x200d, 0x200e, 0x200f, 0x2013, 0x2014, 0x2015, + 0x2017, 0x2018, 0x2019, 0x201a, 0x201c, 0x201d, 0x201e, 0x2020, 0x2021, 0x2022, + 0x2026, 0x2030, 0x2032, 0x2033, 0x2039, 0x203a, 0x2044, 0x20a7, 0x20aa, 0x20ab, + 0x20ac, 0x2116, 0x2118, 0x2122, 0x2126, 0x2135, 0x2190, 0x2191, 0x2192, 0x2193, + 0x2194, 0x2195, 0x21b5, 0x21d0, 0x21d1, 0x21d2, 0x21d3, 0x21d4, 0x2200, 0x2202, + 0x2203, 0x2205, 0x2206, 0x2207, 0x2208, 0x2209, 0x220b, 0x220f, 0x2211, 0x2212, + 0x2215, 0x2217, 0x2219, 0x221a, 0x221d, 0x221e, 0x2220, 0x2227, 0x2228, 0x2229, + 0x222a, 0x222b, 0x2234, 0x223c, 0x2245, 0x2248, 0x2260, 0x2261, 0x2264, 0x2265, + 0x2282, 0x2283, 0x2284, 0x2286, 0x2287, 0x2295, 0x2297, 0x22a5, 0x22c5, 0x2310, + 0x2320, 0x2321, 0x2329, 0x232a, 0x2469, 0x2500, 0x2502, 0x250c, 0x2510, 0x2514, + 0x2518, 0x251c, 0x2524, 0x252c, 0x2534, 0x253c, 0x2550, 0x2551, 0x2552, 0x2553, + 0x2554, 0x2555, 0x2556, 0x2557, 0x2558, 0x2559, 0x255a, 0x255b, 0x255c, 0x255d, + 0x255e, 0x255f, 0x2560, 0x2561, 0x2562, 0x2563, 0x2564, 0x2565, 0x2566, 0x2567, + 0x2568, 0x2569, 0x256a, 0x256b, 0x256c, 0x2580, 0x2584, 0x2588, 0x258c, 0x2590, + 0x2591, 0x2592, 0x2593, 0x25a0, 0x25b2, 0x25bc, 0x25c6, 0x25ca, 0x25cf, 0x25d7, + 0x2605, 0x260e, 0x261b, 0x261e, 0x2660, 0x2663, 0x2665, 0x2666, 0x2701, 0x2702, + 0x2703, 0x2704, 0x2706, 0x2707, 0x2708, 0x2709, 0x270c, 0x270d, 0x270e, 0x270f, + 0x2710, 0x2711, 0x2712, 0x2713, 0x2714, 0x2715, 0x2716, 0x2717, 0x2718, 0x2719, + 0x271a, 0x271b, 0x271c, 0x271d, 0x271e, 0x271f, 0x2720, 0x2721, 0x2722, 0x2723, + 0x2724, 0x2725, 0x2726, 0x2727, 0x2729, 0x272a, 0x272b, 0x272c, 0x272d, 0x272e, + 0x272f, 0x2730, 0x2731, 0x2732, 0x2733, 0x2734, 0x2735, 0x2736, 0x2737, 0x2738, + 0x2739, 0x273a, 0x273b, 0x273c, 0x273d, 0x273e, 0x273f, 0x2740, 0x2741, 0x2742, + 0x2743, 0x2744, 0x2745, 0x2746, 0x2747, 0x2748, 0x2749, 0x274a, 0x274b, 0x274d, + 0x274f, 0x2750, 0x2751, 0x2752, 0x2756, 0x2758, 0x2759, 0x275a, 0x275b, 0x275c, + 0x275d, 0x275e, 0x2761, 0x2762, 0x2763, 0x2764, 0x2765, 0x2766, 0x2767, 0x277f, + 0x2789, 0x2793, 0x2794, 0x2798, 0x2799, 0x279a, 0x279b, 0x279c, 0x279d, 0x279e, + 0x279f, 0x27a0, 0x27a1, 0x27a2, 0x27a3, 0x27a4, 0x27a5, 0x27a6, 0x27a7, 0x27a8, + 0x27a9, 0x27aa, 0x27ab, 0x27ac, 0x27ad, 0x27ae, 0x27af, 0x27b1, 0x27b2, 0x27b3, + 0x27b4, 0x27b5, 0x27b6, 0x27b7, 0x27b8, 0x27b9, 0x27ba, 0x27bb, 0x27bc, 0x27bd, + 0x27be, 0xf6d9, 0xf6da, 0xf6db, 0xf8d7, 0xf8d8, 0xf8d9, 0xf8da, 0xf8db, 0xf8dc, + 0xf8dd, 0xf8de, 0xf8df, 0xf8e0, 0xf8e1, 0xf8e2, 0xf8e3, 0xf8e4, 0xf8e5, 0xf8e6, + 0xf8e7, 0xf8e8, 0xf8e9, 0xf8ea, 0xf8eb, 0xf8ec, 0xf8ed, 0xf8ee, 0xf8ef, 0xf8f0, + 0xf8f1, 0xf8f2, 0xf8f3, 0xf8f4, 0xf8f5, 0xf8f6, 0xf8f7, 0xf8f8, 0xf8f9, 0xf8fa, + 0xf8fb, 0xf8fc, 0xf8fd, 0xf8fe, 0xfe7c, 0xfe7d, +); +?> diff --git a/src/php-gettext/AUTHORS b/src/php-gettext/AUTHORS new file mode 100644 index 0000000..da6ade7 --- /dev/null +++ b/src/php-gettext/AUTHORS @@ -0,0 +1,3 @@ +Danilo Segan <danilo@kvota.net> +Nico Kaiser <nico@siriux.net> (contributed most changes between 1.0.2 and 1.0.3, bugfix for 1.0.5) +Steven Armstrong <sa@c-area.ch> (gettext.inc, leading to 1.0.6) diff --git a/src/php-gettext/COPYING b/src/php-gettext/COPYING new file mode 100644 index 0000000..5b6e7c6 --- /dev/null +++ b/src/php-gettext/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/src/php-gettext/ChangeLog b/src/php-gettext/ChangeLog new file mode 100644 index 0000000..31e782e --- /dev/null +++ b/src/php-gettext/ChangeLog @@ -0,0 +1,152 @@ +---- semanticscuttle-specific ----- +2009-11-03 Christian Weiske <cweiske@cweiske.de> + * fix magic detection on 64 bit systems, apply patch from: + http://source.ibiblio.org/trac/lyceum/attachment/ticket/652/gettext-64bit-fix.diff + + +---- version 1.07 ----- + +2006-02-07 Danilo Å egan <danilo@gnome.org> + + * examples/pigs_dropin.php: comment-out bind_textdomain_codeset + + * gettext.inc (T_bind_textdomain_codeset): bind_textdomain_codeset + is available only in PHP 4.2.0+ (thanks to Jens A. Tkotz). + + * Makefile: Include gettext.inc in DIST_FILES, VERSION up to + 1.0.7. + +2006-02-03 Danilo Å egan <danilo@gnome.org> + + Added setlocale() emulation as well. + + * examples/pigs_dropin.php: Use T_setlocale() and locale_emulation(). + * examples/pigs_fallback.php: Use T_setlocale() and locale_emulation(). + + * gettext.inc: Added globals $EMULATEGETTEXT and $CURRENTLOCALE. + (locale_emulation): Whether emulation is active. + (_check_locale): Rewrite. + (_setlocale): Added emulated setlocale function. + (T_setlocale): Wrapper around _setlocale. + (_get_reader): Use variables and _setlocale. + +2006-02-02 Danilo Å egan <danilo@gnome.org> + + Fix bug #12192. + + * examples/locale/sr_CS/LC_MESSAGES/messages.po: Correct grammar. + * examples/locale/sr_CS/LC_MESSAGES/messages.mo: Rebuild. + +2006-02-02 Danilo Å egan <danilo@gnome.org> + + Fix bug #15419. + + * streams.php: Support for PHP 5.1.1 fread() which reads most 8kb. + (Fix by Piotr Szotkowski <shot@hot.pl>) + +2006-02-02 Danilo Å egan <danilo@gnome.org> + + Merge Steven Armstrong's changes, supporting standard gettext + interfaces: + + * examples/*: Restructured examples. + * gettext.inc: Added. + * AUTHORS: Added Steven. + * Makefile (VERSION): Up to 1.0.6. + +2006-01-28 Nico Kaiser <nico@siriux.net> + + * gettext.php (select_string): Fix "true" <-> 1 difference of PHP + +2005-07-29 Danilo Å egan <danilo@gnome.org> + + * Makefile (VERSION): Up to 1.0.5. + +2005-07-29 Danilo Å egan <danilo@gnome.org> + + Fixes bug #13850. + + * gettext.php (gettext_reader): check $Reader->error as well. + +2005-07-29 Danilo Å egan <danilo@gnome.org> + + * Makefile (VERSION): Up to 1.0.4. + +2005-07-29 Danilo Å egan <danilo@gnome.org> + + Fixes bug #13771. + + * gettext.php (gettext_reader->get_plural_forms): Plural forms + header extraction regex change. Reported by Edgar Gonzales. + +2005-02-28 Danilo Å egan <dsegan@gmx.net> + + * AUTHORS: Added Nico to the list. + + * Makefile (VERSION): Up to 1.0.3. + + * README: Updated. + +2005-02-28 Danilo Å egan <dsegan@gmx.net> + + * gettext.php: Added pre-loading, code documentation, and many + code clean-ups by Nico Kaiser <nico@siriux.net>. + +2005-02-28 Danilo Å egan <dsegan@gmx.net> + + * streams.php (FileReader.read): Handle read($bytes = 0). + + * examples/pigs.php: Prefix gettext function names with T or T_. + + * examples/update: Use the same keywords T_ and T_ngettext. + + * streams.php: Added CachedFileReader. + +2003-11-11 Danilo Å egan <dsegan@gmx.net> + + * gettext.php: Added hashing to find_string. + +2003-11-01 Danilo Å egan <dsegan@gmx.net> + + * Makefile (DIST_FILES): Replaced LICENSE with COPYING. + (VERSION): Up to 1.0.2. + + * AUTHORS: Minor edits. + + * README: Minor edits. + + * COPYING: Removed LICENSE, added this file. + + * gettext.php: Added copyright notice and disclaimer. + * streams.php: Same. + * examples/pigs.php: Same. + +2003-10-23 Danilo Å egan <dsegan@gmx.net> + + * Makefile: Upped version to 1.0.1. + + * gettext.php (gettext_reader): Remove a call to set_total_plurals. + (set_total_plurals): Removed unused function for some better days. + +2003-10-23 Danilo Å egan <dsegan@gmx.net> + + * Makefile: Added, version 1.0.0. + + * examples/*: Added an example of usage. + + * README: Described all the crap. + +2003-10-22 Danilo Å egan <dsegan@gmx.net> + + * gettext.php: Plural forms implemented too. + + * streams.php: Added FileReader for direct access to files (no + need to keep file in memory). + + * gettext.php: It works, except for plural forms. + + * streams.php: Created abstract class StreamReader. + Added StringReader class. + + * gettext.php: Started writing gettext_reader. + diff --git a/src/php-gettext/Makefile b/src/php-gettext/Makefile new file mode 100644 index 0000000..a6cce12 --- /dev/null +++ b/src/php-gettext/Makefile @@ -0,0 +1,38 @@ +PACKAGE = php-gettext-$(VERSION) +VERSION = 1.0.10 + +DIST_FILES = \ + gettext.php \ + gettext.inc \ + streams.php \ + AUTHORS \ + README \ + COPYING \ + Makefile \ + examples/index.php \ + examples/pigs_dropin.php \ + examples/pigs_fallback.php \ + examples/locale/sr_CS/LC_MESSAGES/messages.po \ + examples/locale/sr_CS/LC_MESSAGES/messages.mo \ + examples/locale/de_CH/LC_MESSAGES/messages.po \ + examples/locale/de_CH/LC_MESSAGES/messages.mo \ + examples/update \ + tests/LocalesTest.php \ + tests/ParsingTest.php + +check: + phpunit --verbose tests + +dist: check + if [ -d $(PACKAGE) ]; then \ + rm -rf $(PACKAGE); \ + fi; \ + mkdir $(PACKAGE); \ + if [ -d $(PACKAGE) ]; then \ + cp -rp --parents $(DIST_FILES) $(PACKAGE); \ + tar cvzf $(PACKAGE).tar.gz $(PACKAGE); \ + rm -rf $(PACKAGE); \ + fi; + +clean: + rm -f $(PACKAGE).tar.gz diff --git a/src/php-gettext/README b/src/php-gettext/README new file mode 100644 index 0000000..bca4f91 --- /dev/null +++ b/src/php-gettext/README @@ -0,0 +1,161 @@ +PHP-gettext 1.0 (https://launchpad.net/php-gettext) + +Copyright 2003, 2006, 2009 -- Danilo "angry with PHP[1]" Segan +Licensed under GPLv2 (or any later version, see COPYING) + +[1] PHP is actually cyrillic, and translates roughly to + "works-doesn't-work" (UTF-8: Ради-Ðе-Ради) + + +Introduction + + How many times did you look for a good translation tool, and + found out that gettext is best for the job? Many times. + + How many times did you try to use gettext in PHP, but failed + miserably, because either your hosting provider didn't support + it, or the server didn't have adequate locale? Many times. + + Well, this is a solution to your needs. It allows using gettext + tools for managing translations, yet it doesn't require gettext + library at all. It parses generated MO files directly, and thus + might be a bit slower than the (maybe provided) gettext library. + + PHP-gettext is a simple reader for GNU gettext MO files. Those + are binary containers for translations, produced by GNU msgfmt. + +Why? + + I got used to having gettext work even without gettext + library. It's there in my favourite language Python, so I was + surprised that I couldn't find it in PHP. I even Googled for it, + but to no avail. + + So, I said, what the heck, I'm going to write it for this + disguisting language of PHP, because I'm often constrained to it. + +Features + + o Support for simple translations + Just define a simple alias for translate() function (suggested + use of _() or gettext(); see provided example). + + o Support for ngettext calls (plural forms, see a note under bugs) + You may also use plural forms. Translations in MO files need to + provide this, and they must also provide "plural-forms" header. + Please see 'info gettext' for more details. + + o Support for reading straight files, or strings (!!!) + Since I can imagine many different backends for reading in the MO + file data, I used imaginary abstract class StreamReader to do all + the input (check streams.php). For your convenience, I've already + provided two classes for reading files: FileReader and + StringReader (CachedFileReader is a combination of the two: it + loads entire file contents into a string, and then works on that). + See example below for usage. You can for instance use StringReader + when you read in data from a database, or you can create your own + derivative of StreamReader for anything you like. + + +Bugs + + Report them on https://bugs.launchpad.net/php-gettext + +Usage + + Put files streams.php and gettext.php somewhere you can load them + from, and require 'em in where you want to use them. + + Then, create one 'stream reader' (a class that provides functions + like read(), seekto(), currentpos() and length()) which will + provide data for the 'gettext_reader', with eg. + $streamer = new FileStream('data.mo'); + + Then, use that as a parameter to gettext_reader constructor: + $wohoo = new gettext_reader($streamer); + + If you want to disable pre-loading of entire message catalog in + memory (if, for example, you have a multi-thousand message catalog + which you'll use only occasionally), use "false" for second + parameter to gettext_reader constructor: + $wohoo = new gettext_reader($streamer, false); + + From now on, you have all the benefits of gettext data at your + disposal, so may run: + print $wohoo->translate("This is a test"); + print $wohoo->ngettext("%d bird", "%d birds", $birds); + + You might need to pass parameter "-k" to xgettext to make it + extract all the strings. In above example, try with + xgettext -ktranslate -kngettext:1,2 file.php + what should create messages.po which contains two messages for + translation. + + I suggest creating simple aliases for these functions (see + example/pigs.php for how do I do it, which means it's probably a + bad way). + + +Usage with gettext.inc (standard gettext interfaces emulation) + + Check example in examples/pig_dropin.php, basically you include + gettext.inc and use all the standard gettext interfaces as + documented on: + + http://www.php.net/gettext + + The only catch is that you can check return value of setlocale() + to see if your locale is system supported or not. + + +Example + + See in examples/ subdirectory. There are a couple of files. + pigs.php is an example, serbian.po is a translation to Serbian + language, and serbian.mo is generated with + msgfmt -o serbian.mo serbian.po + There is also simple "update" script that can be used to generate + POT file and to update the translation using msgmerge. + +TODO: + + o Improve speed to be even more comparable to the native gettext + implementation. + + o Try to use hash tables in MO files: with pre-loading, would it + be useful at all? + +Never-asked-questions: + + o Why did you mark this as version 1.0 when this is the first code + release? + + Well, it's quite simple. I consider that the first released thing + should be labeled "version 1" (first, right?). Zero is there to + indicate that there's zero improvement and/or change compared to + "version 1". + + I plan to use version numbers 1.0.* for small bugfixes, and to + release 1.1 as "first stable release of version 1". + + This may trick someone that this is actually useful software, but + as with any other free software, I take NO RESPONSIBILITY for + creating such a masterpiece that will smoke crack, trash your + hard disk, and make lasers in your CD device dance to the tune of + Mozart's 40th Symphony (there is one like that, right?). + + o Can I...? + + Yes, you can. This is free software (as in freedom, free speech), + and you might do whatever you wish with it, provided you do not + limit freedom of others (GPL). + + I'm considering licensing this under LGPL, but I *do* want + *every* PHP-gettext user to contribute and respect ideas of free + software, so don't count on it happening anytime soon. + + I'm sorry that I'm taking away your freedom of taking others' + freedom away, but I believe that's neglible as compared to what + freedoms you could take away. ;-) + + Uhm, whatever. diff --git a/src/php-gettext/examples/index.php b/src/php-gettext/examples/index.php new file mode 100644 index 0000000..263cd3d --- /dev/null +++ b/src/php-gettext/examples/index.php @@ -0,0 +1,27 @@ +<html> +<head> +<title>PHP-gettext examples</title> +</head> +<body> +<h1>PHP-gettext</h1> + +<h2>Introduction</h2> +<p>PHP-gettext provides a simple gettext replacement that works independently from the system's gettext abilities. +It can read MO files and use them for translating strings.</p> +<p>This version has the ability to cache all strings and translations to speed up the string lookup. +While the cache is enabled by default, it can be switched off with the second parameter in the constructor (e.g. when using very large MO files +that you don't want to keep in memory)</p> + + +<h2>Examples</h2> +<ul> + <li><a href="pigs_dropin.php">PHP-gettext as a dropin replacement</a></li> + <li><a href="pigs_fallback.php">PHP-gettext as a fallback solution</a></li> +</ul> + +<hr /> +<p>Copyright (c) 2003-2006 Danilo Segan</p> +<p>Copyright (c) 2005-2006 Steven Armstrong</p> + +</body> +</html> diff --git a/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.mo b/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.mo Binary files differnew file mode 100644 index 0000000..9193037 --- /dev/null +++ b/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.mo diff --git a/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.po b/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.po new file mode 100644 index 0000000..6e4886b --- /dev/null +++ b/src/php-gettext/examples/locale/de_CH/LC_MESSAGES/messages.po @@ -0,0 +1,30 @@ +# Sample translation for PHP-gettext 1.0 +# Copyright (c) 2003 Danilo Segan <danilo@kvota.net> +# +msgid "" +msgstr "" +"Project-Id-Version: pigs\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2003-10-23 04:50+0200\n" +"PO-Revision-Date: 2003-11-01 23:40+0100\n" +"Last-Translator: Danilo Segan <danilo@kvota.net>\n" +"Language-Team: Serbian (sr) <danilo@kvota.net>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +#"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: pigs.php:19 +msgid "" +"This is how the story goes.\n" +"\n" +msgstr "" +"Und so geht die Geschichte.\n" +"\n" + +#: pigs.php:21 +#, php-format +msgid "%d pig went to the market\n" +msgid_plural "%d pigs went to the market\n" +msgstr[0] "%d Schwein ging zum Markt\n" +msgstr[1] "%d Schweine gingen zum Markt\n" diff --git a/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.mo b/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.mo Binary files differnew file mode 100644 index 0000000..497c883 --- /dev/null +++ b/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.mo diff --git a/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.po b/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.po new file mode 100644 index 0000000..e5da0e9 --- /dev/null +++ b/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.po @@ -0,0 +1,30 @@ +# Sample translation for PHP-gettext 1.0 +# Copyright (c) 2003,2006 Danilo Segan <danilo@kvota.net> +# +msgid "" +msgstr "" +"Project-Id-Version: pigs\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2003-10-23 04:50+0200\n" +"PO-Revision-Date: 2006-02-02 21:06+0100\n" +"Last-Translator: Danilo Segan <danilo@kvota.net>\n" +"Language-Team: Serbian (sr) <danilo@kvota.net>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" + +#: pigs.php:19 +msgid "" +"This is how the story goes.\n" +"\n" +msgstr "Овако иде прича.\n\n" + +#: pigs.php:21 +#, php-format +msgid "%d pig went to the market\n" +msgid_plural "%d pigs went to the market\n" +msgstr[0] "%d мало праÑе је отишло на пијац\n" +msgstr[1] "%d мала праÑета Ñу отишла на пијац\n" +msgstr[2] "%d малих праÑића је отишло на пијац\n" diff --git a/src/php-gettext/examples/pigs_dropin.php b/src/php-gettext/examples/pigs_dropin.php new file mode 100644 index 0000000..94fd850 --- /dev/null +++ b/src/php-gettext/examples/pigs_dropin.php @@ -0,0 +1,89 @@ +<?php +/* + Copyright (c) 2003,2004,2005,2009 Danilo Segan <danilo@kvota.net>. + Copyright (c) 2005,2006 Steven Armstrong <sa@c-area.ch> + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + +error_reporting(E_ALL | E_STRICT); + +// define constants +define('PROJECT_DIR', realpath('./')); +define('LOCALE_DIR', PROJECT_DIR .'/locale'); +define('DEFAULT_LOCALE', 'en_US'); + +require_once('../gettext.inc'); + +$supported_locales = array('en_US', 'sr_CS', 'de_CH'); +$encoding = 'UTF-8'; + +$locale = (isset($_GET['lang']))? $_GET['lang'] : DEFAULT_LOCALE; + +// gettext setup +T_setlocale(LC_MESSAGES, $locale); +// Set the text domain as 'messages' +$domain = 'messages'; +bindtextdomain($domain, LOCALE_DIR); +// bind_textdomain_codeset is supported only in PHP 4.2.0+ +if (function_exists('bind_textdomain_codeset')) + bind_textdomain_codeset($domain, $encoding); +textdomain($domain); + +header("Content-type: text/html; charset=$encoding"); +?> +<html> +<head> +<title>PHP-gettext dropin example</title> +</head> +<body> +<h1>PHP-gettext as a dropin replacement</h1> +<p>Example showing how to use PHP-gettext as a dropin replacement for the native gettext library.</p> +<?php +print "<p>"; +foreach($supported_locales as $l) { + print "[<a href=\"?lang=$l\">$l</a>] "; +} +print "</p>\n"; + +if (!locale_emulation()) { + print "<p>locale '$locale' is supported by your system, using native gettext implementation.</p>\n"; +} +else { + print "<p>locale '$locale' is _not_ supported on your system, using the default locale '". DEFAULT_LOCALE ."'.</p>\n"; +} +?> + +<hr /> + +<?php +// using PHP-gettext +print "<pre>"; +print _("This is how the story goes.\n\n"); +for ($number=6; $number>=0; $number--) { + print sprintf(T_ngettext("%d pig went to the market\n", + "%d pigs went to the market\n", $number), + $number ); +} +print "</pre>\n"; +?> + +<hr /> +<p>« <a href="./">back</a></p> +</body> +</html> diff --git a/src/php-gettext/examples/pigs_fallback.php b/src/php-gettext/examples/pigs_fallback.php new file mode 100644 index 0000000..353190d --- /dev/null +++ b/src/php-gettext/examples/pigs_fallback.php @@ -0,0 +1,88 @@ +<?php +/* + Copyright (c) 2003,2004,2005,2009 Danilo Segan <danilo@kvota.net>. + Copyright (c) 2005,2006 Steven Armstrong <sa@c-area.ch> + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + +error_reporting(E_ALL | E_STRICT); + +// define constants +define('PROJECT_DIR', realpath('./')); +define('LOCALE_DIR', PROJECT_DIR .'/locale'); +define('DEFAULT_LOCALE', 'en_US'); + +require_once('../gettext.inc'); + +$supported_locales = array('en_US', 'sr_CS', 'de_CH'); +$encoding = 'UTF-8'; + +$locale = (isset($_GET['lang']))? $_GET['lang'] : DEFAULT_LOCALE; + +// gettext setup +T_setlocale(LC_MESSAGES, $locale); +// Set the text domain as 'messages' +$domain = 'messages'; +T_bindtextdomain($domain, LOCALE_DIR); +T_bind_textdomain_codeset($domain, $encoding); +T_textdomain($domain); + +header("Content-type: text/html; charset=$encoding"); +?> +<html> +<head> +<title>PHP-gettext fallback example</title> +</head> +<body> +<h1>PHP-gettext as a fallback solution</h1> +<p>Example showing how to use PHP-gettext as a fallback solution if the native gettext library is not available or the system does not support the requested locale.</p> + +<?php +print "<p>"; +foreach($supported_locales as $l) { + print "[<a href=\"?lang=$l\">$l</a>] "; +} +print "</p>\n"; + +if (!locale_emulation()) { + print "<p>locale '$locale' is supported by your system, using native gettext implementation.</p>\n"; +} +else { + print "<p>locale '$locale' is <strong>not</strong> supported on your system, using custom gettext implementation.</p>\n"; +} +?> + +<hr /> + +<?php +// using PHP-gettext +print "<pre>"; +print T_("This is how the story goes.\n\n"); +for ($number=6; $number>=0; $number--) { + print sprintf( T_ngettext("%d pig went to the market\n", + "%d pigs went to the market\n", $number), + $number ); +} +print "</pre>\n"; +?> + +<hr /> +<p>« <a href="./">back</a></p> +</body> +</html> diff --git a/src/php-gettext/examples/update b/src/php-gettext/examples/update new file mode 100644 index 0000000..76b4308 --- /dev/null +++ b/src/php-gettext/examples/update @@ -0,0 +1,14 @@ +#!/bin/sh +TEMPLATE=pigs.pot +xgettext -kT_ngettext:1,2 -kT_ -L PHP -o $TEMPLATE pigs_dropin.php +if [ "x$1" = "x-p" ]; then + msgfmt --statistics $TEMPLATE +else + if [ -f $1.po ]; then + msgmerge -o .tmp$1.po $1.po $TEMPLATE + mv .tmp$1.po $1.po + msgfmt --statistics $1.po + else + echo "Usage: $0 [-p|<basename>]" + fi +fi diff --git a/src/php-gettext/gettext.inc b/src/php-gettext/gettext.inc new file mode 100644 index 0000000..399a0f2 --- /dev/null +++ b/src/php-gettext/gettext.inc @@ -0,0 +1,534 @@ +<?php +/* + Copyright (c) 2005 Steven Armstrong <sa at c-area dot ch> + Copyright (c) 2009 Danilo Segan <danilo@kvota.net> + + Drop in replacement for native gettext. + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ +/* +LC_CTYPE 0 +LC_NUMERIC 1 +LC_TIME 2 +LC_COLLATE 3 +LC_MONETARY 4 +LC_MESSAGES 5 +LC_ALL 6 +*/ + + +// LC_MESSAGES is not available if php-gettext is not loaded +// while the other constants are already available from session extension. +if (!defined('LC_MESSAGES')) { + define('LC_MESSAGES', 5); +} + +require('streams.php'); +require('gettext.php'); + + +// Variables + +global $text_domains, $default_domain, $LC_CATEGORIES, $EMULATEGETTEXT, $CURRENTLOCALE; +$text_domains = array(); +$default_domain = 'messages'; +$LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MONETARY', 'LC_MESSAGES', 'LC_ALL'); +$EMULATEGETTEXT = 0; +$CURRENTLOCALE = ''; + +/* Class to hold a single domain included in $text_domains. */ +class domain { + var $l10n; + var $path; + var $codeset; +} + +// Utility functions + +/** + * Return a list of locales to try for any POSIX-style locale specification. + */ +function get_list_of_locales($locale) { + /* Figure out all possible locale names and start with the most + * specific ones. I.e. for sr_CS.UTF-8@latin, look through all of + * sr_CS.UTF-8@latin, sr_CS@latin, sr@latin, sr_CS.UTF-8, sr_CS, sr. + */ + $locale_names = array(); + $lang = NULL; + $country = NULL; + $charset = NULL; + $modifier = NULL; + if ($locale) { + if (preg_match("/^(?P<lang>[a-z]{2,3})" // language code + ."(?:_(?P<country>[A-Z]{2}))?" // country code + ."(?:\.(?P<charset>[-A-Za-z0-9_]+))?" // charset + ."(?:@(?P<modifier>[-A-Za-z0-9_]+))?$/", // @ modifier + $locale, $matches)) { + + if (isset($matches["lang"])) $lang = $matches["lang"]; + if (isset($matches["country"])) $country = $matches["country"]; + if (isset($matches["charset"])) $charset = $matches["charset"]; + if (isset($matches["modifier"])) $modifier = $matches["modifier"]; + + if ($modifier) { + if ($country) { + if ($charset) + array_push($locale_names, "${lang}_$country.$charset@$modifier"); + array_push($locale_names, "${lang}_$country@$modifier"); + } elseif ($charset) + array_push($locale_names, "${lang}.$charset@$modifier"); + array_push($locale_names, "$lang@$modifier"); + } + if ($country) { + if ($charset) + array_push($locale_names, "${lang}_$country.$charset"); + array_push($locale_names, "${lang}_$country"); + } elseif ($charset) + array_push($locale_names, "${lang}.$charset"); + array_push($locale_names, $lang); + } + + // If the locale name doesn't match POSIX style, just include it as-is. + if (!in_array($locale, $locale_names)) + array_push($locale_names, $locale); + } + return $locale_names; +} + +/** + * Utility function to get a StreamReader for the given text domain. + */ +function _get_reader($domain=null, $category=5, $enable_cache=true) { + global $text_domains, $default_domain, $LC_CATEGORIES; + if (!isset($domain)) $domain = $default_domain; + if (!isset($text_domains[$domain]->l10n)) { + // get the current locale + $locale = _setlocale(LC_MESSAGES, 0); + $bound_path = isset($text_domains[$domain]->path) ? + $text_domains[$domain]->path : './'; + $subpath = $LC_CATEGORIES[$category] ."/$domain.mo"; + + $locale_names = get_list_of_locales($locale); + $input = null; + foreach ($locale_names as $locale) { + $full_path = $bound_path . $locale . "/" . $subpath; + if (file_exists($full_path)) { + $input = new FileReader($full_path); + break; + } + } + + if (!array_key_exists($domain, $text_domains)) { + // Initialize an empty domain object. + $text_domains[$domain] = new domain(); + } + $text_domains[$domain]->l10n = new gettext_reader($input, + $enable_cache); + } + return $text_domains[$domain]->l10n; +} + +/** + * Returns whether we are using our emulated gettext API or PHP built-in one. + */ +function locale_emulation() { + global $EMULATEGETTEXT; + return $EMULATEGETTEXT; +} + +/** + * Checks if the current locale is supported on this system. + */ +function _check_locale_and_function($function=false) { + global $EMULATEGETTEXT; + if ($function and !function_exists($function)) + return false; + return !$EMULATEGETTEXT; +} + +/** + * Get the codeset for the given domain. + */ +function _get_codeset($domain=null) { + global $text_domains, $default_domain, $LC_CATEGORIES; + if (!isset($domain)) $domain = $default_domain; + return (isset($text_domains[$domain]->codeset))? $text_domains[$domain]->codeset : ini_get('mbstring.internal_encoding'); +} + +/** + * Convert the given string to the encoding set by bind_textdomain_codeset. + */ +function _encode($text) { + $source_encoding = mb_detect_encoding($text); + $target_encoding = _get_codeset(); + if ($source_encoding != $target_encoding) { + return mb_convert_encoding($text, $target_encoding, $source_encoding); + } + else { + return $text; + } +} + + +// Custom implementation of the standard gettext related functions + +/** + * Returns passed in $locale, or environment variable $LANG if $locale == ''. + */ +function _get_default_locale($locale) { + if ($locale == '') // emulate variable support + return getenv('LANG'); + else + return $locale; +} + +/** + * Sets a requested locale, if needed emulates it. + */ +function _setlocale($category, $locale) { + global $CURRENTLOCALE, $EMULATEGETTEXT; + if ($locale === 0) { // use === to differentiate between string "0" + if ($CURRENTLOCALE != '') + return $CURRENTLOCALE; + else + // obey LANG variable, maybe extend to support all of LC_* vars + // even if we tried to read locale without setting it first + return _setlocale($category, $CURRENTLOCALE); + } else { + if (function_exists('setlocale')) { + $ret = setlocale($category, $locale); + if (($locale == '' and !$ret) or // failed setting it by env + ($locale != '' and $ret != $locale)) { // failed setting it + // Failed setting it according to environment. + $CURRENTLOCALE = _get_default_locale($locale); + $EMULATEGETTEXT = 1; + } else { + $CURRENTLOCALE = $ret; + $EMULATEGETTEXT = 0; + } + } else { + // No function setlocale(), emulate it all. + $CURRENTLOCALE = _get_default_locale($locale); + $EMULATEGETTEXT = 1; + } + // Allow locale to be changed on the go for one translation domain. + global $text_domains, $default_domain; + unset($text_domains[$default_domain]->l10n); + return $CURRENTLOCALE; + } +} + +/** + * Sets the path for a domain. + */ +function _bindtextdomain($domain, $path) { + global $text_domains; + // ensure $path ends with a slash ('/' should work for both, but lets still play nice) + if (substr(php_uname(), 0, 7) == "Windows") { + if ($path[strlen($path)-1] != '\\' and $path[strlen($path)-1] != '/') + $path .= '\\'; + } else { + if ($path[strlen($path)-1] != '/') + $path .= '/'; + } + if (!array_key_exists($domain, $text_domains)) { + // Initialize an empty domain object. + $text_domains[$domain] = new domain(); + } + $text_domains[$domain]->path = $path; +} + +/** + * Specify the character encoding in which the messages from the DOMAIN message catalog will be returned. + */ +function _bind_textdomain_codeset($domain, $codeset) { + global $text_domains; + $text_domains[$domain]->codeset = $codeset; +} + +/** + * Sets the default domain. + */ +function _textdomain($domain) { + global $default_domain; + $default_domain = $domain; +} + +/** + * Lookup a message in the current domain. + */ +function _gettext($msgid) { + $l10n = _get_reader(); + return _encode($l10n->translate($msgid)); +} + +/** + * Alias for gettext. + */ +function __($msgid) { + return _gettext($msgid); +} + +/** + * Plural version of gettext. + */ +function _ngettext($single, $plural, $number) { + $l10n = _get_reader(); + return _encode($l10n->ngettext($single, $plural, $number)); +} + +/** + * Override the current domain. + */ +function _dgettext($domain, $msgid) { + $l10n = _get_reader($domain); + return _encode($l10n->translate($msgid)); +} + +/** + * Plural version of dgettext. + */ +function _dngettext($domain, $single, $plural, $number) { + $l10n = _get_reader($domain); + return _encode($l10n->ngettext($single, $plural, $number)); +} + +/** + * Overrides the domain and category for a single lookup. + */ +function _dcgettext($domain, $msgid, $category) { + $l10n = _get_reader($domain, $category); + return _encode($l10n->translate($msgid)); +} +/** + * Plural version of dcgettext. + */ +function _dcngettext($domain, $single, $plural, $number, $category) { + $l10n = _get_reader($domain, $category); + return _encode($l10n->ngettext($single, $plural, $number)); +} + +/** + * Context version of gettext. + */ +function _pgettext($context, $msgid) { + $l10n = _get_reader(); + return _encode($l10n->pgettext($context, $msgid)); +} + +/** + * Override the current domain in a context gettext call. + */ +function _dpgettext($domain, $context, $msgid) { + $l10n = _get_reader($domain); + return _encode($l10n->pgettext($context, $msgid)); +} + +/** + * Overrides the domain and category for a single context-based lookup. + */ +function _dcpgettext($domain, $context, $msgid, $category) { + $l10n = _get_reader($domain, $category); + return _encode($l10n->pgettext($context, $msgid)); +} + +/** + * Context version of ngettext. + */ +function _npgettext($context, $singular, $plural) { + $l10n = _get_reader(); + return _encode($l10n->npgettext($context, $singular, $plural)); +} + +/** + * Override the current domain in a context ngettext call. + */ +function _dnpgettext($domain, $context, $singular, $plural) { + $l10n = _get_reader($domain); + return _encode($l10n->npgettext($context, $singular, $plural)); +} + +/** + * Overrides the domain and category for a plural context-based lookup. + */ +function _dcnpgettext($domain, $context, $singular, $plural, $category) { + $l10n = _get_reader($domain, $category); + return _encode($l10n->npgettext($context, $singular, $plural)); +} + + + +// Wrappers to use if the standard gettext functions are available, +// but the current locale is not supported by the system. +// Use the standard impl if the current locale is supported, use the +// custom impl otherwise. + +function T_setlocale($category, $locale) { + return _setlocale($category, $locale); +} + +function T_bindtextdomain($domain, $path) { + if (_check_locale_and_function()) return bindtextdomain($domain, $path); + else return _bindtextdomain($domain, $path); +} +function T_bind_textdomain_codeset($domain, $codeset) { + // bind_textdomain_codeset is available only in PHP 4.2.0+ + if (_check_locale_and_function('bind_textdomain_codeset')) + return bind_textdomain_codeset($domain, $codeset); + else return _bind_textdomain_codeset($domain, $codeset); +} +function T_textdomain($domain) { + if (_check_locale_and_function()) return textdomain($domain); + else return _textdomain($domain); +} +function T_gettext($msgid) { + if (_check_locale_and_function()) return gettext($msgid); + else return _gettext($msgid); +} +function T_($msgid) { + if (_check_locale_and_function()) return _($msgid); + return __($msgid); +} +function T_ngettext($single, $plural, $number) { + if (_check_locale_and_function()) + return ngettext($single, $plural, $number); + else return _ngettext($single, $plural, $number); +} +function T_dgettext($domain, $msgid) { + if (_check_locale_and_function()) return dgettext($domain, $msgid); + else return _dgettext($domain, $msgid); +} +function T_dngettext($domain, $single, $plural, $number) { + if (_check_locale_and_function()) + return dngettext($domain, $single, $plural, $number); + else return _dngettext($domain, $single, $plural, $number); +} +function T_dcgettext($domain, $msgid, $category) { + if (_check_locale_and_function()) + return dcgettext($domain, $msgid, $category); + else return _dcgettext($domain, $msgid, $category); +} +function T_dcngettext($domain, $single, $plural, $number, $category) { + if (_check_locale_and_function()) + return dcngettext($domain, $single, $plural, $number, $category); + else return _dcngettext($domain, $single, $plural, $number, $category); +} + +function T_pgettext($context, $msgid) { + if (_check_locale_and_function('pgettext')) + return pgettext($context, $msgid); + else + return _pgettext($context, $msgid); +} + +function T_dpgettext($domain, $context, $msgid) { + if (_check_locale_and_function('dpgettext')) + return dpgettext($domain, $context, $msgid); + else + return _dpgettext($domain, $context, $msgid); +} + +function T_dcpgettext($domain, $context, $msgid, $category) { + if (_check_locale_and_function('dcpgettext')) + return dcpgettext($domain, $context, $msgid, $category); + else + return _dcpgettext($domain, $context, $msgid, $category); +} + +function T_npgettext($context, $singular, $plural) { + if (_check_locale_and_function('npgettext')) + return npgettext($context, $single, $plural, $number); + else + return _npgettext($context, $single, $plural, $number); +} + +function T_dnpgettext($domain, $context, $singular, $plural) { + if (_check_locale_and_function('dnpgettext')) + return dnpgettext($domain, $context, $single, $plural, $number); + else + return _dnpgettext($domain, $context, $single, $plural, $number); +} + +function T_dcnpgettext($domain, $context, $singular, $plural, $category) { + if (_check_locale_and_function('dcnpgettext')) + return dcnpgettext($domain, $context, $single, + $plural, $number, $category); + else + return _dcnpgettext($domain, $context, $single, + $plural, $number, $category); +} + + + +// Wrappers used as a drop in replacement for the standard gettext functions + +if (!function_exists('gettext')) { + function bindtextdomain($domain, $path) { + return _bindtextdomain($domain, $path); + } + function bind_textdomain_codeset($domain, $codeset) { + return _bind_textdomain_codeset($domain, $codeset); + } + function textdomain($domain) { + return _textdomain($domain); + } + function gettext($msgid) { + return _gettext($msgid); + } + function _($msgid) { + return __($msgid); + } + function ngettext($single, $plural, $number) { + return _ngettext($single, $plural, $number); + } + function dgettext($domain, $msgid) { + return _dgettext($domain, $msgid); + } + function dngettext($domain, $single, $plural, $number) { + return _dngettext($domain, $single, $plural, $number); + } + function dcgettext($domain, $msgid, $category) { + return _dcgettext($domain, $msgid, $category); + } + function dcngettext($domain, $single, $plural, $number, $category) { + return _dcngettext($domain, $single, $plural, $number, $category); + } + function pgettext($context, $msgid) { + return _pgettext($context, $msgid); + } + function npgettext($context, $single, $plural, $number) { + return _npgettext($context, $single, $plural, $number); + } + function dpgettext($domain, $context, $msgid) { + return _dpgettext($domain, $context, $msgid); + } + function dnpgettext($domain, $context, $single, $plural, $number) { + return _dnpgettext($domain, $context, $single, $plural, $number); + } + function dcpgettext($domain, $context, $msgid, $category) { + return _dcpgettext($domain, $context, $msgid, $category); + } + function dcnpgettext($domain, $context, $single, $plural, + $number, $category) { + return _dcnpgettext($domain, $context, $single, $plural, + $number, $category); + } +} + +?> diff --git a/src/php-gettext/gettext.php b/src/php-gettext/gettext.php new file mode 100644 index 0000000..a121f9c --- /dev/null +++ b/src/php-gettext/gettext.php @@ -0,0 +1,421 @@ +<?php +/* + Copyright (c) 2003, 2009 Danilo Segan <danilo@kvota.net>. + Copyright (c) 2005 Nico Kaiser <nico@siriux.net> + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + +/** + * Provides a simple gettext replacement that works independently from + * the system's gettext abilities. + * It can read MO files and use them for translating strings. + * The files are passed to gettext_reader as a Stream (see streams.php) + * + * This version has the ability to cache all strings and translations to + * speed up the string lookup. + * While the cache is enabled by default, it can be switched off with the + * second parameter in the constructor (e.g. whenusing very large MO files + * that you don't want to keep in memory) + */ +class gettext_reader { + //public: + var $error = 0; // public variable that holds error code (0 if no error) + + //private: + var $BYTEORDER = 0; // 0: low endian, 1: big endian + var $STREAM = NULL; + var $short_circuit = false; + var $enable_cache = false; + var $originals = NULL; // offset of original table + var $translations = NULL; // offset of translation table + var $pluralheader = NULL; // cache header field for plural forms + var $total = 0; // total string count + var $table_originals = NULL; // table for original strings (offsets) + var $table_translations = NULL; // table for translated strings (offsets) + var $cache_translations = NULL; // original -> translation mapping + + + /* Methods */ + + + /** + * Reads a 32bit Integer from the Stream + * + * @access private + * @return Integer from the Stream + */ + function readint() { + if ($this->BYTEORDER == 0) { + // low endian + $input=unpack('V', $this->STREAM->read(4)); + return array_shift($input); + } else { + // big endian + $input=unpack('N', $this->STREAM->read(4)); + return array_shift($input); + } + } + + function read($bytes) { + return $this->STREAM->read($bytes); + } + + /** + * Reads an array of Integers from the Stream + * + * @param int count How many elements should be read + * @return Array of Integers + */ + function readintarray($count) { + if ($this->BYTEORDER == 0) { + // low endian + return unpack('V'.$count, $this->STREAM->read(4 * $count)); + } else { + // big endian + return unpack('N'.$count, $this->STREAM->read(4 * $count)); + } + } + + /** + * Constructor + * + * @param object Reader the StreamReader object + * @param boolean enable_cache Enable or disable caching of strings (default on) + */ + function gettext_reader($Reader, $enable_cache = true) { + // If there isn't a StreamReader, turn on short circuit mode. + if (! $Reader || isset($Reader->error) ) { + $this->short_circuit = true; + return; + } + + // Caching can be turned off + $this->enable_cache = $enable_cache; + + $MAGIC1 = "\x95\x04\x12\xde"; + $MAGIC2 = "\xde\x12\x04\x95"; + + $this->STREAM = $Reader; + $magic = $this->read(4); + if ($magic == $MAGIC1) { + $this->BYTEORDER = 1; + } elseif ($magic == $MAGIC2) { + $this->BYTEORDER = 0; + } else { + $this->error = 1; // not MO file + return false; + } + + // FIXME: Do we care about revision? We should. + $revision = $this->readint(); + + $this->total = $this->readint(); + $this->originals = $this->readint(); + $this->translations = $this->readint(); + } + + /** + * Loads the translation tables from the MO file into the cache + * If caching is enabled, also loads all strings into a cache + * to speed up translation lookups + * + * @access private + */ + function load_tables() { + if (is_array($this->cache_translations) && + is_array($this->table_originals) && + is_array($this->table_translations)) + return; + + /* get original and translations tables */ + if (!is_array($this->table_originals)) { + $this->STREAM->seekto($this->originals); + $this->table_originals = $this->readintarray($this->total * 2); + } + if (!is_array($this->table_translations)) { + $this->STREAM->seekto($this->translations); + $this->table_translations = $this->readintarray($this->total * 2); + } + + if ($this->enable_cache) { + $this->cache_translations = array (); + /* read all strings in the cache */ + for ($i = 0; $i < $this->total; $i++) { + $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); + $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); + $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); + $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); + $this->cache_translations[$original] = $translation; + } + } + } + + /** + * Returns a string from the "originals" table + * + * @access private + * @param int num Offset number of original string + * @return string Requested string if found, otherwise '' + */ + function get_original_string($num) { + $length = $this->table_originals[$num * 2 + 1]; + $offset = $this->table_originals[$num * 2 + 2]; + if (! $length) + return ''; + $this->STREAM->seekto($offset); + $data = $this->STREAM->read($length); + return (string)$data; + } + + /** + * Returns a string from the "translations" table + * + * @access private + * @param int num Offset number of original string + * @return string Requested string if found, otherwise '' + */ + function get_translation_string($num) { + $length = $this->table_translations[$num * 2 + 1]; + $offset = $this->table_translations[$num * 2 + 2]; + if (! $length) + return ''; + $this->STREAM->seekto($offset); + $data = $this->STREAM->read($length); + return (string)$data; + } + + /** + * Binary search for string + * + * @access private + * @param string string + * @param int start (internally used in recursive function) + * @param int end (internally used in recursive function) + * @return int string number (offset in originals table) + */ + function find_string($string, $start = -1, $end = -1) { + if (($start == -1) or ($end == -1)) { + // find_string is called with only one parameter, set start end end + $start = 0; + $end = $this->total; + } + if (abs($start - $end) <= 1) { + // We're done, now we either found the string, or it doesn't exist + $txt = $this->get_original_string($start); + if ($string == $txt) + return $start; + else + return -1; + } else if ($start > $end) { + // start > end -> turn around and start over + return $this->find_string($string, $end, $start); + } else { + // Divide table in two parts + $half = (int)(($start + $end) / 2); + $cmp = strcmp($string, $this->get_original_string($half)); + if ($cmp == 0) + // string is exactly in the middle => return it + return $half; + else if ($cmp < 0) + // The string is in the upper half + return $this->find_string($string, $start, $half); + else + // The string is in the lower half + return $this->find_string($string, $half, $end); + } + } + + /** + * Translates a string + * + * @access public + * @param string string to be translated + * @return string translated string (or original, if not found) + */ + function translate($string) { + if ($this->short_circuit) + return $string; + $this->load_tables(); + + if ($this->enable_cache) { + // Caching enabled, get translated string from cache + if (array_key_exists($string, $this->cache_translations)) + return $this->cache_translations[$string]; + else + return $string; + } else { + // Caching not enabled, try to find string + $num = $this->find_string($string); + if ($num == -1) + return $string; + else + return $this->get_translation_string($num); + } + } + + /** + * Sanitize plural form expression for use in PHP eval call. + * + * @access private + * @return string sanitized plural form expression + */ + function sanitize_plural_expression($expr) { + // Get rid of disallowed characters. + $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); + + // Add parenthesis for tertiary '?' operator. + $expr .= ';'; + $res = ''; + $p = 0; + for ($i = 0; $i < strlen($expr); $i++) { + $ch = $expr[$i]; + switch ($ch) { + case '?': + $res .= ' ? ('; + $p++; + break; + case ':': + $res .= ') : ('; + break; + case ';': + $res .= str_repeat( ')', $p) . ';'; + $p = 0; + break; + default: + $res .= $ch; + } + } + return $res; + } + + /** + * Parse full PO header and extract only plural forms line. + * + * @access private + * @return string verbatim plural form header field + */ + function extract_plural_forms_header_from_po_header($header) { + if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) + $expr = $regs[2]; + else + $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; + return $expr; + } + + /** + * Get possible plural forms from MO header + * + * @access private + * @return string plural form header + */ + function get_plural_forms() { + // lets assume message number 0 is header + // this is true, right? + $this->load_tables(); + + // cache header field for plural forms + if (! is_string($this->pluralheader)) { + if ($this->enable_cache) { + $header = $this->cache_translations[""]; + } else { + $header = $this->get_translation_string(0); + } + $expr = $this->extract_plural_forms_header_from_po_header($header); + $this->pluralheader = $this->sanitize_plural_expression($expr); + } + return $this->pluralheader; + } + + /** + * Detects which plural form to take + * + * @access private + * @param n count + * @return int array index of the right plural form + */ + function select_string($n) { + $string = $this->get_plural_forms(); + $string = str_replace('nplurals',"\$total",$string); + $string = str_replace("n",$n,$string); + $string = str_replace('plural',"\$plural",$string); + + $total = 0; + $plural = 0; + + eval("$string"); + if ($plural >= $total) $plural = $total - 1; + return $plural; + } + + /** + * Plural version of gettext + * + * @access public + * @param string single + * @param string plural + * @param string number + * @return translated plural form + */ + function ngettext($single, $plural, $number) { + if ($this->short_circuit) { + if ($number != 1) + return $plural; + else + return $single; + } + + // find out the appropriate form + $select = $this->select_string($number); + + // this should contains all strings separated by NULLs + $key = $single . chr(0) . $plural; + + + if ($this->enable_cache) { + if (! array_key_exists($key, $this->cache_translations)) { + return ($number != 1) ? $plural : $single; + } else { + $result = $this->cache_translations[$key]; + $list = explode(chr(0), $result); + return $list[$select]; + } + } else { + $num = $this->find_string($key); + if ($num == -1) { + return ($number != 1) ? $plural : $single; + } else { + $result = $this->get_translation_string($num); + $list = explode(chr(0), $result); + return $list[$select]; + } + } + } + + function pgettext($context, $msgid) { + $key = $context . chr(4) . $msgid; + return $this->translate($key); + } + + function npgettext($context, $singular, $plural, $number) { + $singular = $context . chr(4) . $singular; + return $this->ngettext($singular, $plural, $number); + } +} + +?> diff --git a/src/php-gettext/streams.php b/src/php-gettext/streams.php new file mode 100644 index 0000000..3cdc158 --- /dev/null +++ b/src/php-gettext/streams.php @@ -0,0 +1,167 @@ +<?php +/* + Copyright (c) 2003, 2005, 2006, 2009 Danilo Segan <danilo@kvota.net>. + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + + + // Simple class to wrap file streams, string streams, etc. + // seek is essential, and it should be byte stream +class StreamReader { + // should return a string [FIXME: perhaps return array of bytes?] + function read($bytes) { + return false; + } + + // should return new position + function seekto($position) { + return false; + } + + // returns current position + function currentpos() { + return false; + } + + // returns length of entire stream (limit for seekto()s) + function length() { + return false; + } +}; + +class StringReader { + var $_pos; + var $_str; + + function StringReader($str='') { + $this->_str = $str; + $this->_pos = 0; + } + + function read($bytes) { + $data = substr($this->_str, $this->_pos, $bytes); + $this->_pos += $bytes; + if (strlen($this->_str)<$this->_pos) + $this->_pos = strlen($this->_str); + + return $data; + } + + function seekto($pos) { + $this->_pos = $pos; + if (strlen($this->_str)<$this->_pos) + $this->_pos = strlen($this->_str); + return $this->_pos; + } + + function currentpos() { + return $this->_pos; + } + + function length() { + return strlen($this->_str); + } + +}; + + +class FileReader { + var $_pos; + var $_fd; + var $_length; + + function FileReader($filename) { + if (file_exists($filename)) { + + $this->_length=filesize($filename); + $this->_pos = 0; + $this->_fd = fopen($filename,'rb'); + if (!$this->_fd) { + $this->error = 3; // Cannot read file, probably permissions + return false; + } + } else { + $this->error = 2; // File doesn't exist + return false; + } + } + + function read($bytes) { + if ($bytes) { + fseek($this->_fd, $this->_pos); + + // PHP 5.1.1 does not read more than 8192 bytes in one fread() + // the discussions at PHP Bugs suggest it's the intended behaviour + $data = ''; + while ($bytes > 0) { + $chunk = fread($this->_fd, $bytes); + $data .= $chunk; + $bytes -= strlen($chunk); + } + $this->_pos = ftell($this->_fd); + + return $data; + } else return ''; + } + + function seekto($pos) { + fseek($this->_fd, $pos); + $this->_pos = ftell($this->_fd); + return $this->_pos; + } + + function currentpos() { + return $this->_pos; + } + + function length() { + return $this->_length; + } + + function close() { + fclose($this->_fd); + } + +}; + +// Preloads entire file in memory first, then creates a StringReader +// over it (it assumes knowledge of StringReader internals) +class CachedFileReader extends StringReader { + function CachedFileReader($filename) { + if (file_exists($filename)) { + + $length=filesize($filename); + $fd = fopen($filename,'rb'); + + if (!$fd) { + $this->error = 3; // Cannot read file, probably permissions + return false; + } + $this->_str = fread($fd, $length); + fclose($fd); + + } else { + $this->error = 2; // File doesn't exist + return false; + } + } +}; + + +?> diff --git a/src/php-gettext/tests/LocalesTest.php b/src/php-gettext/tests/LocalesTest.php new file mode 100644 index 0000000..3000286 --- /dev/null +++ b/src/php-gettext/tests/LocalesTest.php @@ -0,0 +1,66 @@ +<?php +require_once('PHPUnit/Framework.php'); +require_once('gettext.inc'); + +class LocaleTest extends PHPUnit_Framework_TestCase +{ + public function test_setlocale() + { + // _setlocale defaults to a locale name from environment variable LANG. + putenv("LANG=sr_RS"); + $this->assertEquals('sr_RS', _setlocale(LC_MESSAGES, 0)); + + // For an existing locale, it never needs emulation. + putenv("LANG=C"); + _setlocale(LC_MESSAGES, ""); + $this->assertEquals(0, locale_emulation()); + + // If we set it to a non-existent locale, it still works, but uses + // emulation. + _setlocale(LC_MESSAGES, "xxx_XXX"); + $this->assertEquals('xxx_XXX', _setlocale(LC_MESSAGES, 0)); + $this->assertEquals(1, locale_emulation()); + } + + public function test_get_list_of_locales() + { + // For a locale containing country code, we prefer + // full locale name, but if that's not found, fall back + // to the language only locale name. + $this->assertEquals(array("sr_RS", "sr"), + get_list_of_locales("sr_RS")); + + // If language code is used, it's the only thing returned. + $this->assertEquals(array("sr"), + get_list_of_locales("sr")); + + // There is support for language and charset only. + $this->assertEquals(array("sr.UTF-8", "sr"), + get_list_of_locales("sr.UTF-8")); + + // It can also split out character set from the full locale name. + $this->assertEquals(array("sr_RS.UTF-8", "sr_RS", "sr"), + get_list_of_locales("sr_RS.UTF-8")); + + // There is support for @modifier in locale names as well. + $this->assertEquals(array("sr_RS.UTF-8@latin", "sr_RS@latin", "sr@latin", + "sr_RS.UTF-8", "sr_RS", "sr"), + get_list_of_locales("sr_RS.UTF-8@latin")); + + // We can pass in only language and modifier. + $this->assertEquals(array("sr@latin", "sr"), + get_list_of_locales("sr@latin")); + + + // If locale name is not following the regular POSIX pattern, + // it's used verbatim. + $this->assertEquals(array("something"), + get_list_of_locales("something")); + + // Passing in an empty string returns an empty array. + $this->assertEquals(array(), + get_list_of_locales("")); + } +} + +?> diff --git a/src/php-gettext/tests/ParsingTest.php b/src/php-gettext/tests/ParsingTest.php new file mode 100644 index 0000000..9b350b2 --- /dev/null +++ b/src/php-gettext/tests/ParsingTest.php @@ -0,0 +1,43 @@ +<?php +require_once('PHPUnit/Framework.php'); +//require_once('gettext.php'); + +class ParsingTest extends PHPUnit_Framework_TestCase +{ + public function test_extract_plural_forms_header_from_po_header() + { + $parser = new gettext_reader(NULL); + // It defaults to a "Western-style" plural header. + $this->assertEquals( + 'nplurals=2; plural=n == 1 ? 0 : 1;', + $parser->extract_plural_forms_header_from_po_header("")); + + // Extracting it from the middle of the header works. + $this->assertEquals( + 'nplurals=1; plural=0;', + $parser->extract_plural_forms_header_from_po_header( + "Content-type: text/html; charset=UTF-8\n" + ."Plural-Forms: nplurals=1; plural=0;\n" + ."Last-Translator: nobody\n" + )); + + // It's also case-insensitive. + $this->assertEquals( + 'nplurals=1; plural=0;', + $parser->extract_plural_forms_header_from_po_header( + "PLURAL-forms: nplurals=1; plural=0;\n" + )); + + // It falls back to default if it's not on a separate line. + $this->assertEquals( + 'nplurals=2; plural=n == 1 ? 0 : 1;', + $parser->extract_plural_forms_header_from_po_header( + "Content-type: text/html; charset=UTF-8" // note the missing \n here + ."Plural-Forms: nplurals=1; plural=0;\n" + ."Last-Translator: nobody\n" + )); + + } + +} +?> |