diff --git a/build.xml b/build.xml index d0637c7..6bd5eb2 100644 --- a/build.xml +++ b/build.xml @@ -9,18 +9,31 @@ - - + + - @@ -30,7 +43,8 @@ - + + + + - - + @@ -66,7 +81,6 @@ - SemanticScuttle A social bookmarking tool @@ -108,30 +122,34 @@ **/.svn build* data/config.php + data/config.testing.php + data/config.testing-tmp.php data/locales/messages.po data/locales/*/LC_MESSAGES/messages.po dist/** - doc/developers/** scripts/** src/php-gettext/examples/** - src/php-gettext/bin/** - *.tgz + src/php-gettext/tests/** *.properties + semanticscuttle-dump.sql + *.tgz - - - - + + @@ -207,49 +225,31 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - @@ -284,7 +284,7 @@ + + + + + + + + + + + + + + + + + + @@ -358,8 +390,6 @@ - - diff --git a/data/schema/6.sql b/data/schema/6.sql index 0c208ad..cf9dd37 100644 --- a/data/schema/6.sql +++ b/data/schema/6.sql @@ -13,3 +13,7 @@ CREATE TABLE `sc_users_sslclientcerts` ( PRIMARY KEY ( `id` ) , UNIQUE (`id`) ) CHARACTER SET utf8 COLLATE utf8_general_ci; + +ALTER TABLE `sc_users` ADD `privateKey` VARCHAR(33) NULL; +CREATE UNIQUE INDEX `privateKey` ON `sc_users` (`privateKey`); + diff --git a/data/tables.sql b/data/tables.sql index d53945e..68d5ba9 100644 --- a/data/tables.sql +++ b/data/tables.sql @@ -72,7 +72,9 @@ CREATE TABLE `sc_users` ( `email` varchar(50) NOT NULL default '', `homepage` varchar(255) default NULL, `uContent` text, - PRIMARY KEY (`uId`) + `privateKey` varchar(33) default NULL, + PRIMARY KEY (`uId`), + UNIQUE KEY `privateKey` (`privateKey`) ) CHARACTER SET utf8 COLLATE utf8_general_ci ; -- -------------------------------------------------------- @@ -84,8 +86,7 @@ CREATE TABLE `sc_users_sslclientcerts` ( `sslClientIssuerDn` VARCHAR( 1024 ) NOT NULL , `sslName` VARCHAR( 64 ) NOT NULL , `sslEmail` VARCHAR( 64 ) NOT NULL , - PRIMARY KEY ( `id` ) , - UNIQUE (`id`) + PRIMARY KEY ( `id` ) ) CHARACTER SET utf8 COLLATE utf8_general_ci; -- diff --git a/data/templates/default/about.tpl.php b/data/templates/default/about.tpl.php index 7bff98d..8a9b206 100644 --- a/data/templates/default/about.tpl.php +++ b/data/templates/default/about.tpl.php @@ -17,7 +17,7 @@ $this->includeTemplate($GLOBALS['top_include']); isAdmin()): ?> -
  • SemanticScuttle v0.97.0
  • +
  • SemanticScuttle v0.98.0
  • diff --git a/data/templates/default/bookmarks.tpl.php b/data/templates/default/bookmarks.tpl.php index 91d50bf..0ed9c1d 100644 --- a/data/templates/default/bookmarks.tpl.php +++ b/data/templates/default/bookmarks.tpl.php @@ -221,13 +221,13 @@ if ($currenttag!= '') { $brss = ''; $size = count($rsschannels); for ($i = 0; $i < $size; $i++) { - $brss = '' - . '' . htmlspecialchars($rsschannels[$i][0]) .'' - . ''; + $brss = '' + . '' . htmlspecialchars($rsschannels[$i][0]) .'' + . ''; } $pagesBanner = '

    '. $bfirst .' / '. $bprev .' / '. $bnext .' / '. $blast .' / '. sprintf(T_('Page %d of %d'), $page, $totalpages) ." ". $brss ."

    \n"; diff --git a/data/templates/default/dojo.inc.php b/data/templates/default/dojo.inc.php deleted file mode 100644 index 366dcfe..0000000 --- a/data/templates/default/dojo.inc.php +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/data/templates/default/editprofile.tpl.php b/data/templates/default/editprofile.tpl.php index cc74f04..25dc3a4 100644 --- a/data/templates/default/editprofile.tpl.php +++ b/data/templates/default/editprofile.tpl.php @@ -28,6 +28,14 @@ $this->includeTemplate($GLOBALS['top_include']); + + + /> +     + + + +

    diff --git a/data/templates/default/rss.tpl.php b/data/templates/default/rss.tpl.php index e6e66f7..6be5425 100644 --- a/data/templates/default/rss.tpl.php +++ b/data/templates/default/rss.tpl.php @@ -3,7 +3,7 @@ echo '<' . '?xml version="1.0" encoding="utf-8" ?' . ">\n"; ?> - <?php echo htmlspecialchars($feedtitle); ?> + <?php echo $feedtitle; ?> @@ -23,4 +23,4 @@ echo '<' . '?xml version="1.0" encoding="utf-8" ?' . ">\n"; - \ No newline at end of file + diff --git a/data/templates/default/top.inc.php b/data/templates/default/top.inc.php index 9eed6ff..17ec982 100644 --- a/data/templates/default/top.inc.php +++ b/data/templates/default/top.inc.php @@ -1,4 +1,4 @@ - +?xml version="1.0" encoding="utf-8"?> @@ -13,7 +13,7 @@ if (isset($rsschannels)) { for ($i = 0; $i < $size; $i++) { echo ' '; + . ' href="'. htmlspecialchars($rsschannels[$i][1]) .'" />' . "\n"; } } ?> diff --git a/data/templates/testdummy/top.inc.php b/data/templates/testdummy/top.inc.php index 7b0874c..b85a1ae 100644 --- a/data/templates/testdummy/top.inc.php +++ b/data/templates/testdummy/top.inc.php @@ -12,7 +12,7 @@ if (isset($rsschannels)) { $size = count($rsschannels); for ($i = 0; $i < $size; $i++) { echo ' '; } } diff --git a/doc/ChangeLog b/doc/ChangeLog index cde9e61..f963ca2 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,28 +1,39 @@ ChangeLog for SemantiScuttle ============================ +.. contents:: -0.98.0 - 2011-XX-XX +0.98.1 - 2011-XX-XX +------------------- +- Fix bug #3375635: XML parsing problem in top.inc.php +- Fix bug #3375428: Forgot to remove some old dojo files +- Fix bug #3160512: Make SemanticScuttle work with FastCGI + + +0.98.0 - 2011-07-21 ------------------- - Switch to jQuery and drop dojo +- Implement request #2928950: Private keys for RSS feeds (Mark Pemberton) +- Implement request #3164348: Configurable default privacy (Brett Dee) +- Implement request #1989987: Theming support +- Implement request #3054906: Show user's full name instead of nickname +- Implement patch #3059829: update ``FR_CA`` translation - Fix bug #3187177: Wrong URL / Export XML Bookmarks -- Fix bug in ``getTagsForBookmarks()`` that fetched all tags - Fix bug #3097187: Using opensearch with two tags does not work in Firefox - Fix bug #3251877: French translation JavaScript Bug when editing bookmarks - Fix bug #3168521: Title of tag-filtered RSS Feed is broken - Fix bug #2853627: Javascript warning -- Implement request #1989987: Theming support -- Implement request #3054906: Show user's full name instead of nickname -- Implement patch #3059829: update ``FR_CA`` translation +- Fix bug in ``getTagsForBookmarks()`` that fetched all tags +- Fix privacy issue when fetching tags of several users +- Fix Google custom search XML - Show error message on mysqli connection errors - Update php-gettext library to 1.0.10 - ``api/posts/add`` respects the "replace" parameter now -- Fix privacy issue when fetching tags of several users -- Fix Google custom search XML - Only URLs with an allowed protocol may be added to the database - Support HTTPS connections when ``$root`` is not configured - SQL schema version table to ease future database upgrades - Documentation is written with rST (reStructuredText) now +- Support per-host configuration files 0.97.2 - 2011-02-17 diff --git a/doc/INSTALL.txt b/doc/INSTALL.txt index 5afc732..fbd44f9 100644 --- a/doc/INSTALL.txt +++ b/doc/INSTALL.txt @@ -22,7 +22,7 @@ Installation instructions on the shell ("semanticscuttle" being the database name) 3. Copy ``data/config.php.dist`` to ``data/config.php`` and modify it as - necessary. + necessary. See configuration_ for more information. 4. Make the cache directory writable by your web server. For example, run :: @@ -31,6 +31,12 @@ Installation instructions on the shell. 5. Set the ``www/`` directory as document root in your web server, restart the web server. +6. That's all! Visit your SemanticScuttle installation web site now + with your browser. +7. Register a user and add bookmarks. + + +.. _configuration: configuration.html Ugly www directory in URLs diff --git a/doc/README.rst b/doc/README.rst index 0c7befe..7702400 100644 --- a/doc/README.rst +++ b/doc/README.rst @@ -4,7 +4,9 @@ SemanticScuttle 0.98 A social bookmarking tool experimenting with new features like structured tags or collaborative descriptions of tags. -https://sourceforge.net/projects/semanticscuttle/ +- Home page: http://semanticscuttle.sourceforge.net/ +- Project page: https://sourceforge.net/projects/semanticscuttle/ +- Demo: http://semanticscuttle.sourceforge.net/demo/ Available under the GNU General Public License diff --git a/doc/UPGRADE.txt b/doc/UPGRADE.txt index 0e570e5..b144af2 100644 --- a/doc/UPGRADE.txt +++ b/doc/UPGRADE.txt @@ -2,6 +2,7 @@ Upgrading SemanticScuttle from a previous version ================================================= +.. contents:: From version 0.97 to 0.98 ========================= @@ -9,6 +10,9 @@ Database updates ---------------- Apply ``data/schema/6.sql`` + ALTER TABLE `sc_users` ADD `privateKey` VARCHAR(33) NULL; + CREATE UNIQUE INDEX `privateKey` ON `sc_users` (`privateKey`); + From version 0.96 to 0.97 ========================= diff --git a/doc/allinone.rst b/doc/allinone.rst new file mode 100644 index 0000000..e5ca3c7 --- /dev/null +++ b/doc/allinone.rst @@ -0,0 +1,31 @@ +============================= +SemanticScuttle documentation +============================= + +.. contents:: + +First reads +=========== +.. include:: README.rst +.. include:: INSTALL.txt +.. include:: UPGRADE.txt + + + +Features +======== +.. include:: authentication.rst +.. include:: ssl-client-certificates.rst +.. include:: themes.rst + + + +Developer documentation +======================= +.. include:: developers/rules.rst +.. include:: developers/api.rst +.. include:: developers/debugging.rst +.. include:: developers/release-new-version.rst +.. include:: developers/running-unit-tests.rst +.. include:: developers/translation.rst +.. include:: ChangeLog diff --git a/doc/configuration.rst b/doc/configuration.rst new file mode 100644 index 0000000..f457ebb --- /dev/null +++ b/doc/configuration.rst @@ -0,0 +1,58 @@ +=================== +Configuration files +=================== + +SemanticScuttle uses at least two configuration files: + +1. Default configuration file ``config.default.php`` +2. Custom configuration file ``config.php`` + + +The **default configuration** file contains sensible defaults for most users +that do not need to be changed to get started. + +Never change it - it will get overwritten with the next update. +If you want to change values in it, copy them into your personal +``config.php`` file - updates to SemanticScuttle will not change that one. + +The **custom configuration** file, ``config.php`` is created by copying the +shipped ``config.php.dist`` file and modifying the values in there. + +It consists of the configuration directives that should be set on every +fresh installation. + + + +Configuration scenarios +======================= + +Simple installation +------------------- +Put your configuration file in ``data/config.php``. +If you installed SemanticScuttle's PEAR package, use:: + + $ pear config-get data_dir + /usr/share/php/data + +to find the data directory location and append ``SemanticScuttle/`` to it. +In this case, the configuration file has to be in:: + + /usr/share/php/data/SemanticScuttle/config.php + + +The configuration file may also be saved into:: + + /etc/semanticscuttle/config.php + + +Multiple SemanticScuttle instances +---------------------------------- +The files of one single SemanticScuttle installation may be shared +for several SemanticScuttle instances. + +To be able to configure them differently, SemanticScuttle supports +per-host configuration files: + +- ``data/config.$hostname.php`` +- ``/etc/semanticscuttle/config.$hostname.php`` + diff --git a/doc/developers/release-new-version.rst b/doc/developers/release-new-version.rst index 4b2540a..a5e77dc 100644 --- a/doc/developers/release-new-version.rst +++ b/doc/developers/release-new-version.rst @@ -2,18 +2,18 @@ How to release a new version of SemanticScuttle =============================================== 0. Run unit tests and verify that all of them pass -1. Update doc/ChangeLog -2. Update doc/UPGRADE.txt -3. Update version in data/templates/about.tpl.php, - build.xml and doc/README.txt +1. Update ``doc/ChangeLog`` +2. Update ``doc/UPGRADE.txt`` +3. Update version in ``data/templates/about.tpl.php``, + ``build.xml`` and ``doc/README.rst`` 4. Create a release zip file via the build script: - Just type "phing". + Just type "``phing``". 5. Make a test installation from your zip file with a fresh database. Register a user, add bookmarks etc. 6. When all is fine, it's time to release. The build script takes care for most of the tasks. - Run "phing release", and it will upload the release to + Run "``phing release``", and it will upload the release to sourceforge and create a svn tag. 7. Write announcement mail to the SemanticScuttle mailing list semanticscuttle-devel@lists.sourceforge.net diff --git a/doc/index.rst b/doc/index.rst index 1b8feea..0a53680 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -20,10 +20,12 @@ Features - `Custom user authentication`__ - `SSL Client certificates`__ - Themes__ +- `Configuration files`__ __ authentication.html __ ssl-client-certificates.html __ themes.html +__ configuration.html @@ -35,6 +37,7 @@ Developer documentation - `How to release a new version`__ - `Running unit testes`__ - `How to translate SemanticScuttle`__ +- `ChangeLog`__ __ developers/rules.html __ developers/api.html @@ -42,4 +45,4 @@ __ developers/debugging.html __ developers/release-new-version.html __ developers/running-unit-tests.html __ developers/translation.html - +__ ChangeLog.html diff --git a/res/docs/header.tpl.html b/res/docs/header.tpl.html new file mode 100644 index 0000000..ee0650f --- /dev/null +++ b/res/docs/header.tpl.html @@ -0,0 +1,8 @@ + diff --git a/res/docs/style.css b/res/docs/style.css new file mode 100644 index 0000000..a49b9c1 --- /dev/null +++ b/res/docs/style.css @@ -0,0 +1,25 @@ +/* SemanticScuttle improvements */ +h1.title { + background-image: url("data:image/png;base64,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"); + background-repeat: no-repeat; + padding-left: 64px; + padding-top: 0.2em; + margin-bottom: 00px; + height: 60px; + border-bottom: 3px solid #666; +} +h2 { + margin-top: 1.5em; + +} +pre { + padding: 1em; + background-color: #EEE; + border: 1px solid #BBB; + width: auto; +} + +tt, code { + background-color: #DDD; + padding: 0.2ex; +} diff --git a/src/SemanticScuttle/Config.php b/src/SemanticScuttle/Config.php new file mode 100644 index 0000000..0773310 --- /dev/null +++ b/src/SemanticScuttle/Config.php @@ -0,0 +1,109 @@ + + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Configuration handling + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Config +{ + /** + * Prefix for configuration files. + * Used to inject stream wrapper protocol for unit testing + * + * @var string + */ + public $filePrefix = ''; + + + + /** + * Finds the correct data directory + * + * @return string Full path to the data directory with a trailing slash + */ + protected function getDataDir() + { + if ('@data_dir@' == '@' . 'data_dir@') { + //non pear-install + $datadir = dirname(__FILE__) . '/../../data/'; + } else { + //pear installation; files are in include path + $datadir = '@data_dir@/SemanticScuttle/'; + } + + return $datadir; + } + + + + /** + * Tries to find a configuration file by looking in different + * places: + * - pear data_dir/SemanticScuttle/config-$hostname.php + * - pear data_dir/SemanticScuttle/config.php + * - /etc/semanticscuttle/config-$hostname.php + * - /etc/semanticscuttle/config.php + * + * Paths with host name have priority. + * + * @return array Array with config file path as first value + * and default config file path as second value. + * Any may be NULL if not found + */ + public function findFiles() + { + //use basename to prevent path injection + $host = basename($_SERVER['HTTP_HOST']); + $datadir = $this->getDataDir(); + + $arFiles = array( + $datadir . 'config.' . $host . '.php', + '/etc/semanticscuttle/config.' . $host . '.php', + $datadir . 'config.php', + '/etc/semanticscuttle/config.php', + ); + + $configfile = null; + foreach ($arFiles as $file) { + if (file_exists($this->filePrefix . $file)) { + $configfile = $file; + break; + } + } + + //find default file + $arDefaultFiles = array_unique( + array( + substr($configfile, 0, -3) . 'default.php', + $datadir . 'config.default.php', + '/etc/semanticscuttle/config.default.php', + ) + ); + $defaultfile = null; + foreach ($arDefaultFiles as $file) { + if (file_exists($this->filePrefix . $file)) { + $defaultfile = $file; + break; + } + } + return array($configfile, $defaultfile); + } +} + +?> \ No newline at end of file diff --git a/src/SemanticScuttle/Environment.php b/src/SemanticScuttle/Environment.php new file mode 100644 index 0000000..e5fe3de --- /dev/null +++ b/src/SemanticScuttle/Environment.php @@ -0,0 +1,52 @@ + + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +/** + * Server environment handling methods + * + * @category Bookmarking + * @package SemanticScuttle + * @author Christian Weiske + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ +class SemanticScuttle_Environment +{ + /** + * Determines the correct $_SERVER['PATH_INFO'] value + * + * @return string New value + */ + public static function getServerPathInfo() + { + /* old code that does not work today. + if you find that this code helps you, tell us + and send us the output of var_export($_SERVER); + // Correct bugs with PATH_INFO (maybe for Apache 1 or CGI) -- for 1&1 host... + if (isset($_SERVER['PATH_INFO']) && isset($_SERVER['ORIG_PATH_INFO'])) { + if (strlen($_SERVER["PATH_INFO"]) \ No newline at end of file diff --git a/src/SemanticScuttle/Model/Bookmark.php b/src/SemanticScuttle/Model/Bookmark.php index 8bda0b3..1330642 100644 --- a/src/SemanticScuttle/Model/Bookmark.php +++ b/src/SemanticScuttle/Model/Bookmark.php @@ -23,6 +23,23 @@ */ class SemanticScuttle_Model_Bookmark { + /** + * Status "public" / visible for all + */ + const SPUBLIC = 0; + + /** + * Status "shared" / visible for people on your watchlist + */ + const SWATCHLIST = 1; + + /** + * Status "private" / visible for yourself only + */ + const SPRIVATE = 2; + + + /** * Checks if the given URL is valid and may be used with this * SemanticScuttle installation. diff --git a/src/SemanticScuttle/Model/User.php b/src/SemanticScuttle/Model/User.php index 500f5b1..3aa617b 100644 --- a/src/SemanticScuttle/Model/User.php +++ b/src/SemanticScuttle/Model/User.php @@ -35,6 +35,7 @@ class SemanticScuttle_Model_User var $content; var $datetime; var $isAdmin; + var $privateKey; /** * Create a new user object @@ -68,6 +69,29 @@ class SemanticScuttle_Model_User return $this->username; } + /** + * Returns private key + * + * @param boolean return sanitized value which basically drops + * leading dash if exists + * + * @return string private key + */ + public function getPrivateKey($sanitized = false) + { + // Look for value only if not already set + if (!isset($this->privateKey)) { + $us = SemanticScuttle_Service_Factory::get('User'); + $user = $us->getUser($this->id); + $this->privateKey = $user['privateKey']; + } + if ($sanitized == true) { + return substr($this->privateKey, -32); + } else { + return $this->privateKey; + } + } + /** * Returns full user name as specified in the profile. * @@ -182,4 +206,4 @@ class SemanticScuttle_Model_User } } -?> \ No newline at end of file +?> diff --git a/src/SemanticScuttle/Service/Bookmark.php b/src/SemanticScuttle/Service/Bookmark.php index 919ca7a..17ab7ba 100644 --- a/src/SemanticScuttle/Service/Bookmark.php +++ b/src/SemanticScuttle/Service/Bookmark.php @@ -427,6 +427,7 @@ class SemanticScuttle_Service_Bookmark extends SemanticScuttle_DbService $existence[$hashes[$row['bHash']]] = $row['count'] > 0; } + $this->db->sql_freeresult($dbresult); return $existence; } diff --git a/src/SemanticScuttle/Service/Bookmark2Tag.php b/src/SemanticScuttle/Service/Bookmark2Tag.php index a10cb61..04ee43d 100644 --- a/src/SemanticScuttle/Service/Bookmark2Tag.php +++ b/src/SemanticScuttle/Service/Bookmark2Tag.php @@ -99,7 +99,7 @@ class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService $tags_count = is_array($tags)?count($tags):0; for ($i = 0; $i < $tags_count; $i++) { - $tags[$i] = trim(strtolower($tags[$i])); + $tags[$i] = trim(utf8_strtolower($tags[$i])); if ($fromApi) { include_once 'SemanticScuttle/functions.php'; $tags[$i] = convertTag($tags[$i], 'in'); @@ -584,7 +584,7 @@ class SemanticScuttle_Service_Bookmark2Tag extends SemanticScuttle_DbService if (is_int($days)) { $query .= ' AND B.bDatetime > "' - . date('Y-m-d H:i:s', time() - (86400 * $days)) + . gmdate('Y-m-d H:i:s', time() - (86400 * $days)) . '"'; } diff --git a/src/SemanticScuttle/Service/Tag.php b/src/SemanticScuttle/Service/Tag.php index 25d3888..8325285 100644 --- a/src/SemanticScuttle/Service/Tag.php +++ b/src/SemanticScuttle/Service/Tag.php @@ -141,10 +141,10 @@ class SemanticScuttle_Service_Tag extends SemanticScuttle_DbService //normalize if(!is_array($tags)) { - $tags = strtolower(trim($tags)); + $tags = utf8_strtolower(trim($tags)); } else { for($i=0; $i 'uId', - 'username' => 'username', - 'password' => 'password' + 'primary' => 'uId', + 'username' => 'username', + 'password' => 'password', + 'privateKey' => 'privateKey' ); protected $profileurl; @@ -215,6 +216,18 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService return $this->_getuser($this->getFieldName('username'), $username); } + /** + * Returns user row from database. + * + * @param string $privateKey Private Key + * + * @return array User array from database, false if no user was found + */ + public function getUserByPrivateKey($privateKey) + { + return $this->_getuser($this->getFieldName('privateKey'), $privateKey); + } + function getObjectUserByUsername($username) { $user = $this->_getuser($this->getFieldName('username'), $username); if($user != false) { @@ -279,6 +292,22 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService return ($this->getCurrentUserId() !== false); } + /** + * Tells you if the private key is enabled and valid + * + * @param string $privateKey Private Key + * + * @return boolean True if enabled and valid + */ + public function isPrivateKeyValid($privateKey) + { + // check length of private key + if (strlen($privateKey) == 32) { + return true; + } + return false; + } + /** * Returns the current user object * @@ -293,7 +322,7 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService { if (!is_null($newval)) { //internal use only: reset currentuser - $currentuser = $newval; + $this->currentuser = $newval; } else if ($refresh || !isset($this->currentuser)) { if ($id = $this->getCurrentUserId()) { $this->currentuser = $this->getUser($id); @@ -509,6 +538,46 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService } } + /** + * Try to authenticate via the privateKey + * + * @param string $privateKey Private Key + * + * @return boolean true if the user could be authenticated, + * false if not. + */ + public function loginPrivateKey($privateKey) + { + /* Check if private key valid and enabled */ + if (!$this->isPrivateKeyValid($privateKey)) { + return false; + } + + $query = 'SELECT '. $this->getFieldName('primary') .' FROM ' + . $this->getTableName() .' WHERE ' + . $this->getFieldName('privateKey') .' = "' + . $this->db->sql_escape($privateKey) .'"'; + + if (!($dbresult = $this->db->sql_query($query))) { + message_die( + GENERAL_ERROR, + 'Could not get user', + '', __LINE__, __FILE__, $query, $this->db + ); + return false; + } + + $row = $this->db->sql_fetchrow($dbresult); + $this->db->sql_freeresult($dbresult); + + if ($row) { + $this->setCurrentUserId($row[$this->getFieldName('primary')], false); + return true; + } else { + return false; + } + } + /** * Logs the user off * @@ -519,7 +588,8 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService @setcookie($this->getCookiekey(), '', time() - 1, '/'); unset($_COOKIE[$this->getCookiekey()]); session_unset(); - $this->getCurrentUser(TRUE, false); + $this->currentuserId = null; + $this->currentuser = null; } function getWatchlist($uId) { @@ -603,11 +673,12 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService return false; } - $arrWatch = array(); + $retval = true; if ($this->db->sql_numrows($dbresult) == 0) - return false; - else - return true; + $retval = false; + + $this->db->sql_freeresult($dbresult); + return $retval; } function setWatchStatus($subjectUserID) { @@ -646,24 +717,26 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService * No checks are done in here - you ought to have checked * everything before calling this method! * - * @param string $username Username to use - * @param string $password Password to use - * @param string $email Email to use + * @param string $username Username to use + * @param string $password Password to use + * @param string $email Email to use + * @param string $privateKey Key for RSS auth * * @return mixed Integer user ID if all is well, * boolean false if an error occured */ - public function addUser($username, $password, $email) + public function addUser($username, $password, $email, $privateKey = null) { // Set up the SQL UPDATE statement. $datetime = gmdate('Y-m-d H:i:s', time()); $password = $this->sanitisePassword($password); $values = array( - 'username' => $username, - 'password' => $password, - 'email' => $email, - 'uDatetime' => $datetime, - 'uModified' => $datetime + 'username' => $username, + 'password' => $password, + 'email' => $email, + 'uDatetime' => $datetime, + 'uModified' => $datetime, + 'privateKey' => $privateKey ); $sql = 'INSERT INTO '. $this->getTableName() . ' '. $this->db->sql_build_array('INSERT', $values); @@ -687,40 +760,64 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService /** * Updates the given user * - * @param integer $uId ID of user to change - * @param string $password Password to use - * @param string $name Realname to use - * @param string $email Email to use - * @param string $homepage User's homepage - * @param string $uContent User note + * @param integer $uId ID of user to change + * @param string $password Password to use + * @param string $name Realname to use + * @param string $email Email to use + * @param string $homepage User's homepage + * @param string $uContent User note + * @param string $privateKey RSS Private Key + * @param boolean $enablePrivateKey RSS Private Key Flag * * @return boolean True when all is well, false if not */ public function updateUser( - $uId, $password, $name, $email, $homepage, $uContent + $uId, $password, $name, $email, $homepage, $uContent, + $privateKey = null, $enablePrivateKey = false ) { if (!is_numeric($uId)) { return false; } + // prepend '-' to privateKey if disabled + if ($privateKey != null && strlen($privateKey) == 32 + && $enablePrivateKey == false + ) { + $privateKey = '-' . $privateKey; + } + + // remove '-' from privateKey if enabling + if ($privateKey != null && strlen($privateKey) == 33 + && $enablePrivateKey == true + ) { + $privateKey = substr($privateKey, 1, 32); + } + + // if new user is enabling Private Key, create new key + if ($privateKey == null && $enablePrivateKey == true) { + $privateKey = $this->getNewPrivateKey(); + } + // Set up the SQL UPDATE statement. $moddatetime = gmdate('Y-m-d H:i:s', time()); if ($password == '') { $updates = array( - 'uModified' => $moddatetime, - 'name' => $name, - 'email' => $email, - 'homepage' => $homepage, - 'uContent' => $uContent + 'uModified' => $moddatetime, + 'name' => $name, + 'email' => $email, + 'homepage' => $homepage, + 'uContent' => $uContent, + 'privateKey' => $privateKey ); } else { $updates = array( - 'uModified' => $moddatetime, - 'password' => $this->sanitisePassword($password), - 'name' => $name, - 'email' => $email, - 'homepage' => $homepage, - 'uContent' => $uContent + 'uModified' => $moddatetime, + 'password' => $this->sanitisePassword($password), + 'name' => $name, + 'email' => $email, + 'homepage' => $homepage, + 'uContent' => $uContent, + 'privateKey' => $privateKey ); } $sql = 'UPDATE '. $this->getTableName() @@ -837,6 +934,56 @@ class SemanticScuttle_Service_User extends SemanticScuttle_DbService } } + /** + * Generates a new private key and confirms it isn't being used. + * Private key is 32 characters long, consisting of lowercase and + * numeric characters. + * + * @return string the new key value + */ + public function getNewPrivateKey() + { + do { + $newKey = md5(uniqid('SemanticScuttle', true)); + } while ($this->privateKeyExists($newKey)); + + return $newKey; + } + + /** + * Checks if a private key already exists + * + * @param string $privateKey key that has been generated + * + * @return boolean true when the private key exists, + * False if not. + */ + public function privateKeyExists($privateKey) + { + if (!$privateKey) { + return false; + } + $crit = array('privateKey' => $privateKey); + + $sql = 'SELECT COUNT(*) as "0" FROM ' + . $GLOBALS['tableprefix'] . 'users' + . ' WHERE '. $this->db->sql_build_array('SELECT', $crit); + + if (!($dbresult = $this->db->sql_query($sql))) { + message_die( + GENERAL_ERROR, 'Could not get vars', '', + __LINE__, __FILE__, $sql, $this->db + ); + } + if ($this->db->sql_fetchfield(0, 0) > 0) { + $exists = true; + } else { + $exists = false; + } + $this->db->sql_freeresult($dbresult); + return $exists; + } + function isReserved($username) { if (in_array($username, $GLOBALS['reservedusers'])) { return true; diff --git a/src/SemanticScuttle/constants.php b/src/SemanticScuttle/constants.php index f8567d9..fcb2d90 100644 --- a/src/SemanticScuttle/constants.php +++ b/src/SemanticScuttle/constants.php @@ -69,16 +69,6 @@ define('PAGE_WATCHLIST', "watchlist"); // installations on the same host server define('INSTALLATION_ID', md5($GLOBALS['dbname'].$GLOBALS['tableprefix'])); -// Correct bugs with PATH_INFO (maybe for Apache 1 or CGI) -- for 1&1 host... -if (isset($_SERVER['PATH_INFO']) && isset($_SERVER['ORIG_PATH_INFO'])) { - if (strlen($_SERVER["PATH_INFO"]) diff --git a/src/SemanticScuttle/header.php b/src/SemanticScuttle/header.php index 6c0d4df..694df54 100644 --- a/src/SemanticScuttle/header.php +++ b/src/SemanticScuttle/header.php @@ -25,8 +25,20 @@ if ('@data_dir@' == '@' . 'data_dir@') { //FIXME: when you have multiple installations, the www_dir will be wrong $wwwdir = '@www_dir@/SemanticScuttle/'; } +require_once dirname(__FILE__) . '/Environment.php'; +require_once dirname(__FILE__) . '/Config.php'; -if (!file_exists($datadir . '/config.php')) { +$cfg = new SemanticScuttle_Config(); +list($configfile, $defaultfile) = $cfg->findFiles(); +if ($defaultfile === null) { + header('HTTP/1.0 500 Internal Server Error'); + die( + 'No default configuration file config.default.php found.' + . ' This is really, really strange' + . "\n" + ); +} +if ($configfile === null) { header('HTTP/1.0 500 Internal Server Error'); die( 'Please copy "config.php.dist" to "config.php" in data/ folder.' @@ -39,8 +51,8 @@ set_include_path( ); // 1 // First requirements part (before debug management) -require_once $datadir . '/config.default.php'; -require_once $datadir . '/config.php'; +require_once $defaultfile; +require_once $configfile; if (isset($_GET['unittestMode']) && $_GET['unittestMode'] == 1 ) { diff --git a/tests/AllTests.php b/tests/AllTests.php index 9e825e8..92f8960 100644 --- a/tests/AllTests.php +++ b/tests/AllTests.php @@ -46,6 +46,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/OpenSearchTest.php'); $suite->addTestFile($tdir . '/Api/PostsAddTest.php'); $suite->addTestFile($tdir . '/Api/PostsDeleteTest.php'); $suite->addTestFile($tdir . '/Api/PostsUpdateTest.php'); @@ -58,4 +59,4 @@ class AllTests extends PHPUnit_Framework_TestSuite { } } -?> \ No newline at end of file +?> diff --git a/tests/Api/PostsAddTest.php b/tests/Api/PostsAddTest.php index 02e2b38..e6d0531 100644 --- a/tests/Api/PostsAddTest.php +++ b/tests/Api/PostsAddTest.php @@ -108,7 +108,7 @@ TXT; $this->assertEquals($bmUrl, $bm['bAddress']); $this->assertEquals($bmTitle, $bm['bTitle']); - $this->assertEquals($bmDescription, $bm['bDescription']); + $this->assertEquals($bmDescription, stripslashes($bm['bDescription'])); $this->assertEquals($bmTags, $bm['tags']); $this->assertEquals( gmdate('Y-m-d H:i:s', strtotime($bmDatetime)), @@ -170,7 +170,7 @@ TXT; $this->assertEquals($bmUrl, $bm['bAddress']); $this->assertEquals($bmTitle, $bm['bTitle']); - $this->assertEquals($bmDescription, $bm['bDescription']); + $this->assertEquals($bmDescription, stripslashes($bm['bDescription'])); $this->assertEquals($bmTags, $bm['tags']); $this->assertEquals( gmdate('Y-m-d H:i:s', strtotime($bmDatetime)), diff --git a/tests/Bookmark2TagTest.php b/tests/Bookmark2TagTest.php index 789540f..0236a5f 100644 --- a/tests/Bookmark2TagTest.php +++ b/tests/Bookmark2TagTest.php @@ -282,10 +282,10 @@ class Bookmark2TagTest extends TestBase public function testGetPopularTagsDays() { $user = $this->addUser(); - $this->addTagBookmark($user, array('one', 'two'), 'today'); - $this->addTagBookmark($user, array('one', 'thr'), 'today'); - $this->addTagBookmark($user, array('one', 'two'), '-1 day 1 hour'); - $this->addTagBookmark($user, array('one', 'thr'), '-3 days 1 hour'); + $this->addTagBookmark($user, array('one', 'two'), 'now'); + $this->addTagBookmark($user, array('one', 'thr'), 'now'); + $this->addTagBookmark($user, array('one', 'two'), '-1 day -1 hour'); + $this->addTagBookmark($user, array('one', 'thr'), '-3 days -1 hour'); $arTags = $this->b2ts->getPopularTags(null, 10, null, 1); $this->assertInternalType('array', $arTags); @@ -608,4 +608,4 @@ class Bookmark2TagTest extends TestBase $this->assertContains(array('tag' => 'usable', 'bCount' => '2'), $arTags); } } -?> \ No newline at end of file +?> diff --git a/tests/BookmarkTest.php b/tests/BookmarkTest.php index b50dab2..44a82d9 100644 --- a/tests/BookmarkTest.php +++ b/tests/BookmarkTest.php @@ -1330,5 +1330,51 @@ class BookmarkTest extends TestBase + /** + * Test private bookmarks + * + * @return void + */ + public function testPrivateBookmarks() + { + $uid = $this->addUser(); + /* create private bookmark */ + $this->bs->addBookmark( + 'http://test', 'test', 'desc', 'note', + 2,//private + array(), null, null, false, false, $uid + ); + /* create public bookmark */ + $this->bs->addBookmark( + 'http://example.org', 'title', 'desc', 'priv', + 0,//public + array(), null, null, false, false, $uid + ); + + $this->assertEquals(1, $this->bs->countBookmarks($uid, 'public')); + $this->assertEquals(1, $this->bs->countBookmarks($uid, 'private')); + $this->assertEquals(0, $this->bs->countBookmarks($uid, 'shared')); + $this->assertEquals(2, $this->bs->countBookmarks($uid, 'all')); + + $this->us->setCurrentUserId($uid); + $bookmarks = $this->bs->getBookmarks(); + // first record should be private bookmark + $b0 = $bookmarks['bookmarks'][0]; + $this->assertEquals('test', $b0['bTitle']); + // second record should be public bookmark + $b0 = $bookmarks['bookmarks'][1]; + $this->assertEquals('title', $b0['bTitle']); + + // test non authenticated query + $this->us->setCurrentUserId(null); + $bookmarks = $this->bs->getBookmarks(); + // should only result in one link - public + $b2 = $bookmarks['bookmarks'][0]; + $this->assertEquals('title', $b2['bTitle']); + // there should be no second record + $this->assertEquals(1,count($bookmarks['bookmarks'])); + + } + } ?> diff --git a/tests/SemanticScuttle/ConfigTest.php b/tests/SemanticScuttle/ConfigTest.php new file mode 100644 index 0000000..670f82a --- /dev/null +++ b/tests/SemanticScuttle/ConfigTest.php @@ -0,0 +1,206 @@ +_setPointer($scope, $varpath)) { + return false; + } + + return parent::url_stat($path, $flags); + } +} + +class SemanticScuttle_ConfigTest extends PHPUnit_Framework_TestCase +{ + /** + * Configuration object to test + */ + protected $cfg; + + + public function setUpWrapper() + { + if (!in_array('unittest', stream_get_wrappers())) { + stream_wrapper_register( + 'unittest', 'SemanticScuttle_ConfigTest_StreamVar' + ); + } + + $this->cfg = $this->getMock( + 'SemanticScuttle_Config', + array('getDataDir') + ); + $this->cfg->expects($this->once()) + ->method('getDataDir') + ->will($this->returnValue('/data-dir/')); + + $this->cfg->filePrefix = 'unittest://GLOBALS/unittest-dir'; + } + + + + public function testFindLocalData() + { + $this->setUpWrapper(); + $GLOBALS['unittest-dir']['data-dir'] = array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ); + $this->assertEquals( + array( + '/data-dir/config.php', + '/data-dir/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindHostPreferredOverNonHostConfig() + { + $this->setUpWrapper(); + $_SERVER['HTTP_HOST'] = 'foo.example.org'; + + $GLOBALS['unittest-dir']['data-dir'] = array( + 'config.php' => 'content', + 'config.foo.example.org.php' => 'content', + 'config.default.php' => 'content' + ); + $this->assertEquals( + array( + '/data-dir/config.foo.example.org.php', + '/data-dir/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindEtcHostPreferredOverLocalConfigPhp() + { + $this->setUpWrapper(); + $_SERVER['HTTP_HOST'] = 'foo.example.org'; + + $GLOBALS['unittest-dir'] = array( + 'etc' => array( + 'semanticscuttle' => array( + 'config.foo.example.org.php' => 'content', + ) + ), + 'data-dir' => array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ) + ); + + $this->assertEquals( + array( + '/etc/semanticscuttle/config.foo.example.org.php', + '/data-dir/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindEtcConfig() + { + $this->setUpWrapper(); + $GLOBALS['unittest-dir'] = array( + 'etc' => array( + 'semanticscuttle' => array( + 'config.php' => 'content' + ) + ), + 'data-dir' => array( + 'config.default.php' => 'content' + ) + ); + $this->assertEquals( + array( + '/etc/semanticscuttle/config.php', + '/data-dir/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindEtcDefaultConfig() + { + $this->setUpWrapper(); + $GLOBALS['unittest-dir'] = array( + 'etc' => array( + 'semanticscuttle' => array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ) + ), + ); + $this->assertEquals( + array( + '/etc/semanticscuttle/config.php', + '/etc/semanticscuttle/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindLocalDefaultPreferredOverEtcDefault() + { + $this->setUpWrapper(); + $GLOBALS['unittest-dir'] = array( + 'etc' => array( + 'semanticscuttle' => array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ) + ), + 'data-dir' => array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ) + ); + $this->assertEquals( + array( + '/data-dir/config.php', + '/data-dir/config.default.php' + ), + $this->cfg->findFiles() + ); + } + + public function testFindSameDirDefaultPreferred() + { + $this->setUpWrapper(); + $GLOBALS['unittest-dir'] = array( + 'etc' => array( + 'semanticscuttle' => array( + 'config.php' => 'content', + 'config.default.php' => 'content' + ) + ), + 'data-dir' => array( + 'config.default.php' => 'content' + ) + ); + $this->assertEquals( + array( + '/etc/semanticscuttle/config.php', + '/etc/semanticscuttle/config.default.php' + ), + $this->cfg->findFiles() + ); + } + +} + +?> \ No newline at end of file diff --git a/tests/SemanticScuttle/EnvironmentTest.php b/tests/SemanticScuttle/EnvironmentTest.php new file mode 100644 index 0000000..a41efa1 --- /dev/null +++ b/tests/SemanticScuttle/EnvironmentTest.php @@ -0,0 +1,95 @@ + 'Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.9.168 Version/11.50', + 'HTTP_HOST' => 'bm-cgi.bogo', + 'HTTP_ACCEPT' => 'text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1', + 'HTTP_ACCEPT_LANGUAGE' => 'de-DE,de;q=0.9,en;q=0.8', + 'HTTP_ACCEPT_ENCODING' => 'gzip, deflate', + 'HTTP_COOKIE' => 'PHPSESSID=ga446jhs0e09hkt60u9bsmp0n0', + 'HTTP_CACHE_CONTROL' => 'no-cache', + 'HTTP_CONNECTION' => 'Keep-Alive', + 'PATH' => '/usr/local/bin:/usr/bin:/bin', + 'SERVER_SIGNATURE' => '
    Apache/2.2.17 (Ubuntu) Server at bm-cgi.bogo Port 80
    ', + 'SERVER_SOFTWARE' => 'Apache/2.2.17 (Ubuntu)', + 'SERVER_NAME' => 'bm-cgi.bogo', + 'SERVER_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + 'REMOTE_ADDR' => '127.0.0.1', + 'DOCUMENT_ROOT' => '/etc/apache2/htdocs', + 'SERVER_ADMIN' => '[no address given]', + 'SCRIPT_FILENAME' => '/home/cweiske/Dev/html/hosts/bm-cgi.bogo/profile.php', + 'REMOTE_PORT' => '45349', + 'GATEWAY_INTERFACE' => 'CGI/1.1', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => '', + 'REQUEST_URI' => '/profile.php/dummy', + 'SCRIPT_NAME' => '/profile.php', + 'PATH_INFO' => '/dummy', + 'PATH_TRANSLATED' => '/home/cweiske/Dev/html/hosts/bm-cgi.bogo/dummy', + 'PHP_SELF' => '/profile.php/dummy', + 'REQUEST_TIME' => 1311422546, + ); + $this->assertEquals( + '/dummy', SemanticScuttle_Environment::getServerPathInfo() + ); + } + + + public function testServerPathInfoFastCgi() + { + $_SERVER = array( + 'PHP_FCGI_MAX_REQUESTS' => '5000', + 'PHPRC' => '/etc/php5/cgi/5.3.6/', + 'PHP_FCGI_CHILDREN' => '3', + 'PWD' => '/var/www/cgi-bin', + 'FCGI_ROLE' => 'RESPONDER', + 'REDIRECT_HANDLER' => 'php-cgi', + 'REDIRECT_STATUS' => '200', + 'HTTP_USER_AGENT' => 'Opera/9.80 (X11; Linux x86_64; U; de) Presto/2.9.168 Version/11.50', + 'HTTP_HOST' => 'bm-cgi.bogo', + 'HTTP_ACCEPT' => 'text/html, application/xml;q=0.9, application/xhtml+xml, image/png, image/webp, image/jpeg, image/gif, image/x-xbitmap, */*;q=0.1', + 'HTTP_ACCEPT_LANGUAGE' => 'de-DE,de;q=0.9,en;q=0.8', + 'HTTP_ACCEPT_ENCODING' => 'gzip, deflate', + 'HTTP_COOKIE' => 'PHPSESSID=ga446jhs0e09hkt60u9bsmp0n0', + 'HTTP_CONNECTION' => 'Keep-Alive', + 'PATH' => '/usr/local/bin:/usr/bin:/bin', + 'SERVER_SIGNATURE' => '
    Apache/2.2.17 (Ubuntu) Server at bm-cgi.bogo Port 80
    ', + 'SERVER_SOFTWARE' => 'Apache/2.2.17 (Ubuntu)', + 'SERVER_NAME' => 'bm-cgi.bogo', + 'SERVER_ADDR' => '127.0.0.1', + 'SERVER_PORT' => '80', + 'REMOTE_ADDR' => '127.0.0.1', + 'DOCUMENT_ROOT' => '/etc/apache2/htdocs', + 'SERVER_ADMIN' => '[no address given]', + 'SCRIPT_FILENAME' => '/home/cweiske/Dev/html/hosts/bm-cgi.bogo/profile.php', + 'REMOTE_PORT' => '45342', + 'REDIRECT_URL' => '/profile.php/dummy', + 'GATEWAY_INTERFACE' => 'CGI/1.1', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'GET', + 'QUERY_STRING' => '', + 'REQUEST_URI' => '/profile.php/dummy', + 'SCRIPT_NAME' => '/profile.php', + 'PATH_INFO' => '/dummy', + 'PATH_TRANSLATED' => '/etc/apache2/htdocs/dummy', + 'ORIG_PATH_INFO' => '/profile.php/dummy', + 'ORIG_SCRIPT_NAME' => '/cgi-bin-php/php-cgi-5.3.6', + 'ORIG_SCRIPT_FILENAME' => '/var/www/cgi-bin/php-cgi-5.3.6', + 'ORIG_PATH_TRANSLATED' => '/home/cweiske/Dev/html/hosts/bm-cgi.bogo/profile.php/dummy', + 'PHP_SELF' => '/profile.php/dummy', + 'REQUEST_TIME' => 1311422521, + ); + $this->assertEquals( + '/dummy', SemanticScuttle_Environment::getServerPathInfo() + ); + } + +} + +?> \ No newline at end of file diff --git a/tests/Tag2TagTest.php b/tests/Tag2TagTest.php index 0b73864..58556f1 100644 --- a/tests/Tag2TagTest.php +++ b/tests/Tag2TagTest.php @@ -332,6 +332,7 @@ class Tag2TagTest extends TestBase $this->assertSame('B3', $results['bookmarks'][0]['bTitle']); $results = $bs->getBookmarks(0, NULL, 1, 'aa+ee'); + $this->assertSame(1, intval($results['total'])); $this->assertSame('B2', $results['bookmarks'][0]['bTitle']); diff --git a/tests/TestBase.php b/tests/TestBase.php index 095f32d..2914749 100644 --- a/tests/TestBase.php +++ b/tests/TestBase.php @@ -76,16 +76,18 @@ class TestBase extends PHPUnit_Framework_TestCase /** * Creates a new user in the database. * - * @param string $username Username - * @param string $password Password + * @param string $username Username, may be null + * @param string $password Password, may be null + * @param mixed $privateKey String private key or boolean true to generate one * * @return integer ID of user * * @uses addUserData() */ - protected function addUser($username = null, $password = null) - { - return reset($this->addUserData($username, $password)); + protected function addUser( + $username = null, $password = null, $privateKey = null + ) { + return reset($this->addUserData($username, $password, $privateKey)); } @@ -93,13 +95,15 @@ class TestBase extends PHPUnit_Framework_TestCase /** * Creates a new user in the database and returns id, username and password. * - * @param string $username Username - * @param string $password Password + * @param string $username Username, may be null + * @param string $password Password, may be null + * @param mixed $privateKey String private key or boolean true to generate one * - * @return array ID of user, Name of user, password of user + * @return array ID of user, Name of user, password of user, privateKey */ - protected function addUserData($username = null, $password = null) - { + protected function addUserData( + $username = null, $password = null, $privateKey = null + ) { $us = SemanticScuttle_Service_Factory::get('User'); $rand = rand(); @@ -109,13 +113,17 @@ class TestBase extends PHPUnit_Framework_TestCase if ($password === null) { $password = $rand; } + if ($privateKey === true) { + $privateKey = $this->us->getNewPrivateKey(); + } $uid = $us->addUser( $username, $password, - 'unittest-' . $rand . '@example.org' + 'unittest-' . $rand . '@example.org', + $privateKey ); - return array($uid, $username, $password); + return array($uid, $username, $password, $privateKey); } @@ -148,4 +156,4 @@ class TestBase extends PHPUnit_Framework_TestCase } } -?> \ No newline at end of file +?> diff --git a/tests/TestBaseApi.php b/tests/TestBaseApi.php index 20574f3..1052ae7 100644 --- a/tests/TestBaseApi.php +++ b/tests/TestBaseApi.php @@ -24,7 +24,20 @@ require_once 'HTTP/Request2.php'; */ class TestBaseApi extends TestBase { + /** + * Created from the configured host and the $urlPart. + * Should be used as base for all generated URLs + * + * @var string + */ protected $url; + + /** + * Part of the URL behind the configured host. + * Needs to be overwritten in each derived test case class. + * + * @var string + */ protected $urlPart = null; /** @@ -164,23 +177,25 @@ class TestBaseApi extends TestBase * * Useful for testing HTML pages or ajax URLs. * - * @param string $urlSuffix Suffix for the URL - * @param mixed $auth If user authentication is needed (true/false) - * or array with username and password + * @param string $urlSuffix Suffix for the URL + * @param mixed $auth If user authentication is needed (true/false) + * or array with username and password + * @param boolean $privateKey True if to add user with private key * * @return array(HTTP_Request2, integer) HTTP request object and user id * * @uses getRequest() */ - protected function getLoggedInRequest($urlSuffix = null, $auth = true) - { + protected function getLoggedInRequest( + $urlSuffix = null, $auth = true, $privateKey = null + ) { if (is_array($auth)) { list($username, $password) = $auth; } else { $username = 'testuser'; $password = 'testpassword'; } - $uid = $this->addUser($username, $password); + $uid = $this->addUser($username, $password, $privateKey); $req = new HTTP_Request2( $GLOBALS['unittestUrl'] . '/login.php?unittestMode=1', @@ -234,7 +249,7 @@ class TestBaseApi extends TestBase */ protected function setUnittestConfig($arConfig) { - $str = '<' . "?php\r\n"; + $str = '<' . "?php\n"; foreach ($arConfig as $name => $value) { $str .= '$' . $name . ' = ' . var_export($value, true) . ";\n"; @@ -253,4 +268,4 @@ class TestBaseApi extends TestBase ); } } -?> \ No newline at end of file +?> diff --git a/tests/UserTest.php b/tests/UserTest.php index 2f57112..6cd6786 100644 --- a/tests/UserTest.php +++ b/tests/UserTest.php @@ -34,6 +34,157 @@ class UserTest extends TestBase + /** + * @covers SemanticScuttle_Service_User::addUser + */ + public function testAddUserPrivateKey() + { + $name = substr(md5(uniqid()), 0, 6); + $pkey = 'my-privateKey'; + $id = $this->us->addUser( + $name, uniqid(), 'foo@example.org', $pkey + ); + $this->assertNotEquals(false, $id); + $this->assertInternalType('integer', $id); + + $arUser = $this->us->getUserByPrivateKey($pkey); + $this->assertNotEquals(false, $arUser, 'user not found by private key'); + $this->assertEquals($id, $arUser['uId'], 'wrong user loaded'); + } + + + /** + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserFalseWhenIdNotNumeric() + { + $this->assertFalse( + $this->us->updateUser('foo', null, null, null, null, null) + ); + } + + + /** + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserPrivateKeyNewKeyEnabled() + { + $pkey = 'testUpdateUserPrivateKeyNewKey12'; + $uid = $this->addUser(); + + $this->assertTrue( + $this->us->updateUser( + $uid, 'password', 'name', 'test@example.org', '', '', + $pkey, true + ) + ); + $arUser = $this->us->getUser($uid); + $this->assertInternalType('array', $arUser); + $this->assertEquals($pkey, $arUser['privateKey']); + } + + + /** + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserPrivateKeyNewKeyDisabled() + { + $pkey = 'testUpdateUserPrivateKeyNewKeyDi'; + $uid = $this->addUser(); + + $this->assertTrue( + $this->us->updateUser( + $uid, 'password', 'name', 'test@example.org', '', '', + $pkey, false + ) + ); + $arUser = $this->us->getUser($uid); + $this->assertInternalType('array', $arUser); + $this->assertEquals( + '-' . $pkey, $arUser['privateKey'], + 'private key did not get disabled' + ); + } + + + /** + * Passing an empty string / NULL as key but enabling it + * should automatically create a new key. + * + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserPrivateKeyNoKeyEnabled() + { + $pkey = 'testUpdateUserPrivateKeyNoKeyEna'; + $uid = $this->addUser(); + + $this->assertTrue( + $this->us->updateUser( + $uid, 'password', 'name', 'test@example.org', '', '', + null, true + ) + ); + $arUser = $this->us->getUser($uid); + $this->assertInternalType('array', $arUser); + $this->assertNotEquals( + '', $arUser['privateKey'], 'private key was not created' + ); + } + + + /** + * Passing an empty string / NULL as key and disabling it + * should keep no key + * + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserPrivateKeyNoKeyDisabled() + { + $pkey = 'testUpdateUserPrivateKeyNoKeyDis'; + $uid = $this->addUser(); + + $this->assertTrue( + $this->us->updateUser( + $uid, 'password', 'name', 'test@example.org', '', '', + null, false + ) + ); + $arUser = $this->us->getUser($uid); + $this->assertInternalType('array', $arUser); + $this->assertEquals( + '', $arUser['privateKey'], 'private key was set' + ); + } + + + /** + * Passing an empty string / NULL as key and disabling it + * should keep no key + * + * @covers SemanticScuttle_Service_User::updateUser + */ + public function testUpdateUserPrivateKeyExistingKeyEnabled() + { + $pkey = '12345678901234567890123456789012'; + $uid = $this->addUser(); + + $this->assertTrue( + $this->us->updateUser( + $uid, 'password', 'name', 'test@example.org', '', '', + '-' . $pkey, true + ) + ); + $arUser = $this->us->getUser($uid); + $this->assertInternalType('array', $arUser); + $this->assertEquals( + $pkey, $arUser['privateKey'], 'private key was not enabled' + ); + } + + //FIXME: verify I cannot re-use private key of different user + + + /** * Test that setting the current user ID is permanent. * and that the current user array is the same ID @@ -176,5 +327,185 @@ class UserTest extends TestBase ); } + + public function testGetUserByPrivateKeyEmptyKey() + { + $arUser = $this->us->getUserByPrivateKey(null); + $this->assertFalse($arUser); + } + + + public function testGetUserByPrivateKeyInvalid() + { + $arUser = $this->us->getUserByPrivateKey('foobar'); + $this->assertFalse($arUser); + + $arUser = $this->us->getUserByPrivateKey('%'); + $this->assertFalse($arUser); + } + + + public function testGetUserByPrivateKeyValidKey() + { + $pkey = $this->us->getNewPrivateKey(); + $uId = $this->addUser(null, null, $pkey); + + $arUser = $this->us->getUserByPrivateKey($pkey); + $this->assertInternalType('array', $arUser); + $this->assertArrayHasKey('uId', $arUser); + $this->assertArrayHasKey('username', $arUser); + + $this->assertEquals($uId, $arUser['uId']); + } + + + /** + * @covers SemanticScuttle_Service_User::privateKeyExists + */ + public function testPrivateKeyExistsEmpty() + { + $this->assertFalse($this->us->privateKeyExists(null)); + $this->assertFalse($this->us->privateKeyExists('')); + } + + + /** + * @covers SemanticScuttle_Service_User::privateKeyExists + */ + public function testPrivateKeyExistsInvalid() + { + $this->assertFalse($this->us->privateKeyExists('-1')); + } + + + /** + * @covers SemanticScuttle_Service_User::privateKeyExists + */ + public function testPrivateKeyExists() + { + $randKey = $this->us->getNewPrivateKey(); + $this->assertFalse($this->us->privateKeyExists($randKey)); + $uid = $this->addUser(null, null, $randKey); + + $this->us->setCurrentUserId($uid); + $this->assertEquals($uid, $this->us->getCurrentUserId()); + + $this->assertTrue($this->us->privateKeyExists($randKey)); + } + + + /** + * @covers SemanticScuttle_Service_User::isPrivateKeyValid + */ + public function testIsPrivateKeyValid() + { + $this->assertFalse( + $this->us->isPrivateKeyValid(null), + 'NULL is an invalid private key' + ); + + $randKey = $this->us->getNewPrivateKey(); + $this->assertTrue( + $this->us->isPrivateKeyValid($randKey), + 'generated key should be valid' + ); + + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $this->assertFalse( + $this->us->isPrivateKeyValid($randKey2), + 'disabled privateKey should return false' + ); + } + + + public function testLoginPrivateKeyInvalid() + { + /* normal user with enabled privateKey */ + $randKey = $this->us->getNewPrivateKey(); + $uid1 = $this->addUser('testusername', 'passw0rd', $randKey); + /* user that has disabled privateKey */ + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $uid2 = $this->addUser('seconduser', 'passw0RD', $randKey2); + + /* test invalid private key */ + $this->assertFalse( + $this->us->loginPrivateKey('02848248084082408240824802408248') + ); + } + + + public function testLoginPrivateKeyValidEnabledKey() + { + /* normal user with enabled privateKey */ + $randKey = $this->us->getNewPrivateKey(); + $uid1 = $this->addUser('testusername', 'passw0rd', $randKey); + /* user that has disabled privateKey */ + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $uid2 = $this->addUser('seconduser', 'passw0RD', $randKey2); + + + /* test valid credentials with private key enabled */ + $this->assertTrue( + $this->us->loginPrivateKey($randKey) + ); + } + + + public function testLoginPrivateKeyInvalidEnabledKey() + { + /* normal user with enabled privateKey */ + $randKey = $this->us->getNewPrivateKey(); + $uid1 = $this->addUser('testusername', 'passw0rd', $randKey); + /* user that has disabled privateKey */ + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $uid2 = $this->addUser('seconduser', 'passw0RD', $randKey2); + + + /* test valid credentials with private key enabled but invalid key */ + $this->assertFalse( + $this->us->loginPrivateKey('123') + ); + } + + + public function testLoginPrivateKeyValidDisabledKey() + { + /* normal user with enabled privateKey */ + $randKey = $this->us->getNewPrivateKey(); + $uid1 = $this->addUser('testusername', 'passw0rd', $randKey); + /* user that has disabled privateKey */ + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $uid2 = $this->addUser('seconduser', 'passw0RD', $randKey2); + + /* confirm user exists so future fails should be due to randkey */ + $this->assertTrue( + $this->us->login('seconduser', 'passw0RD', false) + ); + + /* test valid credentials with private key disabled */ + $this->assertFalse( + $this->us->loginPrivateKey($randKey2) + ); + } + + + public function testLoginPrivateKeyInvalidDisabled() + { + /* normal user with enabled privateKey */ + $randKey = $this->us->getNewPrivateKey(); + $uid1 = $this->addUser('testusername', 'passw0rd', $randKey); + /* user that has disabled privateKey */ + $randKey2 = '-'.$this->us->getNewPrivateKey(); + $uid2 = $this->addUser('seconduser', 'passw0RD', $randKey2); + + /* test valid credentials with private key disabled and invalid key */ + $this->assertFalse( + $this->us->loginPrivateKey('-1') + ); + $this->assertFalse( + $this->us->loginPrivateKey(null) + ); + } + } -?> \ No newline at end of file +?> diff --git a/tests/www/bookmarksTest.php b/tests/www/bookmarksTest.php index df360cc..ae82118 100755 --- a/tests/www/bookmarksTest.php +++ b/tests/www/bookmarksTest.php @@ -4,7 +4,7 @@ require_once 'HTTP/Request2.php'; class www_bookmarksTest extends TestBaseApi { - protected $urlPart = 'api/posts/add'; + protected $urlPart = 'bookmarks.php'; /** * Test that the default privacy setting is selected in the Privacy @@ -16,23 +16,11 @@ class www_bookmarksTest extends TestBaseApi $this->setUnittestConfig( array('defaults' => array('privacy' => 2)) ); + list($req, $uId) = $this->getLoggedInRequest(); - $cookies = $req->getCookieJar(); - $req->setMethod(HTTP_Request2::METHOD_POST); - $req->addPostParameter('url', 'http://www.example.org/testdefaultprivacyposts_bookmarksget'); - $req->addPostParameter('description', 'Test bookmark 1 for default privacy.'); - $req->addPostParameter('status', '0'); - $req->send(); - - $bms = $this->bs->getBookmarks(0, null, $uId); - $this->assertEquals(1, count($bms['bookmarks'])); $user = $this->us->getUser($uId); - $reqUrl = $GLOBALS['unittestUrl'] . 'bookmarks.php/' . $user['username'] . '?action=get' . '&unittestMode=1'; - - list($req, $uId) = $this->getAuthRequest('?unittestMode=1'); $req->setMethod(HTTP_Request2::METHOD_POST); - $req->setUrl($reqUrl); - $req->setCookieJar($cookies); + $req->setUrl($this->getTestUrl('/' . $user['username'] . '?action=get')); $req->addPostParameter('submitted', '1'); $response = $req->send(); $response_body = $response->getBody(); @@ -41,12 +29,15 @@ class www_bookmarksTest extends TestBaseApi $ns = $x->getDocNamespaces(); $x->registerXPathNamespace('ns', reset($ns)); - $elements = $x->xpath('//ns:select[@name="status"]/ns:option[@selected="selected"]'); + $elements = $x->xpath( + '//ns:select[@name="status"]/ns:option[@selected="selected"]' + ); $this->assertEquals(1, count($elements), 'No selected status option found'); $this->assertEquals(2, (string)$elements[0]['value']); }//end testDefaultPrivacyBookmarksAddMissingTitleMissingPrivacy + /** * Test that the default privacy setting is selected in the Privacy * drop-down list when a new bookmark is being created. @@ -56,13 +47,10 @@ class www_bookmarksTest extends TestBaseApi $this->setUnittestConfig( array('defaults' => array('privacy' => 1)) ); - list($req, $uId) = $this->getLoggedInRequest('?unittestMode=1'); + list($req, $uId) = $this->getLoggedInRequest(); $user = $this->us->getUser($uId); - $reqUrl = $GLOBALS['unittestUrl'] . 'bookmarks.php/' - . $user['username'] . '?action=add' . '&unittestMode=1'; - $req->setUrl($reqUrl); - $req->setMethod(HTTP_Request2::METHOD_GET); + $req->setUrl($this->getTestUrl('/' . $user['username'] . '?action=add')); $response = $req->send(); $response_body = $response->getBody(); $this->assertNotEquals('', $response_body, 'Response is empty'); @@ -71,10 +59,70 @@ class www_bookmarksTest extends TestBaseApi $ns = $x->getDocNamespaces(); $x->registerXPathNamespace('ns', reset($ns)); - $elements = $x->xpath('//ns:select[@name="status"]/ns:option[@selected="selected"]'); + $elements = $x->xpath( + '//ns:select[@name="status"]/ns:option[@selected="selected"]' + ); $this->assertEquals(1, count($elements), 'No selected status option found'); $this->assertEquals(1, (string)$elements[0]['value']); }//end testDefaultPrivacyBookmarksAdd + + + /** + * Test that the private RSS link exists when a user + * has a private key and is enabled + */ + public function testVerifyPrivateRSSLinkExists() + { + list($req, $uId) = $this->getLoggedInRequest('?unittestMode=1', true, true); + + $user = $this->us->getUser($uId); + $req->setUrl($this->getTestUrl('/' . $user['username'])); + $response = $req->send(); + $response_body = $response->getBody(); + $this->assertNotEquals('', $response_body, 'Response is empty'); + + $x = simplexml_load_string($response_body); + $ns = $x->getDocNamespaces(); + $x->registerXPathNamespace('ns', reset($ns)); + + $elements = $x->xpath( + '//ns:link[@rel="alternate" and @type="application/rss+xml"]' + ); + $this->assertEquals( + 2, count($elements), 'Number of Links in Head not correct' + ); + $this->assertContains('privateKey=', (string)$elements[1]['href']); + }//end testVerifyPrivateRSSLinkExists + + + + /** + * Test that the private RSS link doesn't exists when a user + * does not have a private key or is not enabled + */ + public function testVerifyPrivateRSSLinkDoesNotExist() + { + list($req, $uId) = $this->getLoggedInRequest('?unittestMode=1', true); + + $user = $this->us->getUser($uId); + $req->setUrl($this->getTestUrl('/' . $user['username'])); + $response = $req->send(); + $response_body = $response->getBody(); + $this->assertNotEquals('', $response_body, 'Response is empty'); + + $x = simplexml_load_string($response_body); + $ns = $x->getDocNamespaces(); + $x->registerXPathNamespace('ns', reset($ns)); + + $elements = $x->xpath( + '//ns:link[@rel="alternate" and @type="application/rss+xml"]' + ); + $this->assertEquals( + 1, count($elements), 'Number of Links in Head not correct' + ); + $this->assertNotContains('privateKey=', (string)$elements[0]['href']); + }//end testVerifyPrivateRSSLinkDoesNotExist + }//end class www_bookmarksTest ?> diff --git a/tests/www/indexTest.php b/tests/www/indexTest.php new file mode 100644 index 0000000..503fd1f --- /dev/null +++ b/tests/www/indexTest.php @@ -0,0 +1,58 @@ +getLoggedInRequest('?unittestMode=1', true, true); + + $user = $this->us->getUser($uId); + $response = $req->send(); + $response_body = $response->getBody(); + + $this->assertNotEquals('', $response_body, 'Response is empty'); + + $x = simplexml_load_string($response_body); + $ns = $x->getDocNamespaces(); + $x->registerXPathNamespace('ns', reset($ns)); + + $elements = $x->xpath('//ns:link[@rel="alternate" and @type="application/rss+xml"]'); + $this->assertEquals(2, count($elements), 'Number of Links in Head not correct'); + $this->assertContains('privateKey=', (string)$elements[1]['href']); + }//end testVerifyPrivateRSSLinkExists + + + + /** + * Test that the private RSS link doesn't exists when a user + * does not have a private key, or the private key is not enabled + */ + public function testVerifyPrivateRSSLinkDoesNotExist() + { + list($req, $uId) = $this->getLoggedInRequest('?unittestMode=1', true); + + $user = $this->us->getUser($uId); + $response = $req->send(); + $response_body = $response->getBody(); + $this->assertNotEquals('', $response_body, 'Response is empty'); + + $x = simplexml_load_string($response_body); + $ns = $x->getDocNamespaces(); + $x->registerXPathNamespace('ns', reset($ns)); + + $elements = $x->xpath('//ns:link[@rel="alternate" and @type="application/rss+xml"]'); + $this->assertEquals(1, count($elements), 'Number of Links in Head not correct'); + $this->assertNotContains('privateKey=', (string)$elements[0]['href']); + }//end testVerifyPrivateRSSLinkDoesNotExist + + +}//end class www_bookmarksTest +?> diff --git a/tests/www/rssTest.php b/tests/www/rssTest.php new file mode 100644 index 0000000..71d0198 --- /dev/null +++ b/tests/www/rssTest.php @@ -0,0 +1,151 @@ +assertEquals($nCount, count($simpleXml->channel->item), $msg); + } + + + + + /** + * A private bookmark should not show up in the global rss feed if the + * user is not logged in nor passes the private key + */ + public function testAllPrivateBookmarkNotLoggedIn() + { + list($uId, $username) = $this->addUserData(); + $this->addBookmark( + $uId, null, SemanticScuttle_Model_Bookmark::SPRIVATE + ); + + $req = $this->getRequest(); + $response_body = $req->send()->getBody(); + + $rss = simplexml_load_string($response_body); + $this->assertItemCount($rss, 0, 'I see a private bookmark'); + } + + + + /** + * A private bookmark should not show up in the user's rss feed if the + * user is not logged in nor passes the private key + */ + public function testUserPrivateBookmarkNotLoggedIn() + { + list($uId, $username) = $this->addUserData(); + $this->addBookmark( + $uId, null, SemanticScuttle_Model_Bookmark::SPRIVATE + ); + + $req = $this->getRequest('/' . $username); + $response_body = $req->send()->getBody(); + + $rss = simplexml_load_string($response_body); + $this->assertItemCount($rss, 0, 'I see a private bookmark'); + } + + + + + /** + * Test the global feed by passing the private key + */ + public function testAllPrivateBookmarkWithPrivateKey() + { + list($uId, $username, $password, $privateKey) = $this->addUserData( + null, null, true + ); + $this->addBookmark( + $uId, null, SemanticScuttle_Model_Bookmark::SPRIVATE, + null, 'private bookmark' + ); + + $req = $this->getRequest('?privateKey=' . $privateKey); + $response_body = $req->send()->getBody(); + + $rss = simplexml_load_string($response_body); + $this->assertItemCount($rss, 1, 'I miss the private bookmark'); + $this->assertEquals( + 'private bookmark', (string)$rss->channel->item[0]->title + ); + } + + + + /** + * Test the user feed by passing the private key + */ + public function testUserPrivateBookmarkWithPrivateKey() + { + list($uId, $username, $password, $privateKey) = $this->addUserData( + null, null, true + ); + $this->addBookmark( + $uId, null, SemanticScuttle_Model_Bookmark::SPRIVATE, + null, 'private bookmark' + ); + + $req = $this->getRequest('/' . $username . '?privateKey=' . $privateKey); + $response_body = $req->send()->getBody(); + + $rss = simplexml_load_string($response_body); + $this->assertItemCount($rss, 1, 'I miss the private bookmark'); + $this->assertEquals( + 'private bookmark', (string)$rss->channel->item[0]->title + ); + } + + + + /** + * Verify that fetching the feed with a private key + * does not keep you logged in + */ + public function testUserPrivateKeyDoesNotKeepLoggedYouIn() + { + list($uId, $username, $password, $privateKey) = $this->addUserData( + null, null, true + ); + $this->addBookmark( + $uId, null, SemanticScuttle_Model_Bookmark::SPRIVATE, + null, 'private bookmark' + ); + + $req = $this->getRequest('/' . $username . '?privateKey=' . $privateKey); + $cookies = $req->setCookieJar()->getCookieJar(); + $response_body = $req->send()->getBody(); + + $rss = simplexml_load_string($response_body); + $items = $rss->channel->item; + + $this->assertEquals(1, count($items), 'I miss the private bookmark'); + $this->assertEquals('private bookmark', (string)$items[0]->title); + + //request the feed again, with the same cookies + $req = $this->getRequest('/' . $username); + $req->setCookieJar($cookies); + $response_body = $req->send()->getBody(); + $rss = simplexml_load_string($response_body); + $this->assertItemCount($rss, 0, 'I still see the private bookmark'); + } + +}//end class www_rssTest +?> diff --git a/tests/www/searchTest.php b/tests/www/searchTest.php index 5dce69d..89af32d 100644 --- a/tests/www/searchTest.php +++ b/tests/www/searchTest.php @@ -1,4 +1,6 @@ \ No newline at end of file +?> diff --git a/www/ajaxGetNewPrivateKey.php b/www/ajaxGetNewPrivateKey.php new file mode 100644 index 0000000..eacebd8 --- /dev/null +++ b/www/ajaxGetNewPrivateKey.php @@ -0,0 +1,35 @@ + + * @author Mark Pemberton + * @license AGPL http://www.gnu.org/licenses/agpl.html + * @link http://sourceforge.net/projects/semanticscuttle + */ + +header("Last-Modified: ". gmdate("D, d M Y H:i:s") ." GMT"); +header("Cache-Control: no-cache, must-revalidate"); + +$httpContentType = 'text/xml'; +require_once 'www-header.php'; + +$us = SemanticScuttle_Service_Factory::get('User'); + +/* Managing all possible inputs */ +isset($_GET['url']) ? define('GET_URL', $_GET['url']): define('GET_URL', ''); + +echo ''; +?> + + +getNewPrivateKey + + +getNewPrivateKey(); ?> + + diff --git a/www/bookmarks.php b/www/bookmarks.php index efc1680..72c063e 100644 --- a/www/bookmarks.php +++ b/www/bookmarks.php @@ -229,12 +229,14 @@ if ($templatename == 'editbookmark.tpl') { $tplVars['sidebar_blocks'] = array('watchstatus'); if (!$cat) { //user page without tags + $rssTitle = "My Bookmarks"; $cat = NULL; $tplVars['currenttag'] = NULL; //$tplVars['sidebar_blocks'][] = 'menu2'; $tplVars['sidebar_blocks'][] = 'linked'; $tplVars['sidebar_blocks'][] = 'popular'; } else { //pages with tags + $rssTitle = "Tags" . $catTitle; $rssCat = '/'. filter($cat, 'url'); $tplVars['currenttag'] = $cat; $tplVars['sidebar_blocks'][] = 'tagactions'; @@ -264,9 +266,32 @@ if ($templatename == 'editbookmark.tpl') { // Set template vars $tplVars['rsschannels'] = array( - array(filter($sitename .': '. $pagetitle), createURL('rss', filter($user, 'url') . $rssCat.'?sort='.getSortOrder())) + array( + sprintf(T_('%s: %s'), $sitename, $rssTitle), + createURL('rss', filter($user, 'url')) + . $rssCat . '?sort='.getSortOrder() + ) ); + if ($userservice->isLoggedOn()) { + $currentUsername = $currentUser->getUsername(); + if ($userservice->isPrivateKeyValid($currentUser->getPrivateKey())) { + array_push( + $tplVars['rsschannels'], + array( + sprintf( + T_('%s: %s (+private %s)'), + $sitename, $rssTitle, $currentUsername + ), + createURL('rss', filter($currentUsername, 'url')) + . $rssCat + . '?sort=' . getSortOrder() + . '&privateKey=' . $currentUser->getPrivateKey() + ) + ); + } + } + $tplVars['page'] = $page; $tplVars['start'] = $start; $tplVars['bookmarkCount'] = $start + 1; diff --git a/www/index.php b/www/index.php index f6704ae..7fbb84c 100644 --- a/www/index.php +++ b/www/index.php @@ -42,9 +42,30 @@ if (GET_ACTION == "logout") { // Header variables $tplVars['loadjs'] = true; $tplVars['rsschannels'] = array( -array(sprintf(T_('%s: Recent bookmarks'), $sitename), createURL('rss').'?sort='.getSortOrder()) + array( + sprintf(T_('%s: Recent bookmarks'), $sitename), + createURL('rss') . '?sort=' . getSortOrder() + ) ); +if ($userservice->isLoggedOn()) { + if ($userservice->isPrivateKeyValid($currentUser->getPrivateKey())) { + $currentUsername = $currentUser->getUsername(); + array_push( + $tplVars['rsschannels'], + array( + sprintf( + T_('%s: Recent bookmarks (+private %s)'), + $sitename, $currentUsername + ), + createURL('rss') + . '?sort=' . getSortOrder() + . '&privateKey=' . $currentUser->getPrivateKey() + ) + ); + } +} + if ($usecache) { // Generate hash for caching on $hashtext = $_SERVER['REQUEST_URI']; diff --git a/www/js/jstree-1.0-rc2/MultiComboBox.js b/www/js/jstree-1.0-rc2/MultiComboBox.js deleted file mode 100644 index b263c8b..0000000 --- a/www/js/jstree-1.0-rc2/MultiComboBox.js +++ /dev/null @@ -1,72 +0,0 @@ -/* - Copyright (c) 2004-2008, The Dojo Foundation All Rights Reserved. - Available via Academic Free License >= 2.1 OR the modified BSD license. - see: http://dojotoolkit.org/license for details -*/ - -/* SemanticScuttle: This script is a light modification of dojox.form.MultiComboBox -This fork allows specific use until DOJO 1.2.3 in Google CDN. */ - - - -if(!dojo._hasResource["js.MultiComboBox"]){ //_hasResource checks added by build. Do not use _hasResource directly in your code. -dojo._hasResource["js.MultiComboBox"] = true; -dojo.provide("js.MultiComboBox"); -dojo.experimental("js.MultiComboBox"); -dojo.require("dijit.form.ComboBox"); -dojo.require("dijit.form.ValidationTextBox"); - -dojo.declare("js.MultiComboBox", - [dijit.form.ValidationTextBox, dijit.form.ComboBoxMixin],{ - // - // summary: A ComboBox that accpets multiple inputs on a single line? - // - // delimiter: String - // The character to use to separate items in the ComboBox input - delimiter: ",", - _previousMatches: false, - - _setValueAttr: function(value){ - if (this.delimiter && value.length != 0){ - value = value+this.delimiter+" "; - arguments[0] = this._addPreviousMatches(value); - } - this.inherited(arguments); - }, - - _addPreviousMatches: function(/* String */text){ - if(this._previousMatches){ - if(!text.match(new RegExp("^"+this._previousMatches))){ - text = this._previousMatches+text; - } - } - text = this._cleanupDelimiters(text); // SScuttle: this line was moved - return text; // String - }, - - _cleanupDelimiters: function(/* String */text){ - if(this.delimiter){ - text = text.replace(new RegExp(" +"), " "); - text = text.replace(new RegExp("^ *"+this.delimiter+"* *"), ""); - text = text.replace(new RegExp(this.delimiter+" *"+this.delimiter), this.delimiter); - } - return text; - }, - - _autoCompleteText: function(/* String */text){ - arguments[0] = this._addPreviousMatches(text); - this.inherited(arguments); - }, - - _startSearch: function(/* String */text){ - text = this._cleanupDelimiters(text); - var re = new RegExp("^.*"+this.delimiter+" *"); - - if((this._previousMatches = text.match(re))){ - arguments[0] = text.replace(re, ""); - } - this.inherited(arguments); - } -}); - -} \ No newline at end of file diff --git a/www/jsScuttle.php b/www/jsScuttle.php index 5e5f31b..de97fd2 100644 --- a/www/jsScuttle.php +++ b/www/jsScuttle.php @@ -90,6 +90,24 @@ function useAddress(ele) { } } +/** + * Makes an ajax call to PHP script to generate an new Private Key + * + * @param input Calling object + * @param response Response object that returned value is placed + * + * @return boolean Returns false to halt execution after call + */ +function getNewPrivateKey(input, response){ + var pk = document.getElementById('pPrivateKey'); + if (response != null) { + pk.value = response.trim(); + } else { + loadXMLDocProc('ajaxGetNewPrivateKey.php'); + } + return false; +} + function getTitle(input, response){ var title = document.getElementById('titleField'); if (title.value == '') { diff --git a/www/profile.php b/www/profile.php index ccdb7a8..63f4da8 100644 --- a/www/profile.php +++ b/www/profile.php @@ -23,12 +23,16 @@ require_once 'www-header.php'; /* Service creation: only useful services are created */ // No specific services +$tplVars['loadjs'] = true; /* Managing all possible inputs */ +isset($_POST['submittedPK']) ? define('POST_SUBMITTEDPK', $_POST['submittedPK']): define('POST_SUBMITTEDPK', ''); isset($_POST['submitted']) ? define('POST_SUBMITTED', $_POST['submitted']): define('POST_SUBMITTED', ''); isset($_POST['pPass']) ? define('POST_PASS', $_POST['pPass']): define('POST_PASS', ''); isset($_POST['pPassConf']) ? define('POST_PASSCONF', $_POST['pPassConf']): define('POST_PASSCONF', ''); isset($_POST['pName']) ? define('POST_NAME', $_POST['pName']): define('POST_NAME', ''); +isset($_POST['pPrivateKey']) ? define('POST_PRIVATEKEY', $_POST['pPrivateKey']): define('POST_PRIVATEKEY', ''); +isset($_POST['pEnablePrivateKey']) ? define('POST_ENABLEPRIVATEKEY', $_POST['pEnablePrivateKey']): define('POST_ENABLEPRIVATEKEY', ''); isset($_POST['pMail']) ? define('POST_MAIL', $_POST['pMail']): define('POST_MAIL', ''); isset($_POST['pPage']) ? define('POST_PAGE', $_POST['pPage']): define('POST_PAGE', ''); isset($_POST['pDesc']) ? define('POST_DESC', $_POST['pDesc']): define('POST_DESC', ''); @@ -61,10 +65,19 @@ if ($user) { exit(); } +$tplVars['privateKeyIsEnabled'] = ''; if ($userservice->isLoggedOn() && $user == $currentUser->getUsername()) { - $title = T_('My Profile'); + $title = T_('My Profile'); + $tplVars['privateKey'] = $currentUser->getPrivateKey(true); + + if ($userservice->isPrivateKeyValid($currentUser->getPrivateKey())) { + $tplVars['privateKeyIsEnabled'] = 'checked="checked"'; + } else { + $tplVars['privateKeyIsEnabled'] = ''; + } } else { - $title = T_('Profile') .': '. $user; + $title = T_('Profile') .': '. $user; + $tplVars['privateKey'] = ''; } $tplVars['pagetitle'] = $title; $tplVars['subtitle'] = $title; @@ -72,11 +85,19 @@ $tplVars['subtitle'] = $title; $tplVars['user'] = $user; $tplVars['userid'] = $userid; +/* Update Private Key */ +if (POST_SUBMITTEDPK!='' && $currentUser->getId() == $userid) { + $userinfo = $userservice->getObjectUserByUsername($user); + $tplVars['privateKey'] = $userservice->getNewPrivateKey(); +} + if (POST_SUBMITTED!='' && $currentUser->getId() == $userid) { $error = false; $detPass = trim(POST_PASS); $detPassConf = trim(POST_PASSCONF); $detName = trim(POST_NAME); + $detPrivateKey = trim(POST_PRIVATEKEY); + $detEnablePrivateKey = trim(POST_ENABLEPRIVATEKEY); $detMail = trim(POST_MAIL); $detPage = trim(POST_PAGE); $detDesc = filter(POST_DESC); @@ -102,13 +123,19 @@ if (POST_SUBMITTED!='' && $currentUser->getId() == $userid) { $tplVars['error'] = T_('E-mail address is not valid.'); } if (!$error) { - if (!$userservice->updateUser($userid, $detPass, $detName, $detMail, $detPage, $detDesc)) { + if (!$userservice->updateUser($userid, $detPass, $detName, $detMail, $detPage, $detDesc, $detPrivateKey, $detEnablePrivateKey)) { $tplVars['error'] = T_('An error occurred while saving your changes.'); } else { $tplVars['msg'] = T_('Changes saved.'); } } $userinfo = $userservice->getObjectUserByUsername($user); + $tplVars['privateKey'] = $userinfo->getPrivateKey(true); + if ($userservice->isPrivateKeyValid($userinfo->getPrivateKey())) { + $tplVars['privateKeyIsEnabled'] = 'checked="checked"'; + } else { + $tplVars['privateKeyIsEnabled'] = ''; + } } if (!$userservice->isLoggedOn() || $currentUser->getId() != $userid) { diff --git a/www/rss.php b/www/rss.php index 50240e5..d888726 100644 --- a/www/rss.php +++ b/www/rss.php @@ -64,7 +64,12 @@ if (!isset($rssEntries) || $rssEntries <= 0) { $rssEntries = $maxRssEntries; } +$privateKey = null; +if (isset($_GET['privateKey'])) { + $privateKey = $_GET['privateKey']; +} +$userid = null; $watchlist = null; $pagetitle = ''; if ($user && $user != 'all') { @@ -78,8 +83,22 @@ if ($user && $user != 'all') { } else { if ($userinfo = $userservice->getUserByUsername($user)) { $userid =& $userinfo[$userservice->getFieldName('primary')]; + /* if user is not logged in and has valid privateKey */ + if (!$userservice->isLoggedOn()) { + if ($privateKey != null) { + if (!$userservice->loginPrivateKey($privateKey)) { + $tplVars['error'] = sprintf(T_('Failed to Autenticate User with username %s using private key'), $user); + header('Content-type: text/html; charset=utf-8'); + $templateservice->loadTemplate('error.404.tpl', $tplVars); + //throw a 404 error + exit(); + } + } + } + } else { $tplVars['error'] = sprintf(T_('User with username %s was not found'), $user); + header('Content-type: text/html; charset=utf-8'); $templateservice->loadTemplate('error.404.tpl', $tplVars); //throw a 404 error exit(); @@ -87,7 +106,17 @@ if ($user && $user != 'all') { } $pagetitle .= ": ". $user; } else { - $userid = null; + if ($privateKey != null) { + if (!$userservice->loginPrivateKey($privateKey)) { + $tplVars['error'] = sprintf(T_('Failed to Autenticate User with username %s using private key'), $user); + header('Content-type: text/html; charset=utf-8'); + $templateservice->loadTemplate('error.404.tpl', $tplVars); + //throw a 404 error + exit(); + } + } else { + $userid = null; + } } if ($cat) { @@ -100,7 +129,8 @@ $tplVars['feeddescription'] = sprintf(T_('Recent bookmarks posted to %s'), $GLOB $bookmarks = $bookmarkservice->getBookmarks( 0, $rssEntries, $userid, $cat, - null, getSortOrder(), $watchlist + null, getSortOrder(), $watchlist, + null, null, null ); $bookmarks_tmp = filter($bookmarks['bookmarks']); diff --git a/www/tags.php b/www/tags.php index a4e6555..127f6c5 100644 --- a/www/tags.php +++ b/www/tags.php @@ -67,9 +67,31 @@ if ($usecache) { $tplVars['pagetitle'] = T_('Tags') .': '. $cat; $tplVars['loadjs'] = true; $tplVars['rsschannels'] = array( -array(filter($sitename .': '. $pagetitle), createURL('rss', 'all/'. filter($cat, 'url')).'?sort='.getSortOrder()) + array( + sprintf(T_('%s: tagged with "%s"'), $sitename, $cat), + createURL('rss', 'all/' . filter($cat, 'url')) + . '?sort='.getSortOrder() + ) ); +if ($userservice->isLoggedOn()) { + if ($userservice->isPrivateKeyValid($currentUser->getPrivateKey())) { + $currentUsername = $currentUser->getUsername(); + array_push( + $tplVars['rsschannels'], + array( + sprintf( + T_('%s: tagged with "%s" (+private %s)'), + $sitename, $cat, $currentUsername + ), + createURL('rss', filter($currentUsername, 'url')) + . '?sort=' . getSortOrder() + . '&privateKey=' . $currentUser->getPrivateKey() + ) + ); + } +} + // Pagination $perpage = getPerPageCount($currentUser); if (intval(GET_PAGE) > 1) {