diff options
Diffstat (limited to 'src')
23 files changed, 1284 insertions, 454 deletions
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/User.php b/src/SemanticScuttle/Model/User.php index ed9f454..500f5b1 100644 --- a/src/SemanticScuttle/Model/User.php +++ b/src/SemanticScuttle/Model/User.php @@ -15,7 +15,7 @@ /** * SemanticScuttle user object. - * Rare fields are filled if required. + * Rarely used fields are filled if required. * * @category Bookmarking * @package SemanticScuttle @@ -133,7 +133,8 @@ class SemanticScuttle_Model_User } /** - * Returns user creation time + * Returns user creation time. + * UTC/Zulu time zone is used. * * @return string Datetime value: "YYYY-MM-DD HH:MM:SS" */ 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/Bookmark.php b/src/SemanticScuttle/Service/Bookmark.php index 364b1a0..a30ad5f 100644 --- a/src/SemanticScuttle/Service/Bookmark.php +++ b/src/SemanticScuttle/Service/Bookmark.php @@ -12,6 +12,7 @@ * @license GPL http://www.gnu.org/licenses/gpl.html * @link http://sourceforge.net/projects/semanticscuttle */ +require_once 'SemanticScuttle/Model/RemoteUser.php'; /** * SemanticScuttle bookmark service. @@ -44,6 +45,13 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService + /** + * Creates a new instance. Initializes the table name. + * + * @param DB $db Database object + * + * @uses $GLOBALS['tableprefix'] + */ public function __construct($db) { $this->db = $db; @@ -168,7 +176,10 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * Retrieves a bookmark with the given URL. * DOES NOT RESPECT PRIVACY SETTINGS! * - * @param string $hash URL + * @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). @@ -176,9 +187,9 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * @uses getBookmarkByHash() * @see getBookmarkByShortname() */ - public function getBookmarkByAddress($address) + public function getBookmarkByAddress($address, $all = true) { - return $this->getBookmarkByHash($this->getHash($address)); + return $this->getBookmarkByHash($this->getHash($address), $all); } @@ -187,16 +198,19 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * Retrieves a bookmark with the given hash. * DOES NOT RESPECT PRIVACY SETTINGS! * - * @param string $hash URL hash + * @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) + public function getBookmarkByHash($hash, $all = true) { - return $this->_getbookmark('bHash', $hash, true); + return $this->_getbookmark('bHash', $hash, $all); } @@ -239,18 +253,18 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService /** * Counts bookmarks for a user. * - * @param integer $uId User ID - * @param string $range Range of bookmarks: - * 'public', 'shared', 'private' - * or 'all' + * @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, $range = 'public') + public function countBookmarks($uId, $status = 'public') { $sql = 'SELECT COUNT(*) as "0" FROM '. $this->getTableName(); $sql.= ' WHERE uId = ' . intval($uId); - switch ($range) { + switch ($status) { case 'all': //no constraints break; @@ -425,7 +439,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * @param string $title Bookmark title * @param string $description Long bookmark description * @param string $privateNote Private note for the user. - * @param string $status Bookmark visibility: + * @param string $status Bookmark visibility / privacy settings: * 0 - public * 1 - shared * 2 - private @@ -453,14 +467,6 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $address = $this->normalize($address); - if (getenv('HTTP_CLIENT_IP')) { - $ip = getenv('HTTP_CLIENT_IP'); - } else if (getenv('REMOTE_ADDR')) { - $ip = getenv('REMOTE_ADDR'); - } else { - $ip = getenv('HTTP_X_FORWARDED_FOR'); - } - /* * Note that if date is NULL, then it's added with a date and * time of now, and if it's present, @@ -480,7 +486,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService // Set up the SQL insert statement and execute it. $values = array( 'uId' => intval($sId), - 'bIp' => $ip, + 'bIp' => SemanticScuttle_Model_RemoteUser::getIp(), 'bDatetime' => $datetime, 'bModified' => $datetime, 'bTitle' => $title, @@ -548,7 +554,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * @param string $title Bookmark title * @param string $description Long bookmark description * @param string $privateNote Private note for the user. - * @param string $status Bookmark visibility: + * @param string $status Bookmark visibility / privacy setting: * 0 - public * 1 - shared * 2 - private @@ -570,15 +576,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService return false; } - // Get the client's IP address and the date; note that the date is in GMT. - if (getenv('HTTP_CLIENT_IP')) - $ip = getenv('HTTP_CLIENT_IP'); - else - if (getenv('REMOTE_ADDR')) - $ip = getenv('REMOTE_ADDR'); - else - $ip = getenv('HTTP_X_FORWARDED_FOR'); - + // Get the the date; note that the date is in GMT. $moddatetime = gmdate('Y-m-d H:i:s', time()); $address = $this->normalize($address); @@ -589,7 +587,11 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService if ($bookmark['bAddress'] != $address && $this->bookmarkExists($address, $bookmark['uId']) ) { - message_die(GENERAL_ERROR, 'Could not update bookmark (URL already existing = '.$address.')', '', __LINE__, __FILE__); + message_die( + GENERAL_ERROR, + 'Could not update bookmark (URL already exists: ' . $address . ')', + '', __LINE__, __FILE__ + ); return false; } @@ -611,25 +613,33 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService if (!is_null($date)) { $datetime = gmdate('Y-m-d H:i:s', strtotime($date)); - $updates[] = array('bDateTime' => $datetime); + $updates['bDatetime'] = $datetime; } - $sql = 'UPDATE '. $GLOBALS['tableprefix'] .'bookmarks SET '. $this->db->sql_build_array('UPDATE', $updates) .' WHERE bId = '. intval($bId); + $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); + message_die( + GENERAL_ERROR, 'Could not update bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); } - $uriparts = explode('.', $address); + $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); + message_die( + GENERAL_ERROR, 'Could not update bookmark', + '', __LINE__, __FILE__, $sql, $this->db + ); } $this->db->sql_transaction('commit'); @@ -724,7 +734,8 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService if (SQL_LAYER == 'mysql4') { $query_1 .= 'SQL_CALC_FOUND_ROWS '; } - $query_1 .= 'B.*, U.'. $userservice->getFieldName('username'); + $query_1 .= 'B.*, U.'. $userservice->getFieldName('username') + . ', U.name'; $query_2 = ' FROM '. $userservice->getTableName() .' AS U' . ', '. $this->getTableName() .' AS B'; @@ -745,7 +756,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $arrWatch = $userservice->getWatchlist($user); if (count($arrWatch) > 0) { $query_3_1 = ''; - foreach($arrWatch as $row) { + foreach ($arrWatch as $row) { $query_3_1 .= 'B.uId = '. intval($row) .' OR '; } $query_3_1 = substr($query_3_1, 0, -3); @@ -756,7 +767,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService } $query_5 = ''; - if($hash == null) { + if ($hash == null) { $query_5.= ' GROUP BY B.bHash'; } @@ -804,7 +815,9 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $query_2 .= ', '. $b2tservice->getTableName() .' AS T'. $i; $query_4 .= ' AND ('; - $allLinkedTags = $tag2tagservice->getAllLinkedTags($this->db->sql_escape($tags[$i]), '>', $user); + $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) .'"'; @@ -825,7 +838,8 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService // 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'; + $query_2 .= ' LEFT JOIN '. $b2tservice->getTableName() .' AS T' + . ' ON B.bId = T.bId'; $dotags = true; } else { $dotags = false; @@ -833,12 +847,24 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $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]) .'%"'; - $query_4 .= ' OR B.bPrivateNote 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 U.username = "'. $this->db->sql_escape($aTerms[$i]) .'"'; //exact match for username + $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 .= ' OR T.tag LIKE "' + . $this->db->sql_escape($aTerms[$i]) + . '%"'; } $query_4 .= ')'; } @@ -860,22 +886,35 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $query = $query_1 . $query_2 . $query_3 . $query_4 . $query_5; - if (!($dbresult = & $this->db->sql_query_limit($query, intval($perpage), intval($start)))) { - message_die(GENERAL_ERROR, 'Could not get bookmarks', '', __LINE__, __FILE__, $query, $this->db); + $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; + $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; + $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); + 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']; @@ -962,10 +1001,14 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService */ public function deleteBookmarksForUser($uId) { - $query = 'DELETE FROM '. $GLOBALS['tableprefix'] .'bookmarks WHERE uId = '. intval($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); + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, 'Could not delete bookmarks', + '', __LINE__, __FILE__, $query, $this->db + ); } return true; @@ -977,12 +1020,6 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * Counts the number of bookmarks that have the same address * as the given address. * - * @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. - * * @param string|array $addresses Address/URL to look for, string * of one address or array with * multiple ones @@ -991,6 +1028,12 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * 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) { diff --git a/src/SemanticScuttle/Service/Bookmark2Tag.php b/src/SemanticScuttle/Service/Bookmark2Tag.php index 4d2c969..a10cb61 100644 --- a/src/SemanticScuttle/Service/Bookmark2Tag.php +++ b/src/SemanticScuttle/Service/Bookmark2Tag.php @@ -454,58 +454,155 @@ class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService return $output; } - function &getAdminTags($limit = 30, $logged_on_user = NULL, $days = NULL) { + + + /** + * 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(); + $userservice = SemanticScuttle_Service_Factory::get('User'); + $adminIds = $userservice->getAdminIds(); // ask for their tags - return $this->getPopularTags($adminIds, $limit, $logged_on_user, $days); + return $this->getPopularTags( + $adminIds, $limit, $logged_on_user, $days, $beginsWith + ); } - function &getContactTags($user, $limit = 30, $logged_on_user = NULL, $days = NULL) { + + + + /** + * 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'); + $userservice = SemanticScuttle_Service_Factory::get('User'); $contacts = $userservice->getWatchlist($user); - // add the user (to show him/her also his/her tags) - if(!is_null($logged_on_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); + return $this->getPopularTags( + $contacts, $limit, $logged_on_user, $days, $beginsWith + ); } - // $users can be {NULL, an id, an array of id} - function &getPopularTags($user = NULL, $limit = 30, $logged_on_user = NULL, $days = NULL) { + + + /** + * 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 + ) { // Only count the tags that are visible to the current user. - if (($user != $logged_on_user) || is_null($user) || ($user === false)) - $privacy = ' AND B.bStatus = 0'; - else - $privacy = ''; + if (($user != $logged_on_user) || is_null($user) || ($user === false)) { + $privacy = ' AND B.bStatus = 0'; + } else { + $privacy = ''; + } - if (is_null($days) || !is_int($days)) - $span = ''; - else - $span = ' AND B.bDatetime > "'. date('Y-m-d H:i:s', time() - (86400 * $days)) .'"'; + $query = 'SELECT' + . ' T.tag, COUNT(T.bId) AS bCount' + . ' FROM ' + . $this->getTableName() . ' AS T' + . ', ' . $GLOBALS['tableprefix'] . 'bookmarks AS B' + . ' WHERE'; - $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'; - } elseif(is_array($user)) { + $query .= ' B.bId = T.bId AND B.bStatus = 0'; + } else if (is_array($user)) { $query .= ' (1 = 0'; //tricks - foreach($user as $u) { - $query .= ' OR B.uId = '. $this->db->sql_escape($u) .' AND B.bId = T.bId'; + foreach ($user as $u) { + if (is_numeric($u)) { + $query .= ' OR B.uId = ' . $this->db->sql_escape($u) + . ' AND B.bId = T.bId'; + } } - $query .= ' )'; + $query .= ' )' . $privacy; } else { - $query .= 'B.uId = '. $this->db->sql_escape($user) .' AND B.bId = T.bId'. $privacy; + $query .= ' B.uId = ' . $this->db->sql_escape($user) + . ' AND B.bId = T.bId' . $privacy; } - $query .= $span .' 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); + if (is_int($days)) { + $query .= ' AND B.bDatetime > "' + . date('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; } @@ -514,6 +611,8 @@ class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService 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) .'"'; @@ -592,7 +691,15 @@ class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService return $output; } - function deleteAll() { + + + /** + * 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/SearchHistory.php b/src/SemanticScuttle/Service/SearchHistory.php index df9b256..a056ce2 100644 --- a/src/SemanticScuttle/Service/SearchHistory.php +++ b/src/SemanticScuttle/Service/SearchHistory.php @@ -26,7 +26,18 @@ */ class SemanticScuttle_Service_SearchHistory extends SemanticScuttle_DbService { - var $sizeSearchHistory; + /** + * 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 @@ -44,109 +55,223 @@ class SemanticScuttle_Service_SearchHistory extends SemanticScuttle_DbService 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->tablename = $GLOBALS['tableprefix'] . 'searchhistory'; + if (isset($GLOBALS['sizeSearchHistory'])) { $this->sizeSearchHistory = $GLOBALS['sizeSearchHistory']; } else { $this->sizeSearchHistory = 10; } } - function addSearch($terms, $range, $nbResults, $uId=0) { - if(strlen($terms) == 0) { + + + /** + * 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); + $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))) { + 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); + message_die( + GENERAL_ERROR, 'Could not insert search history', + '', __LINE__, __FILE__, $sql, $this->db + ); return false; } - if($this->sizeSearchHistory != -1 && - $this->countSearches() > $this->sizeSearchHistory) { + if ($this->sizeSearchHistory != -1 + && $this->countSearches() > $this->sizeSearchHistory + ) { $this->deleteOldestSearch(); } + + return true; } - function getAllSearches($range = NULL, $uId = NULL, $nb = NULL, $start = NULL, $distinct = false, $withResults = false) { - $sql = 'SELECT DISTINCT(shTerms), shId, shRange, shNbResults, shDatetime, uId'; + + + /** + * 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) { + if ($range != null) { $sql.= ' AND shRange = "'.$range.'"'; } else { $sql.= ' AND shRange = "all"'; } - if($uId != NULL) { + if ($uId != null) { $sql.= ' AND uId = '.$uId; } - if($withResults = true) { + if ($withResults == true) { $sql.= ' AND shNbResults > 0'; } - if($distinct) { + 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); + 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)) { + while ($row = $this->db->sql_fetchrow($dbresult)) { $searches[] = $row; } $this->db->sql_freeresult($dbresult); return $searches; } - function countSearches() { + + + /** + * 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); + 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 */ - function deleteOldestSearch() { + + + /** + * 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(); - $sql.= ' ORDER BY shId ASC LIMIT 1'; // warning: here the limit is important + // 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))) { + 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); + message_die( + GENERAL_ERROR, 'Could not delete bookmarks', + '', __LINE__, __FILE__, $query, $this->db + ); return false; } + + return true; } - 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); + + /** + * 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; } - function deleteAll() { + + + /** + * 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/Tag2Tag.php b/src/SemanticScuttle/Service/Tag2Tag.php index 8666209..33d211b 100644 --- a/src/SemanticScuttle/Service/Tag2Tag.php +++ b/src/SemanticScuttle/Service/Tag2Tag.php @@ -14,7 +14,7 @@ */ /** - * SemanticScuttle tag-to-tag service. + * SemanticScuttle tag-to-tag service which works with tag relations. * * @category Bookmarking * @package SemanticScuttle @@ -102,18 +102,49 @@ class SemanticScuttle_Service_Tag2Tag extends SemanticScuttle_DbService return true; } - // Return linked tags just for admin users - function getAdminLinkedTags($tag, $relationType, $inverseRelation = false, $stopList = array()) { + + + /** + * 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(); + $adminIds = $userservice->getAdminIds(); //ask for their linked tags - return $this->getLinkedTags($tag, $relationType, $adminIds, $inverseRelation, $stopList); + return $this->getLinkedTags( + $tag, $relationType, $adminIds, $inverseRelation, $stopList + ); } - // Return the target linked tags. If inverseRelation is true, return the source linked tags. - function getLinkedTags($tag, $relationType, $uId = null, $inverseRelation = false, $stopList = array()) { + + + /** + * 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"; diff --git a/src/SemanticScuttle/Service/User.php b/src/SemanticScuttle/Service/User.php index b92abc6..5fee21d 100644 --- a/src/SemanticScuttle/Service/User.php +++ b/src/SemanticScuttle/Service/User.php @@ -211,18 +211,26 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService } } - /* Takes an numerical "id" or a string "username" - and returns the numerical "id" if the user exists else returns NULL */ - function getIdFromUser($user) { + /** + * 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) { + if ($objectUser != null) { return $objectUser->getId(); } } - return NULL; + return null; } /** @@ -404,10 +412,10 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService public function getCurrentUserId() { if (isset($_SESSION[$this->getSessionKey()])) { - return $_SESSION[$this->getSessionKey()]; + return (int)$_SESSION[$this->getSessionKey()]; } else if (isset($_COOKIE[$this->getCookieKey()])) { - $cook = split(':', $_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') . @@ -425,10 +433,10 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService if ($row = $this->db->sql_fetchrow($dbresult)) { $this->setCurrentUserId( - $row[$this->getFieldName('primary')] + (int)$row[$this->getFieldName('primary')] ); $this->db->sql_freeresult($dbresult); - return $_SESSION[$this->getSessionKey()]; + return (int)$_SESSION[$this->getSessionKey()]; } } return false; @@ -716,7 +724,7 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService } /** - * Delete all bookmarks. + * Delete all users and their watch states. * Mainly used in unit tests. * * @return void @@ -725,6 +733,9 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService { $query = 'TRUNCATE TABLE `'. $this->getTableName() .'`'; $this->db->sql_query($query); + + $query = 'TRUNCATE TABLE `' . $GLOBALS['tableprefix'] . 'watched' . '`'; + $this->db->sql_query($query); } /** diff --git a/src/SemanticScuttle/constants.php b/src/SemanticScuttle/constants.php index 95c4384..b023840 100644 --- a/src/SemanticScuttle/constants.php +++ b/src/SemanticScuttle/constants.php @@ -1,34 +1,51 @@ <?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']); - define('DEBUG_EXTRA', $GLOBALS['debugMode']); // Constant used exclusively into db/ directory +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') { - $rootTmp .= $piece .'/'; - } - } - if (($rootTmp != '/') && (substr($rootTmp, -1, 1) != '/')) { - $rootTmp .= '/'; - } - - define('ROOT', 'http://'. $_SERVER['HTTP_HOST'] . $rootTmp); + $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 .= '/'; + } + + define('ROOT', 'http://'. $_SERVER['HTTP_HOST'] . $rootTmp); } else { - define('ROOT', $GLOBALS['root']); + define('ROOT', $GLOBALS['root']); } +define('ROOT_JS', ROOT . 'js/jstree-1.0-rc2/'); // Error codes define('GENERAL_MESSAGE', 200); @@ -44,19 +61,21 @@ 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 +// 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'])); // Correct bugs with PATH_INFO (maybe for Apache 1 or CGI) -- for 1&1 host... if (isset($_SERVER['PATH_INFO']) && isset($_SERVER['ORIG_PATH_INFO'])) { - if(strlen($_SERVER["PATH_INFO"])<strlen($_SERVER["ORIG_PATH_INFO"])) { - $_SERVER["PATH_INFO"] = $_SERVER["ORIG_PATH_INFO"]; - } - if(strcasecmp($_SERVER["PATH_INFO"], $_SERVER["SCRIPT_NAME "]) == 0) { - unset($_SERVER["PATH_INFO"]); - } - if(strpos($_SERVER["PATH_INFO"], '.php') !== false) { - unset($_SERVER["PATH_INFO"]); - } + if (strlen($_SERVER["PATH_INFO"])<strlen($_SERVER["ORIG_PATH_INFO"])) { + $_SERVER["PATH_INFO"] = $_SERVER["ORIG_PATH_INFO"]; + } + if (strcasecmp($_SERVER["PATH_INFO"], $_SERVER["SCRIPT_NAME "]) == 0) { + unset($_SERVER["PATH_INFO"]); + } + if (strpos($_SERVER["PATH_INFO"], '.php') !== false) { + unset($_SERVER["PATH_INFO"]); + } } ?> diff --git a/src/SemanticScuttle/db/mysqli.php b/src/SemanticScuttle/db/mysqli.php index 03b36ea..9a2709a 100644 --- a/src/SemanticScuttle/db/mysqli.php +++ b/src/SemanticScuttle/db/mysqli.php @@ -50,7 +50,7 @@ class sql_db } } - return $this->sql_error(''); + return $this->sql_error(mysqli_connect_error()); } // diff --git a/src/SemanticScuttle/header.php b/src/SemanticScuttle/header.php index 8939b60..75e5204 100644 --- a/src/SemanticScuttle/header.php +++ b/src/SemanticScuttle/header.php @@ -14,9 +14,21 @@ * @license GPL http://www.gnu.org/licenses/gpl.html * @link http://sourceforge.net/projects/semanticscuttle */ -if (!file_exists(dirname(__FILE__) .'/../../data/config.php')) { + +if ('@data_dir@' == '@' . 'data_dir@') { + //non pear-install + $datadir = dirname(__FILE__) . '/../../data/'; +} else { + //pear installation; files are in include path + $datadir = '@data_dir@/SemanticScuttle/'; +} + +if (!file_exists($datadir . '/config.php')) { header('HTTP/1.0 500 Internal Server Error'); - die('Please copy "config.php.dist" to "config.php" in data/ folder.'); + die( + 'Please copy "config.php.dist" to "config.php" in data/ folder.' + . "\n" + ); } set_include_path( get_include_path() . PATH_SEPARATOR @@ -24,10 +36,23 @@ set_include_path( ); // 1 // First requirements part (before debug management) -$datadir = dirname(__FILE__) . '/../../data/'; require_once $datadir . '/config.default.php'; require_once $datadir . '/config.php'; +if (isset($_GET['unittestMode']) && $_GET['unittestMode'] == 1 +) { + if ($allowUnittestMode !== true) { + header('HTTP/1.0 400 Bad Request'); + die("Unittestmode is not allowed\n"); + } + + $unittestConfigFile = $datadir . '/config.unittest.php'; + if (file_exists($unittestConfigFile)) { + require_once $unittestConfigFile; + } + define('HTTP_UNIT_TEST_MODE', true); + define('UNIT_TEST_MODE', true); +} if (defined('UNIT_TEST_MODE')) { //make local config vars global - needed for unit tests //run with phpunit @@ -57,6 +82,7 @@ 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/UserArray.php'; if (count($GLOBALS['serviceoverrides']) > 0 && !defined('UNIT_TEST_MODE') @@ -106,11 +132,18 @@ $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')) { +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)) { - $httpContentType = 'text/html'; - //$httpContentType = 'application/xhtml+xml'; + 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/php-gettext/Makefile b/src/php-gettext/Makefile index 2dba911..a6cce12 100644 --- a/src/php-gettext/Makefile +++ b/src/php-gettext/Makefile @@ -1,12 +1,11 @@ PACKAGE = php-gettext-$(VERSION) -VERSION = 1.0.7 +VERSION = 1.0.10 DIST_FILES = \ gettext.php \ gettext.inc \ streams.php \ AUTHORS \ - ChangeLog \ README \ COPYING \ Makefile \ @@ -17,9 +16,14 @@ DIST_FILES = \ 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 + examples/update \ + tests/LocalesTest.php \ + tests/ParsingTest.php -dist: +check: + phpunit --verbose tests + +dist: check if [ -d $(PACKAGE) ]; then \ rm -rf $(PACKAGE); \ fi; \ @@ -30,3 +34,5 @@ dist: rm -rf $(PACKAGE); \ fi; +clean: + rm -f $(PACKAGE).tar.gz diff --git a/src/php-gettext/README b/src/php-gettext/README index c7525e2..bca4f91 100644 --- a/src/php-gettext/README +++ b/src/php-gettext/README @@ -1,9 +1,9 @@ -PHP-gettext 1.0 +PHP-gettext 1.0 (https://launchpad.net/php-gettext) -Copyright 2003, 2006 -- Danilo "angry with PHP[1]" Segan +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 +[1] PHP is actually cyrillic, and translates roughly to "works-doesn't-work" (UTF-8: Ради-Не-Ради) @@ -50,36 +50,16 @@ Features 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. - + 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 - - Plural-forms field in MO header (translation for empty string, - i.e. "") is treated according to PHP syntactic rules (it's - eval()ed). Since these should actually follow C syntax, there are - some problems. - For instance, I'm used to using this: - 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; - but it fails with PHP (it sets $plural=2 instead of 0 for $n==1). - - The fix is usually simple, but I'm lazy to go into the details of - PHP operator precedence, and maybe try to fix it. In here, I had - to put everything after the first ':' in parenthesis: - 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); - That works, and I'm satisfied. +Bugs - Besides this one, there are probably a bunch of other bugs, since - I hate PHP (did I mention it already? no? strange), and don't - know it very well. So, feel free to fix any of those and report - them back to me at <danilo@kvota.net>. + Report them on https://bugs.launchpad.net/php-gettext Usage @@ -94,19 +74,19 @@ Usage 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 + 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: + 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 + 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. @@ -118,8 +98,8 @@ Usage 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 + 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 @@ -137,20 +117,12 @@ Example There is also simple "update" script that can be used to generate POT file and to update the translation using msgmerge. -Interesting TODO: +TODO: - o Try to parse "plural-forms" header field, and to follow C syntax - rules. This won't be easy. + o Improve speed to be even more comparable to the native gettext + implementation. -Boring TODO: - - o Learn PHP and fix bugs, slowness and other stuff resulting from - my lack of knowledge (but *maybe*, it's not my knowledge that is - bad, but PHP itself ;-). - - (This is mostly done thanks to Nico Kaiser.) - - o Try to use hash tables in MO files: with pre-loading, would it + o Try to use hash tables in MO files: with pre-loading, would it be useful at all? Never-asked-questions: @@ -160,7 +132,7 @@ Never-asked-questions: 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 + 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 @@ -173,7 +145,7 @@ Never-asked-questions: 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). 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 differindex 6ffccfd..497c883 100644 --- a/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.mo +++ 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 index 7e620cc..e5da0e9 100644 --- a/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.po +++ b/src/php-gettext/examples/locale/sr_CS/LC_MESSAGES/messages.po @@ -12,7 +12,8 @@ msgstr "" "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" +"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 "" diff --git a/src/php-gettext/examples/pigs_dropin.php b/src/php-gettext/examples/pigs_dropin.php index edd2b0d..94fd850 100644 --- a/src/php-gettext/examples/pigs_dropin.php +++ b/src/php-gettext/examples/pigs_dropin.php @@ -1,6 +1,6 @@ <?php /* - Copyright (c) 2003,2004,2005 Danilo Segan <danilo@kvota.net>. + 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. @@ -21,10 +21,12 @@ */ +error_reporting(E_ALL | E_STRICT); + // define constants -define(PROJECT_DIR, realpath('./')); -define(LOCALE_DIR, PROJECT_DIR .'/locale'); -define(DEFAULT_LOCALE, 'en_US'); +define('PROJECT_DIR', realpath('./')); +define('LOCALE_DIR', PROJECT_DIR .'/locale'); +define('DEFAULT_LOCALE', 'en_US'); require_once('../gettext.inc'); diff --git a/src/php-gettext/examples/pigs_fallback.php b/src/php-gettext/examples/pigs_fallback.php index b50f752..353190d 100644 --- a/src/php-gettext/examples/pigs_fallback.php +++ b/src/php-gettext/examples/pigs_fallback.php @@ -1,6 +1,6 @@ <?php /* - Copyright (c) 2003,2004,2005 Danilo Segan <danilo@kvota.net>. + 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. @@ -21,10 +21,12 @@ */ +error_reporting(E_ALL | E_STRICT); + // define constants -define(PROJECT_DIR, realpath('./')); -define(LOCALE_DIR, PROJECT_DIR .'/locale'); -define(DEFAULT_LOCALE, 'en_US'); +define('PROJECT_DIR', realpath('./')); +define('LOCALE_DIR', PROJECT_DIR .'/locale'); +define('DEFAULT_LOCALE', 'en_US'); require_once('../gettext.inc'); diff --git a/src/php-gettext/examples/update b/src/php-gettext/examples/update index c8d8b61..76b4308 100644 --- a/src/php-gettext/examples/update +++ b/src/php-gettext/examples/update @@ -1,7 +1,7 @@ #!/bin/sh TEMPLATE=pigs.pot -xgettext -kT_ngettext:1,2 -kT_ -L PHP -o $TEMPLATE pigs.php -if [ x$1 == 'x-p' ]; then +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 diff --git a/src/php-gettext/gettext.inc b/src/php-gettext/gettext.inc index a67811f..399a0f2 100644 --- a/src/php-gettext/gettext.inc +++ b/src/php-gettext/gettext.inc @@ -1,9 +1,10 @@ <?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 @@ -22,15 +23,22 @@ */ /* -LC_CTYPE 0 -LC_NUMERIC 1 -LC_TIME 2 -LC_COLLATE 3 -LC_MONETARY 4 -LC_MESSAGES 5 -LC_ALL 6 +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'); @@ -44,29 +52,96 @@ $LC_CATEGORIES = array('LC_CTYPE', 'LC_NUMERIC', 'LC_TIME', 'LC_COLLATE', 'LC_MO $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); - $p = isset($text_domains[$domain]->path) ? $text_domains[$domain]->path : './'; - $path = $p . "$locale/". $LC_CATEGORIES[$category] ."/$domain.mo"; - if (file_exists($path)) { - $input = new FileReader($path); - } - else { - $input = null; - } - $text_domains[$domain]->l10n = new gettext_reader($input, $enable_cache); - } - return $text_domains[$domain]->l10n; + 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; } /** @@ -80,8 +155,10 @@ function locale_emulation() { /** * Checks if the current locale is supported on this system. */ -function _check_locale() { +function _check_locale_and_function($function=false) { global $EMULATEGETTEXT; + if ($function and !function_exists($function)) + return false; return !$EMULATEGETTEXT; } @@ -89,56 +166,70 @@ function _check_locale() { * 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'); + 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; - } + $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 != '') + if ($CURRENTLOCALE != '') return $CURRENTLOCALE; - else + 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 { - $ret = 0; - if (function_exists('setlocale')) // I don't know if this ever happens ;) - $ret = @setlocale($category, $locale); //the @ hides warning messages on few installations - if (($ret and $locale == '') or ($ret == $locale)) { - $EMULATEGETTEXT = 0; + 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 { - if ($locale == '') // emulate variable support - $CURRENTLOCALE = getenv('LANG'); - else - $CURRENTLOCALE = $locale; - $EMULATEGETTEXT = 1; + // 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; } } @@ -147,135 +238,240 @@ function _setlocale($category, $locale) { * Sets the path for a domain. */ function _bindtextdomain($domain, $path) { - global $text_domains; - // ensure $path ends with a slash - if ($path[strlen($path) - 1] != '/') $path .= '/'; - elseif ($path[strlen($path) - 1] != '\\') $path .= '\\'; - $text_domains[$domain]->path = $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; + global $text_domains; + $text_domains[$domain]->codeset = $codeset; } /** * Sets the default domain. */ function _textdomain($domain) { - global $default_domain; - $default_domain = $domain; + global $default_domain; + $default_domain = $domain; } /** * Lookup a message in the current domain. */ function _gettext($msgid) { - $l10n = _get_reader(); - //return $l10n->translate($msgid); - return _encode($l10n->translate($msgid)); + $l10n = _get_reader(); + return _encode($l10n->translate($msgid)); } + /** * Alias for gettext. */ function __($msgid) { - return _gettext($msgid); + return _gettext($msgid); } + /** * Plural version of gettext. */ function _ngettext($single, $plural, $number) { - $l10n = _get_reader(); - //return $l10n->ngettext($single, $plural, $number); - return _encode($l10n->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 $l10n->translate($msgid); - return _encode($l10n->translate($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 $l10n->ngettext($single, $plural, $number); - return _encode($l10n->ngettext($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 $l10n->translate($msgid); - return _encode($l10n->translate($msgid)); + $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 $l10n->ngettext($single, $plural, $number); - return _encode($l10n->ngettext($single, $plural, $number)); + $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. + +// 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()) return bindtextdomain($domain, $path); - else return _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_exists('bind_textdomain_codeset')) return bind_textdomain_codeset($domain, $codeset); - else return _bind_textdomain_codeset($domain, $codeset); + 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()) return textdomain($domain); - else return _textdomain($domain); + if (_check_locale_and_function()) return textdomain($domain); + else return _textdomain($domain); } function T_gettext($msgid) { - if (_check_locale()) return gettext($msgid); - else return _gettext($msgid); + if (_check_locale_and_function()) return gettext($msgid); + else return _gettext($msgid); } function T_($msgid) { - if (_check_locale()) return _($msgid); - return __($msgid); + if (_check_locale_and_function()) return _($msgid); + return __($msgid); } function T_ngettext($single, $plural, $number) { - if (_check_locale()) return ngettext($single, $plural, $number); - else return _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()) return dgettext($domain, $msgid); - else return _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()) return dngettext($domain, $single, $plural, $number); - else return _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()) return dcgettext($domain, $msgid, $category); - else return _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()) return dcngettext($domain, $single, $plural, $number, $category); - else return _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); } @@ -283,36 +479,56 @@ function T_dcngettext($domain, $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 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 index 978794c..a121f9c 100644 --- a/src/php-gettext/gettext.php +++ b/src/php-gettext/gettext.php @@ -1,8 +1,8 @@ <?php /* - Copyright (c) 2003 Danilo Segan <danilo@kvota.net>. + 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 @@ -20,13 +20,13 @@ 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 @@ -36,7 +36,7 @@ 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; @@ -52,27 +52,33 @@ class gettext_reader { /* Methods */ - - + + /** * Reads a 32bit Integer from the Stream - * + * * @access private * @return Integer from the Stream */ function readint() { if ($this->BYTEORDER == 0) { // low endian - return array_shift(unpack('V', $this->STREAM->read(4))); + $input=unpack('V', $this->STREAM->read(4)); + return array_shift($input); } else { // big endian - return array_shift(unpack('N', $this->STREAM->read(4))); + $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 */ @@ -85,10 +91,10 @@ class gettext_reader { 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) */ @@ -98,39 +104,37 @@ class gettext_reader { $this->short_circuit = true; return; } - + // Caching can be turned off $this->enable_cache = $enable_cache; - // $MAGIC1 = (int)0x950412de; //bug in PHP 5 - $MAGIC1 = (int) - 1794895138; - // $MAGIC2 = (int)0xde120495; //bug - $MAGIC2 = (int) - 569244523; + $MAGIC1 = "\x95\x04\x12\xde"; + $MAGIC2 = "\xde\x12\x04\x95"; $this->STREAM = $Reader; - $magic = $this->readint(); - if ($magic == $MAGIC1 || $magic == ($MAGIC1 & 0xFFFFFFFF)) { - $this->BYTEORDER = 0; - } elseif ($magic == $MAGIC2 || $magic == ($MAGIC2 & 0xFFFFFFFF)) { + $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() { @@ -138,13 +142,17 @@ class gettext_reader { is_array($this->table_originals) && is_array($this->table_translations)) return; - + /* get original and translations tables */ - $this->STREAM->seekto($this->originals); - $this->table_originals = $this->readintarray($this->total * 2); - $this->STREAM->seekto($this->translations); - $this->table_translations = $this->readintarray($this->total * 2); - + 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 */ @@ -157,10 +165,10 @@ class gettext_reader { } } } - + /** * Returns a string from the "originals" table - * + * * @access private * @param int num Offset number of original string * @return string Requested string if found, otherwise '' @@ -174,10 +182,10 @@ class gettext_reader { $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 '' @@ -191,10 +199,10 @@ class gettext_reader { $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) @@ -232,10 +240,10 @@ class gettext_reader { 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) @@ -243,8 +251,8 @@ class gettext_reader { function translate($string) { if ($this->short_circuit) return $string; - $this->load_tables(); - + $this->load_tables(); + if ($this->enable_cache) { // Caching enabled, get translated string from cache if (array_key_exists($string, $this->cache_translations)) @@ -262,16 +270,65 @@ class gettext_reader { } /** + * 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 + // 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) { @@ -279,18 +336,15 @@ class gettext_reader { } else { $header = $this->get_translation_string(0); } - if (eregi("plural-forms: ([^\n]*)\n", $header, $regs)) - $expr = $regs[1]; - else - $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; - $this->pluralheader = $expr; + $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 @@ -300,7 +354,7 @@ class gettext_reader { $string = str_replace('nplurals',"\$total",$string); $string = str_replace("n",$n,$string); $string = str_replace('plural',"\$plural",$string); - + $total = 0; $plural = 0; @@ -311,7 +365,7 @@ class gettext_reader { /** * Plural version of gettext - * + * * @access public * @param string single * @param string plural @@ -327,12 +381,12 @@ class gettext_reader { } // find out the appropriate form - $select = $this->select_string($number); - + $select = $this->select_string($number); + // this should contains all strings separated by NULLs - $key = $single.chr(0).$plural; - - + $key = $single . chr(0) . $plural; + + if ($this->enable_cache) { if (! array_key_exists($key, $this->cache_translations)) { return ($number != 1) ? $plural : $single; @@ -353,6 +407,15 @@ class gettext_reader { } } + 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 index 4237de1..3cdc158 100644 --- a/src/php-gettext/streams.php +++ b/src/php-gettext/streams.php @@ -1,6 +1,6 @@ <?php /* - Copyright (c) 2003, 2005 Danilo Segan <danilo@kvota.net>. + Copyright (c) 2003, 2005, 2006, 2009 Danilo Segan <danilo@kvota.net>. This file is part of PHP-gettext. @@ -21,29 +21,29 @@ */ -// Simple class to wrap file streams, string streams, etc. -// seek is essential, and it should be byte stream + // 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; @@ -78,7 +78,7 @@ class StringReader { return strlen($this->_str); } -} +}; class FileReader { @@ -93,8 +93,8 @@ class FileReader { $this->_pos = 0; $this->_fd = fopen($filename,'rb'); if (!$this->_fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; + $this->error = 3; // Cannot read file, probably permissions + return false; } } else { $this->error = 2; // File doesn't exist @@ -115,7 +115,7 @@ class FileReader { $bytes -= strlen($chunk); } $this->_pos = ftell($this->_fd); - + return $data; } else return ''; } @@ -138,9 +138,9 @@ class FileReader { fclose($this->_fd); } -} +}; -// Preloads entire file in memory first, then creates a StringReader +// 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) { @@ -150,8 +150,8 @@ class CachedFileReader extends StringReader { $fd = fopen($filename,'rb'); if (!$fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; + $this->error = 3; // Cannot read file, probably permissions + return false; } $this->_str = fread($fd, $length); fclose($fd); @@ -161,7 +161,7 @@ class CachedFileReader extends StringReader { 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" + )); + + } + +} +?> |