From da20b90bbaafef116c7a5d439e44210b126c3522 Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:08:02 +0000 Subject: [PATCH 01/16] update php-gettext to 1.0.10 git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@763 b3834d28-1941-0410-a4f8-b48e95affb8f --- src/php-gettext/Makefile | 14 +- src/php-gettext/README | 74 +-- .../locale/sr_CS/LC_MESSAGES/messages.mo | Bin 834 -> 829 bytes .../locale/sr_CS/LC_MESSAGES/messages.po | 3 +- src/php-gettext/examples/pigs_dropin.php | 10 +- src/php-gettext/examples/pigs_fallback.php | 10 +- src/php-gettext/examples/update | 4 +- src/php-gettext/gettext.inc | 480 +++++++++++++----- src/php-gettext/gettext.php | 179 ++++--- src/php-gettext/streams.php | 32 +- src/php-gettext/tests/LocalesTest.php | 66 +++ src/php-gettext/tests/ParsingTest.php | 43 ++ 12 files changed, 643 insertions(+), 272 deletions(-) create mode 100644 src/php-gettext/tests/LocalesTest.php create mode 100644 src/php-gettext/tests/ParsingTest.php 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. - - 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 . + 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 index 6ffccfd52d91a1db0870a94d12472e104f9c6b2b..497c8830ca84f880f8d484df057c0bf725163edd 100644 GIT binary patch delta 44 xcmX@awwG;!iRd~;28L}63=G?WIF*TkK^{oUZuC6FIGK-0X|f@c>Ev{#+W_V;3U~kj delta 51 zcmdnXc8G0)iRflV28L}63=G?WIE#saK^{meZS*|E$fTe!nTtu8QDd?$lPRO-=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 @@ . + Copyright (c) 2003,2004,2005,2009 Danilo Segan . Copyright (c) 2005,2006 Steven Armstrong 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 @@ . + Copyright (c) 2003,2004,2005,2009 Danilo Segan . Copyright (c) 2005,2006 Steven Armstrong 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 @@ - + Copyright (c) 2009 Danilo Segan + 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[a-z]{2,3})" // language code + ."(?:_(?P[A-Z]{2}))?" // country code + ."(?:\.(?P[-A-Za-z0-9_]+))?" // charset + ."(?:@(?P[-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; - $CURRENTLOCALE = $ret; - } else { - if ($locale == '') // emulate variable support - $CURRENTLOCALE = getenv('LANG'); - else - $CURRENTLOCALE = $locale; + 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; } } @@ -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 @@ . + Copyright (c) 2003, 2009 Danilo Segan . Copyright (c) 2005 Nico Kaiser - + 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)) @@ -261,17 +269,66 @@ 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 @@ . + Copyright (c) 2003, 2005, 2006, 2009 Danilo Segan . 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 @@ +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 @@ +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" + )); + + } + +} +?> From f20135d16a0fa2bb8cd2820323052fccab8f3451 Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:08:58 +0000 Subject: [PATCH 02/16] add gettext changelog entry git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@764 b3834d28-1941-0410-a4f8-b48e95affb8f --- doc/ChangeLog | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/ChangeLog b/doc/ChangeLog index 2633fc8..e2562e4 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -6,6 +6,7 @@ ChangeLog for SemantiScuttle - Fix bug getTagsForBookmarks() that fetched all tags - Show error message on mysqli connection errors - Implement patch #3059829: update FR_CA translation +- Update php-gettext library to 1.0.10 - Fix bug #3073215: Updating bookmark time does not work - Fix bug #3074816: French translation breaks edit javascript From b9256b14377251e6303d6abbe3cd7e7edc2583a4 Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:09:58 +0000 Subject: [PATCH 03/16] add header to httpauth.inc.php git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@765 b3834d28-1941-0410-a4f8-b48e95affb8f --- www/api/httpauth.inc.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/www/api/httpauth.inc.php b/www/api/httpauth.inc.php index 0e3a66d..ee5c7f2 100644 --- a/www/api/httpauth.inc.php +++ b/www/api/httpauth.inc.php @@ -1,10 +1,29 @@ + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ require_once '../www-header.php'; -// Provides HTTP Basic authentication of a user -// and logs the user in if necessary - -function authenticate() { +/** + * Sends HTTP auth headers to the browser + */ +function authenticate() +{ header('WWW-Authenticate: Basic realm="SemanticScuttle API"'); header('HTTP/1.0 401 Unauthorized'); @@ -26,7 +45,9 @@ if (!$userservice->isLoggedOn()) { if (!isset($_SERVER['PHP_AUTH_USER'])) { authenticate(); } else { - $login = $userservice->login($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']); + $login = $userservice->login( + $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] + ); if ($login) { $currentUser = $userservice->getCurrentObjectUser(); } else { From 6b3f1d476ea0d1be46b3e4a7df6983320865303b Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:10:53 +0000 Subject: [PATCH 04/16] update docblocks git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@766 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/TestBaseApi.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/TestBaseApi.php b/tests/TestBaseApi.php index 645ead9..eb1457f 100644 --- a/tests/TestBaseApi.php +++ b/tests/TestBaseApi.php @@ -29,6 +29,16 @@ class TestBaseApi extends TestBase protected $url; protected $urlPart = null; + /** + * @var SemanticScuttle_Service_User + */ + protected $us; + + /** + * @var SemanticScuttle_Service_Bookmark + */ + protected $bs; + protected function setUp() @@ -52,11 +62,14 @@ class TestBaseApi extends TestBase /** - * Gets a HTTP request object + * Gets a HTTP request object. + * Uses $this->url plus $urlSuffix as request URL. * * @param string $urlSuffix Suffix for the URL * * @return HTTP_Request2 HTTP request object + * + * @uses $url */ protected function getRequest($urlSuffix = null) { @@ -71,13 +84,17 @@ class TestBaseApi extends TestBase /** - * Gets a HTTP request object + * Creates a user and a HTTP request object and prepares + * the request object with authentication details, so that + * the user is logged in. * * @param string $urlSuffix Suffix for the URL * @param mixed $auth If user authentication is needed (true/false) * or array with username and password * * @return array(HTTP_Request2, integer) HTTP request object and user id + * + * @uses getRequest() */ protected function getAuthRequest($urlSuffix = null, $auth = true) { From b31886b67aaa814dc0ffe48bb7d5c4863de0106e Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:11:59 +0000 Subject: [PATCH 05/16] tests for deleting bookmarks via the API. two of them fail currently because of a security issue git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@767 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/Api/PostsDeleteTest.php | 302 ++++++++++++++++++++++++++++++++++ 1 file changed, 302 insertions(+) create mode 100644 tests/Api/PostsDeleteTest.php diff --git a/tests/Api/PostsDeleteTest.php b/tests/Api/PostsDeleteTest.php new file mode 100644 index 0000000..705f94e --- /dev/null +++ b/tests/Api/PostsDeleteTest.php @@ -0,0 +1,302 @@ + + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once dirname(__FILE__) . '/../prepare.php'; +require_once 'HTTP/Request2.php'; + +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Api_PostsDeleteTest::main'); +} + +/** + * Unit tests for the SemanticScuttle post deletion API. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class Api_PostsDeleteTest extends TestBaseApi +{ + protected $urlPart = 'api/posts/delete'; + + + + /** + * Used to run this test class standalone + * + * @return void + */ + public static function main() + { + require_once 'PHPUnit/TextUI/TestRunner.php'; + PHPUnit_TextUI_TestRunner::run( + new PHPUnit_Framework_TestSuite(__CLASS__) + ); + } + + + + /** + * Test if authentication is required when sending no auth data + */ + public function testAuthWithoutAuthData() + { + $req = $this->getRequest(null, false); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * Test if authentication is required when sending wrong user data + + */ + public function testAuthWrongCredentials() + { + $req = $this->getRequest(null, false); + $req->setAuth('user', 'password', HTTP_Request2::AUTH_BASIC); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * Test if deleting an own bookmark works. + */ + public function testDeleteOwnBookmark() + { + $this->bs->deleteAll(); + + $bookmarkUrl = 'http://example.org/tag-1'; + + list($req, $uId) = $this->getAuthRequest( + '?url=' . urlencode($bookmarkUrl) + ); + + $bId = $this->addBookmark( + $uId, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + //user has one bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + + //send request + $res = $req->send(); + + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //bookmark should be deleted now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + } + + + + /** + * Test if deleting an own bookmark via POST works. + */ + public function testDeleteOwnBookmarkPost() + { + $this->bs->deleteAll(); + + $bookmarkUrl = 'http://example.org/tag-1'; + + list($req, $uId) = $this->getAuthRequest(); + + $bId = $this->addBookmark( + $uId, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + //user has one bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + + //send request + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $bookmarkUrl); + $res = $req->send(); + + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //bookmark should be deleted now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + } + + + + /** + * Verify that deleting a bookmark of a different does not work + */ + public function testDeleteOtherBookmark() + { + $this->bs->deleteAll(); + + $bookmarkUrl = 'http://example.org/tag-1'; + + list($req, $uId) = $this->getAuthRequest( + '?url=' . urlencode($bookmarkUrl) + ); + $uId2 = $this->addUser(); + + $bId = $this->addBookmark( + $uId2, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + //user 1 has no bookmarks + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + //user 2 has one bookmark + $data = $this->bs->getBookmarks(0, null, $uId2); + $this->assertEquals(1, $data['total']); + + //send request + $res = $req->send(); + + //401 - unauthorized + $this->assertEquals(401, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertNotTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + '', false + ); + + //bookmark should still be there + $data = $this->bs->getBookmarks(0, null, $uId2); + $this->assertEquals(1, $data['total']); + } + + + + /** + * Test if deleting a bookmark works that also other users + * bookmarked. + */ + public function testDeleteBookmarkOneOfTwo() + { + $this->bs->deleteAll(); + + $bookmarkUrl = 'http://example.org/tag-1'; + + list($req, $uId) = $this->getAuthRequest( + '?url=' . urlencode($bookmarkUrl) + ); + $uId2 = $this->addUser(); + $uId3 = $this->addUser(); + + //important: the order of addition is crucial here + $this->addBookmark( + $uId2, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + $bId = $this->addBookmark( + $uId, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + $this->addBookmark( + $uId3, $bookmarkUrl, 0, + array('unittest', 'tag1') + ); + + //user one and two have a bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $data = $this->bs->getBookmarks(0, null, $uId2); + $this->assertEquals(1, $data['total']); + + //send request + $res = $req->send(); + + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + '', false + ); + + //bookmark should be deleted now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + //user 2 should still have his + $data = $this->bs->getBookmarks(0, null, $uId2); + $this->assertEquals(1, $data['total']); + //user 3 should still have his, too + $data = $this->bs->getBookmarks(0, null, $uId3); + $this->assertEquals(1, $data['total']); + } + +} + +if (PHPUnit_MAIN_METHOD == 'Api_PostsDeleteTest::main') { + Api_PostsDeleteTest::main(); +} +?> \ No newline at end of file From df8216d607a9806b57b83eea9eb55577eae7d54f Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:12:55 +0000 Subject: [PATCH 06/16] execute api tests git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@768 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/AllTests.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/AllTests.php b/tests/AllTests.php index d29de7f..61e1a57 100644 --- a/tests/AllTests.php +++ b/tests/AllTests.php @@ -64,6 +64,8 @@ class AllTests extends PHPUnit_Framework_TestSuite $suite->addTestFile($tdir . '/TagTest.php'); $suite->addTestFile($tdir . '/VoteTest.php'); $suite->addTestFile($tdir . '/UserTest.php'); + $suite->addTestFile($tdir . '/Api/ExportCsvTest.php'); + $suite->addTestFile($tdir . '/Api/PostsDeleteTest.php'); return $suite; } From 22c9a01ee845d2b92fcab6b6cb10ac6ff0eec52e Mon Sep 17 00:00:00 2001 From: cweiske Date: Tue, 28 Sep 2010 22:14:31 +0000 Subject: [PATCH 07/16] rewrite api/posts/delete to be more secure and add unit tests for it git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@769 b3834d28-1941-0410-a4f8-b48e95affb8f --- src/SemanticScuttle/Service/Bookmark.php | 18 ++++--- tests/Api/PostsDeleteTest.php | 9 ++-- www/api/posts_delete.php | 63 +++++++++++++++++------- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/src/SemanticScuttle/Service/Bookmark.php b/src/SemanticScuttle/Service/Bookmark.php index dde1df5..4e18d3f 100644 --- a/src/SemanticScuttle/Service/Bookmark.php +++ b/src/SemanticScuttle/Service/Bookmark.php @@ -176,7 +176,10 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService * Retrieves a bookmark with the given URL. * DOES NOT RESPECT PRIVACY SETTINGS! * - * @param string $address URL to get bookmarks for + * @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). @@ -184,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); } @@ -195,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); } diff --git a/tests/Api/PostsDeleteTest.php b/tests/Api/PostsDeleteTest.php index 705f94e..626746f 100644 --- a/tests/Api/PostsDeleteTest.php +++ b/tests/Api/PostsDeleteTest.php @@ -202,8 +202,9 @@ class Api_PostsDeleteTest extends TestBaseApi //send request $res = $req->send(); - //401 - unauthorized - $this->assertEquals(401, $res->getStatus()); + //404 - user does not have that bookmark + $this->assertEquals(404, $res->getStatus()); + //verify MIME content type $this->assertEquals( 'text/xml; charset=utf-8', @@ -211,10 +212,10 @@ class Api_PostsDeleteTest extends TestBaseApi ); //verify xml - $this->assertNotTag( + $this->assertTag( array( 'tag' => 'result', - 'attributes' => array('code' => 'done') + 'attributes' => array('code' => 'something went wrong') ), $res->getBody(), '', false diff --git a/www/api/posts_delete.php b/www/api/posts_delete.php index a63cc62..982b686 100644 --- a/www/api/posts_delete.php +++ b/www/api/posts_delete.php @@ -1,33 +1,60 @@ + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ // Force HTTP authentication first! $httpContentType = 'text/xml'; require_once 'httpauth.inc.php'; -/* Service creation: only useful services are created */ -$bookmarkservice =SemanticScuttle_Service_Factory::get('Bookmark'); +$bs = SemanticScuttle_Service_Factory::get('Bookmark'); +$uId = $userservice->getCurrentUserId(); -// Note that del.icio.us only errors out if no URL was passed in; there's no error on attempting -// to delete a bookmark you don't have. - // Error out if there's no address -if (is_null($_REQUEST['url'])) { +if (!isset($_REQUEST['url']) + || $_REQUEST['url'] == '' +) { $deleted = false; +} else if (!$bs->bookmarkExists($_REQUEST['url'], $uId)) { + //the user does not have such a bookmark + // Note that del.icio.us only errors out if no URL was passed in; + // there's no error on attempting to delete a bookmark you don't have. + // this sucks, and I don't care about being different but correct here. + header('HTTP/1.0 404 Not Found'); + $deleted = false; + } else { - $bookmark = $bookmarkservice->getBookmarkByAddress($_REQUEST['url']); - $bid = $bookmark['bId']; - $delete = $bookmarkservice->deleteBookmark($bid); - $deleted = true; + $bookmark = $bs->getBookmarkByAddress($_REQUEST['url'], false); + $bId = $bookmark['bId']; + $deleted = $bs->deleteBookmark($bId); + if (!$deleted) { + //something really went wrong + header('HTTP/1.0 500 Internal Server Error'); + } } // Set up the XML file and output the result. -echo '\r\n"; -echo ''; +echo '\r\n"; +echo ''; ?> \ No newline at end of file From 70c39a8eea7896271c0ad3f0c435ec06c64074d1 Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:49:14 +0000 Subject: [PATCH 08/16] delicious returns a proper error message when deleting non-existant items, which we do now, too git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@770 b3834d28-1941-0410-a4f8-b48e95affb8f --- doc/developers/api | 10 ++++++++++ tests/Api/PostsDeleteTest.php | 2 +- www/api/posts_delete.php | 14 +++++--------- 3 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 doc/developers/api diff --git a/doc/developers/api b/doc/developers/api new file mode 100644 index 0000000..efa05fe --- /dev/null +++ b/doc/developers/api @@ -0,0 +1,10 @@ +SemanticScuttle API +=================== + +SemanticScuttle tries to implement the delicious API v1 as closely as sensible. + +Where it makes sense and the delicious API just does things plainly wrong +(i.e. when returning a wrong status code on an error), we do it better. + +- http://www.delicious.com/help/api +- http://support.delicious.com/forum/comments.php?DiscussionID=5286&page=1 diff --git a/tests/Api/PostsDeleteTest.php b/tests/Api/PostsDeleteTest.php index 626746f..d9fb6cd 100644 --- a/tests/Api/PostsDeleteTest.php +++ b/tests/Api/PostsDeleteTest.php @@ -215,7 +215,7 @@ class Api_PostsDeleteTest extends TestBaseApi $this->assertTag( array( 'tag' => 'result', - 'attributes' => array('code' => 'something went wrong') + 'attributes' => array('code' => 'item not found') ), $res->getBody(), '', false diff --git a/www/api/posts_delete.php b/www/api/posts_delete.php index 982b686..03cc968 100644 --- a/www/api/posts_delete.php +++ b/www/api/posts_delete.php @@ -4,8 +4,6 @@ * The delicious API is implemented here. * * The delicious API behaves like that: - * - returns "done" even if the bookmark doesn't exist - * - we do it correctly * - does NOT allow the hash for the url parameter * - doesn't set the Content-Type to text/xml * - we do it correctly, too @@ -35,26 +33,24 @@ $uId = $userservice->getCurrentUserId(); if (!isset($_REQUEST['url']) || $_REQUEST['url'] == '' ) { - $deleted = false; + $msg = 'something went wrong'; } else if (!$bs->bookmarkExists($_REQUEST['url'], $uId)) { //the user does not have such a bookmark - // Note that del.icio.us only errors out if no URL was passed in; - // there's no error on attempting to delete a bookmark you don't have. - // this sucks, and I don't care about being different but correct here. header('HTTP/1.0 404 Not Found'); - $deleted = false; - + $msg = 'item not found'; } else { $bookmark = $bs->getBookmarkByAddress($_REQUEST['url'], false); $bId = $bookmark['bId']; $deleted = $bs->deleteBookmark($bId); + $msg = 'done'; if (!$deleted) { //something really went wrong header('HTTP/1.0 500 Internal Server Error'); + $msg = 'something really went wrong'; } } // Set up the XML file and output the result. echo '\r\n"; -echo ''; +echo ''; ?> \ No newline at end of file From 20ec8b4958dc2d6a4a08f9b0dcae27c90f0155ef Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:50:38 +0000 Subject: [PATCH 09/16] test for api/posts/update git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@771 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/AllTests.php | 1 + tests/Api/PostsUpdateTest.php | 135 ++++++++++++++++++++++++++++++++++ www/api/posts_delete.php | 1 + www/api/posts_update.php | 46 ++++++++---- 4 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 tests/Api/PostsUpdateTest.php diff --git a/tests/AllTests.php b/tests/AllTests.php index 61e1a57..799f43b 100644 --- a/tests/AllTests.php +++ b/tests/AllTests.php @@ -66,6 +66,7 @@ class AllTests extends PHPUnit_Framework_TestSuite $suite->addTestFile($tdir . '/UserTest.php'); $suite->addTestFile($tdir . '/Api/ExportCsvTest.php'); $suite->addTestFile($tdir . '/Api/PostsDeleteTest.php'); + $suite->addTestFile($tdir . '/Api/PostsUpdateTest.php'); return $suite; } diff --git a/tests/Api/PostsUpdateTest.php b/tests/Api/PostsUpdateTest.php new file mode 100644 index 0000000..c497a55 --- /dev/null +++ b/tests/Api/PostsUpdateTest.php @@ -0,0 +1,135 @@ + + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once dirname(__FILE__) . '/../prepare.php'; +require_once 'HTTP/Request2.php'; + +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Api_PostsUpdateTest::main'); +} + +/** + * Unit tests for the SemanticScuttle last-update time API. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class Api_PostsUpdateTest extends TestBaseApi +{ + protected $urlPart = 'api/posts/update'; + + + + /** + * Used to run this test class standalone + * + * @return void + */ + public static function main() + { + require_once 'PHPUnit/TextUI/TestRunner.php'; + PHPUnit_TextUI_TestRunner::run( + new PHPUnit_Framework_TestSuite(__CLASS__) + ); + } + + + + /** + * Test if authentication is required when sending no auth data + */ + public function testAuthWithoutAuthData() + { + $req = $this->getRequest(null, false); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * Test if authentication is required when sending wrong user data + + */ + public function testAuthWrongCredentials() + { + $req = $this->getRequest(null, false); + $req->setAuth('user', 'password', HTTP_Request2::AUTH_BASIC); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * See if posts/update behaves correct if there is one bookmark + */ + public function testPostUpdateOneBookmark() + { + $this->bs->deleteAll(); + + list($req, $uId) = $this->getAuthRequest(); + $bId = $this->addBookmark( + $uId, 'http://example.org/tag1', 0, + array('unittest', 'tag1') + ); + + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $bookmark = $data['bookmarks'][0]; + + //send request + $res = $req->send(); + + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'update', + 'attributes' => array( + 'inboxnew' => '0' + ) + ), + $res->getBody(), + '', false + ); + //check time + $xml = simplexml_load_string($res->getBody()); + $this->assertTrue(isset($xml['time'])); + $this->assertEquals( + strtotime($bookmark['bDatetime']), + strtotime( + (string)$xml['time'] + ) + ); + } + +} + +if (PHPUnit_MAIN_METHOD == 'Api_PostsUpdateTest::main') { + Api_PostsUpdateTest::main(); +} +?> \ No newline at end of file diff --git a/www/api/posts_delete.php b/www/api/posts_delete.php index 03cc968..69b2429 100644 --- a/www/api/posts_delete.php +++ b/www/api/posts_delete.php @@ -19,6 +19,7 @@ * @author Eric Dane * @license GPL http://www.gnu.org/licenses/gpl.html * @link http://sourceforge.net/projects/semanticscuttle + * @link http://www.delicious.com/help/api */ // Force HTTP authentication first! diff --git a/www/api/posts_update.php b/www/api/posts_update.php index 4aeedc3..4b080e2 100644 --- a/www/api/posts_update.php +++ b/www/api/posts_update.php @@ -1,24 +1,44 @@ + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + * @link http://www.delicious.com/help/api + */ // Force HTTP authentication first! $httpContentType = 'text/xml'; require_once 'httpauth.inc.php'; -/* Service creation: only useful services are created */ -$bookmarkservice =SemanticScuttle_Service_Factory::get('Bookmark'); - - -// Get the posts relevant to the passed-in variables. -$bookmarks =& $bookmarkservice->getBookmarks(0, 1, $userservice->getCurrentUserId()); +$bs = SemanticScuttle_Service_Factory::get('Bookmark'); +$bookmarks = $bs->getBookmarks(0, 1, $userservice->getCurrentUserId()); // Set up the XML file and output all the tags. -echo '\r\n"; -foreach($bookmarks['bookmarks'] as $row) { - echo ''; +echo '\r\n"; +//foreach is used in case there are no bookmarks +foreach ($bookmarks['bookmarks'] as $row) { + echo ''; } ?> \ No newline at end of file From 2f3b23f96a5f595c9d0ef02abf5037f41e907390 Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:52:01 +0000 Subject: [PATCH 10/16] first tests for api/posts/add git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@772 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/AllTests.php | 1 + tests/Api/PostsAddTest.php | 197 +++++++++++++++++++++++++++++++++++++ www/api/posts_add.php | 2 + 3 files changed, 200 insertions(+) create mode 100644 tests/Api/PostsAddTest.php diff --git a/tests/AllTests.php b/tests/AllTests.php index 799f43b..ded6824 100644 --- a/tests/AllTests.php +++ b/tests/AllTests.php @@ -65,6 +65,7 @@ class AllTests extends PHPUnit_Framework_TestSuite $suite->addTestFile($tdir . '/VoteTest.php'); $suite->addTestFile($tdir . '/UserTest.php'); $suite->addTestFile($tdir . '/Api/ExportCsvTest.php'); + $suite->addTestFile($tdir . '/Api/PostsAddTest.php'); $suite->addTestFile($tdir . '/Api/PostsDeleteTest.php'); $suite->addTestFile($tdir . '/Api/PostsUpdateTest.php'); return $suite; diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php new file mode 100644 index 0000000..1db8af9 --- /dev/null +++ b/tests/Api/PostsAddTest.php @@ -0,0 +1,197 @@ + + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +require_once dirname(__FILE__) . '/../prepare.php'; +require_once 'HTTP/Request2.php'; + +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Api_PostsAddTest::main'); +} + +/** + * Unit tests for the SemanticScuttle post addition API. + * + * @category Bookmarking + * @package SemanticScuttle + * @author Benjamin Huynh-Kim-Bang + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class Api_PostsAddTest extends TestBaseApi +{ + protected $urlPart = 'api/posts/add'; + + + + /** + * Used to run this test class standalone + * + * @return void + */ + public static function main() + { + require_once 'PHPUnit/TextUI/TestRunner.php'; + PHPUnit_TextUI_TestRunner::run( + new PHPUnit_Framework_TestSuite(__CLASS__) + ); + } + + + + /** + * Test if authentication is required when sending no auth data + */ + public function testAuthWithoutAuthData() + { + $req = $this->getRequest(null, false); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * Test if authentication is required when sending wrong user data + */ + public function testAuthWrongCredentials() + { + $req = $this->getRequest(null, false); + $req->setAuth('user', 'password', HTTP_Request2::AUTH_BASIC); + $res = $req->send(); + $this->assertEquals(401, $res->getStatus()); + } + + + + /** + * Test if adding a bookmark via POST works. + */ + public function testAddBookmarkPost() + { + $this->bs->deleteAll(); + + $bmUrl = 'http://example.org/tag-1'; + $bmTags = array('foo', 'bar', 'baz'); + $bmDatetime = '2010-09-08T03:02:01Z'; + $bmTitle = 'This is a foo title'; + $bmDescription = <<?&\$ÄÖ'"§special"' +characters +TXT; + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $bmUrl); + $req->addPostParameter('description', $bmTitle); + $req->addPostParameter('extended', $bmDescription); + $req->addPostParameter('tags', implode(' ', $bmTags)); + $res = $req->send(); + + //all should be well + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //user should have one bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $bm = $data['bookmarks'][0]; + + $this->assertEquals($bmUrl, $bm['bAddress']); + $this->assertEquals($bmTitle, $bm['bTitle']); + $this->assertEquals($bmDescription, $bm['bDescription']); + $this->assertEquals($bmTags, $bm['tags']); + } + + + + /** + * Test if adding a bookmark via GET works. + */ + public function testAddBookmarkGet() + { + $this->bs->deleteAll(); + + $bmUrl = 'http://example.org/tag-1'; + $bmTags = array('foo', 'bar', 'baz'); + $bmDatetime = '2010-09-08T03:02:01Z'; + $bmTitle = 'This is a foo title'; + $bmDescription = <<?&\$ÄÖ'"§special"' +characters +TXT; + + list($req, $uId) = $this->getAuthRequest( + '?url=' . urlencode($bmUrl) + . '&description=' . urlencode($bmTitle) + . '&extended=' . urlencode($bmDescription) + . '&tags=' . urlencode(implode(' ', $bmTags)) + ); + $res = $req->send(); + + //all should be well + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //user should have one bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $bm = $data['bookmarks'][0]; + + $this->assertEquals($bmUrl, $bm['bAddress']); + $this->assertEquals($bmTitle, $bm['bTitle']); + $this->assertEquals($bmDescription, $bm['bDescription']); + $this->assertEquals($bmTags, $bm['tags']); + } + +} + +if (PHPUnit_MAIN_METHOD == 'Api_PostsAddTest::main') { + Api_PostsAddTest::main(); +} +?> \ No newline at end of file diff --git a/www/api/posts_add.php b/www/api/posts_add.php index 59f7dce..b392a80 100644 --- a/www/api/posts_add.php +++ b/www/api/posts_add.php @@ -5,6 +5,8 @@ // del.icio.us behavior: // - tags can't have spaces // - address and description are mandatory +// - description == title in semanticscuttle +// - extended == description in semanticscuttle // Scuttle behavior: // - Additional 'status' variable for privacy From c9398ccd5b5208b87feef21d3996761c8be590a7 Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:52:59 +0000 Subject: [PATCH 11/16] first tests for api/posts/add git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@773 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/Api/PostsAddTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php index 1db8af9..cc9a2c9 100644 --- a/tests/Api/PostsAddTest.php +++ b/tests/Api/PostsAddTest.php @@ -101,6 +101,7 @@ TXT; $req->addPostParameter('description', $bmTitle); $req->addPostParameter('extended', $bmDescription); $req->addPostParameter('tags', implode(' ', $bmTags)); + $req->addPostParameter('dt', $bmDatetime); $res = $req->send(); //all should be well @@ -130,6 +131,10 @@ TXT; $this->assertEquals($bmTitle, $bm['bTitle']); $this->assertEquals($bmDescription, $bm['bDescription']); $this->assertEquals($bmTags, $bm['tags']); + $this->assertEquals( + strtotime($bmDatetime), + strtotime($bm['bDatetime']) + ); } From da272b5a20bec8d39c6d9cdf2fd5573de99083be Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:53:57 +0000 Subject: [PATCH 12/16] check datetime setting git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@774 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/Api/PostsAddTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php index cc9a2c9..0769bdd 100644 --- a/tests/Api/PostsAddTest.php +++ b/tests/Api/PostsAddTest.php @@ -132,8 +132,8 @@ TXT; $this->assertEquals($bmDescription, $bm['bDescription']); $this->assertEquals($bmTags, $bm['tags']); $this->assertEquals( - strtotime($bmDatetime), - strtotime($bm['bDatetime']) + gmdate('Y-m-d H:i:s', strtotime($bmDatetime)), + $bm['bDatetime'] ); } @@ -162,6 +162,7 @@ TXT; . '&description=' . urlencode($bmTitle) . '&extended=' . urlencode($bmDescription) . '&tags=' . urlencode(implode(' ', $bmTags)) + . '&dt=' . urlencode($bmDatetime) ); $res = $req->send(); @@ -192,6 +193,10 @@ TXT; $this->assertEquals($bmTitle, $bm['bTitle']); $this->assertEquals($bmDescription, $bm['bDescription']); $this->assertEquals($bmTags, $bm['tags']); + $this->assertEquals( + gmdate('Y-m-d H:i:s', strtotime($bmDatetime)), + $bm['bDatetime'] + ); } } From 3ff661c0e0180b7808b1b95f9d25cc736d710026 Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:55:14 +0000 Subject: [PATCH 13/16] send 400 status code if a parameter is missing git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@775 b3834d28-1941-0410-a4f8-b48e95affb8f --- tests/Api/PostsAddTest.php | 107 +++++++++++++++++++++++++++++++++++++ www/api/posts_add.php | 26 +++++---- 2 files changed, 124 insertions(+), 9 deletions(-) diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php index 0769bdd..dea002e 100644 --- a/tests/Api/PostsAddTest.php +++ b/tests/Api/PostsAddTest.php @@ -199,6 +199,113 @@ TXT; ); } + /** + * Verify that the URL and description/title are enough parameters + * to add a bookmark. + */ + public function testUrlDescEnough() + { + $this->bs->deleteAll(); + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', 'http://example.org/tag2'); + $req->addPostParameter('description', 'foo bar'); + $res = $req->send(); + + //all should be well + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //user has 1 bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + } + + /** + * Verify that the URL is required + */ + public function testUrlRequired() + { + $this->bs->deleteAll(); + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + //$req->addPostParameter('url', 'http://example.org/tag2'); + $req->addPostParameter('description', 'foo bar'); + $res = $req->send(); + + //all should be well + $this->assertEquals(400, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'URL missing') + ), + $res->getBody(), + null, false + ); + + //user still has 0 bookmarks + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + } + + /** + * Verify that the description/title is required + */ + public function testDescriptionRequired() + { + $this->bs->deleteAll(); + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', 'http://example.org/tag2'); + $res = $req->send(); + + //all should be well + $this->assertEquals(400, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'Description missing') + ), + $res->getBody(), + null, false + ); + + //user still has 0 bookmarks + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(0, $data['total']); + } } if (PHPUnit_MAIN_METHOD == 'Api_PostsAddTest::main') { diff --git a/www/api/posts_add.php b/www/api/posts_add.php index b392a80..8b63a16 100644 --- a/www/api/posts_add.php +++ b/www/api/posts_add.php @@ -73,17 +73,25 @@ if (isset($_REQUEST['shared']) && (trim($_REQUEST['shared']) == 'no')) { } // Error out if there's no address or description -if (is_null($url) || is_null($description)) { - $added = false; +if (is_null($url)) { + header('HTTP/1.0 400 Bad Request'); + $msg = 'URL missing'; +} else if (is_null($description)) { + header('HTTP/1.0 400 Bad Request'); + $msg = 'Description missing'; } else { -// We're good with info; now insert it! - if ($bookmarkservice->bookmarkExists($url, $userservice->getCurrentUserId())) - $added = false; - else - $added = $bookmarkservice->addBookmark($url, $description, $extended, '', $status, $tags, null, $dt, true); + // We're good with info; now insert it! + if ($bookmarkservice->bookmarkExists($url, $userservice->getCurrentUserId())) { + $msg = 'something went wrong'; + } else { + $added = $bookmarkservice->addBookmark( + $url, $description, $extended, '', $status, $tags, null, $dt, true + ); + $msg = 'done'; + } } // Set up the XML file and output the result. -echo '\r\n"; -echo ''; +echo '\r\n"; +echo ''; ?> \ No newline at end of file From 66af94feaf9ef817da260c0d293577bf74e53bcf Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:56:10 +0000 Subject: [PATCH 14/16] reformat api/posts/add and document it better git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@776 b3834d28-1941-0410-a4f8-b48e95affb8f --- www/api/posts_add.php | 115 +++++++++++++++++++++++++++--------------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/www/api/posts_add.php b/www/api/posts_add.php index 8b63a16..0e06d50 100644 --- a/www/api/posts_add.php +++ b/www/api/posts_add.php @@ -1,69 +1,102 @@ + * @author Christian Weiske + * @author Eric Dane + * @license GPL http://www.gnu.org/licenses/gpl.html + * @link http://sourceforge.net/projects/semanticscuttle + * @link http://www.delicious.com/help/api + */ // Force HTTP authentication $httpContentType = 'text/xml'; require_once 'httpauth.inc.php'; -/* Service creation: only useful services are created */ -$bookmarkservice =SemanticScuttle_Service_Factory::get('Bookmark'); +$bs = SemanticScuttle_Service_Factory::get('Bookmark'); // Get all the bookmark's passed-in information -if (isset($_REQUEST['url']) && (trim($_REQUEST['url']) != '')) +if (isset($_REQUEST['url']) && (trim($_REQUEST['url']) != '')) { $url = trim(urldecode($_REQUEST['url'])); -else - $url = NULL; +} else { + $url = null; +} -if (isset($_REQUEST['description']) && (trim($_REQUEST['description']) != '')) +if (isset($_REQUEST['description']) && (trim($_REQUEST['description']) != '')) { $description = trim($_REQUEST['description']); -else - $description = NULL; +} else { + $description = null; +} -if (isset($_REQUEST['extended']) && (trim($_REQUEST['extended']) != "")) +if (isset($_REQUEST['extended']) && (trim($_REQUEST['extended']) != '')) { $extended = trim($_REQUEST['extended']); -else - $extended = NULL; +} else { + $extended = null; +} -if (isset($_REQUEST['tags']) && (trim($_REQUEST['tags']) != '') && (trim($_REQUEST['tags']) != ',')) +if (isset($_REQUEST['tags']) && (trim($_REQUEST['tags']) != '') + && (trim($_REQUEST['tags']) != ',') +) { $tags = trim($_REQUEST['tags']); -else - $tags = NULL; +} else { + $tags = null; +} -if (isset($_REQUEST['dt']) && (trim($_REQUEST['dt']) != '')) +if (isset($_REQUEST['dt']) && (trim($_REQUEST['dt']) != '')) { $dt = trim($_REQUEST['dt']); -else - $dt = NULL; +} else { + $dt = null; +} $status = 0; if (isset($_REQUEST['status'])) { $status_str = trim($_REQUEST['status']); if (is_numeric($status_str)) { $status = intval($status_str); - if($status < 0 || $status > 2) { + if ($status < 0 || $status > 2) { $status = 0; } } else { switch ($status_str) { - case 'private': - $status = 2; - break; - case 'shared': - $status = 1; - break; - default: - $status = 0; - break; + case 'private': + $status = 2; + break; + case 'shared': + $status = 1; + break; + default: + $status = 0; + break; } } } @@ -81,10 +114,10 @@ if (is_null($url)) { $msg = 'Description missing'; } else { // We're good with info; now insert it! - if ($bookmarkservice->bookmarkExists($url, $userservice->getCurrentUserId())) { + if ($bs->bookmarkExists($url, $userservice->getCurrentUserId())) { $msg = 'something went wrong'; } else { - $added = $bookmarkservice->addBookmark( + $added = $bs->addBookmark( $url, $description, $extended, '', $status, $tags, null, $dt, true ); $msg = 'done'; From 8e2b25a095ab769146ac7fa672d7a4c4eda32ab4 Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 20:57:30 +0000 Subject: [PATCH 15/16] api/posts/add respects the "replace" parameter now git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@777 b3834d28-1941-0410-a4f8-b48e95affb8f --- doc/ChangeLog | 7 ++- tests/Api/PostsAddTest.php | 121 +++++++++++++++++++++++++++++++++++++ www/api/posts_add.php | 24 ++++++-- 3 files changed, 145 insertions(+), 7 deletions(-) diff --git a/doc/ChangeLog b/doc/ChangeLog index e2562e4..d711dd4 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -3,12 +3,13 @@ ChangeLog for SemantiScuttle 0.9X.X - 2010-XX-XX ------------------- -- Fix bug getTagsForBookmarks() that fetched all tags +- Fix bug in getTagsForBookmarks() that fetched all tags +- Fix bug #3073215: Updating bookmark time does not work +- Fix bug #3074816: French translation breaks edit javascript - Show error message on mysqli connection errors - Implement patch #3059829: update FR_CA translation - Update php-gettext library to 1.0.10 -- Fix bug #3073215: Updating bookmark time does not work -- Fix bug #3074816: French translation breaks edit javascript +- api/posts/add respects the "replace" parameter now 0.97.0 - 2010-06-09 diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php index dea002e..1f21d04 100644 --- a/tests/Api/PostsAddTest.php +++ b/tests/Api/PostsAddTest.php @@ -306,6 +306,127 @@ TXT; $data = $this->bs->getBookmarks(0, null, $uId); $this->assertEquals(0, $data['total']); } + + /** + * Test that the replace=no parameter prevents the bookmark from being + * overwritten. + */ + public function testReplaceNo() + { + $this->bs->deleteAll(); + + $url = 'http://example.org/tag2'; + $title1 = 'foo bar 1'; + $title2 = 'bar 2 foo'; + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $url); + $req->addPostParameter('description', $title1); + $res = $req->send(); + + //all should be well + $this->assertEquals(200, $res->getStatus()); + + //send it a second time, with different title + list($req, $dummy) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $url); + $req->addPostParameter('description', $title2); + $req->addPostParameter('replace', 'no'); + $res = $req->send(); + + //this time we should get an error + $this->assertEquals(409, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'bookmark does already exist') + ), + $res->getBody(), + null, false + ); + + //user still has 1 bookmark now + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $this->assertEquals($title1, $data['bookmarks'][0]['bTitle']); + + //send it a third time, without the replace parameter + // it defaults to "no", so the bookmark should not get overwritten + list($req, $dummy) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $url); + $req->addPostParameter('description', $title2); + $res = $req->send(); + + //this time we should get an error + $this->assertEquals(409, $res->getStatus()); + + //bookmark should not have changed + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $this->assertEquals($title1, $data['bookmarks'][0]['bTitle']); + } + + /** + * Test that the replace=yes parameter causes the bookmark to be updated. + */ + public function testReplaceYes() + { + $this->bs->deleteAll(); + + $url = 'http://example.org/tag2'; + $title1 = 'foo bar 1'; + $title2 = 'bar 2 foo'; + + list($req, $uId) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $url); + $req->addPostParameter('description', $title1); + $res = $req->send(); + + //all should be well + $this->assertEquals(200, $res->getStatus()); + + //send it a second time, with different title + list($req, $dummy) = $this->getAuthRequest(); + $req->setMethod(HTTP_Request2::METHOD_POST); + $req->addPostParameter('url', $url); + $req->addPostParameter('description', $title2); + $req->addPostParameter('replace', 'yes'); + $res = $req->send(); + + //no error + $this->assertEquals(200, $res->getStatus()); + //verify MIME content type + $this->assertEquals( + 'text/xml; charset=utf-8', + $res->getHeader('content-type') + ); + + //verify xml + $this->assertTag( + array( + 'tag' => 'result', + 'attributes' => array('code' => 'done') + ), + $res->getBody(), + null, false + ); + + //user still has 1 bookmark now, but with the new title + $data = $this->bs->getBookmarks(0, null, $uId); + $this->assertEquals(1, $data['total']); + $this->assertEquals($title2, $data['bookmarks'][0]['bTitle']); + } } if (PHPUnit_MAIN_METHOD == 'Api_PostsAddTest::main') { diff --git a/www/api/posts_add.php b/www/api/posts_add.php index 0e06d50..7f9dc59 100644 --- a/www/api/posts_add.php +++ b/www/api/posts_add.php @@ -16,6 +16,8 @@ * - 0 or 'public': Everyone can see the bookmark * @param string $shared "no" or "yes": Switches between private and * public (optional) + * @param string $replace "yes" or "no" - replaces a bookmark with the + * same URL (optional) * * Notes: * - tags cannot have spaces @@ -23,7 +25,6 @@ * - delicious "description" is the "title" in SemanticScuttle * - delicious "extended" is the "description" in SemanticScuttle * - "status" is a SemanticScuttle addition to this API method - * - SemanticScuttle currently ignores the "replace" parameter * * SemanticScuttle - your social bookmark manager. * @@ -78,6 +79,8 @@ if (isset($_REQUEST['dt']) && (trim($_REQUEST['dt']) != '')) { $dt = null; } +$replace = isset($_REQUEST['replace']) && ($_REQUEST['replace'] == 'yes'); + $status = 0; if (isset($_REQUEST['status'])) { $status_str = trim($_REQUEST['status']); @@ -114,9 +117,22 @@ if (is_null($url)) { $msg = 'Description missing'; } else { // We're good with info; now insert it! - if ($bs->bookmarkExists($url, $userservice->getCurrentUserId())) { - $msg = 'something went wrong'; - } else { + $exists = $bs->bookmarkExists($url, $userservice->getCurrentUserId()); + if ($exists) { + if (!$replace) { + header('HTTP/1.0 409 Conflict'); + $msg = 'bookmark does already exist'; + } else { + //delete it before we re-add it + $bookmark = $bs->getBookmarkByAddress($url, false); + $bId = $bookmark['bId']; + $bs->deleteBookmark($bId); + + $exists = false; + } + } + + if (!$exists) { $added = $bs->addBookmark( $url, $description, $extended, '', $status, $tags, null, $dt, true ); From e1f9d9e3a07652d4ec668a2337d8b788e031ab9f Mon Sep 17 00:00:00 2001 From: cweiske Date: Wed, 29 Sep 2010 22:01:29 +0000 Subject: [PATCH 16/16] merge changelog from 0.97 branch git-svn-id: https://semanticscuttle.svn.sourceforge.net/svnroot/semanticscuttle/trunk@781 b3834d28-1941-0410-a4f8-b48e95affb8f --- doc/ChangeLog | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/ChangeLog b/doc/ChangeLog index d711dd4..0a60bff 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -12,6 +12,14 @@ ChangeLog for SemantiScuttle - api/posts/add respects the "replace" parameter now +0.97.1 - 2010-09-30 +------------------- +This is a security release! We do highly recommend to update +your SemanticScuttle installations! + +- Fix bug #3077187: Permission problem when deleting bookmarks + + 0.97.0 - 2010-06-09 ------------------- - Many SQL optimizations - SemanticScuttle shows bookmarks 4 times faster now