## Release History\r
\r
+### Current Release\r
+\r
+**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%) | [war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) | [express](http://code.google.com/p/gitblit/downloads/detail?name=%EXPRESS%) | [fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) | [manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) | [api](http://code.google.com/p/gitblit/downloads/detail?name=%API%)) based on [%JGIT%][jgit] *released %BUILDDATE%*\r
+\r
+#### fixes\r
+\r
+- Can't set reset settings with $ or { characters through Gitblit Manager because they are not properly escaped\r
+\r
+#### additions\r
+\r
++ - Optional periodic LDAP user and team pre-fetching & synchronization (github/mschaefers)\r
+ - Display name and version in Tomcat Manager (github/thefake) \r
+ - FogBugz post-receive hook script (github/djschny)\r
+ - Implemented multiple repository owners (github/akquinet)\r
+ - Chinese translation (github/dapengme, github/yin8086)\r
+\r
+### Older Releases\r
+\r
<div class="alert alert-info">\r
-<h4>Update Note</h4>\r
-The permissions model has changed in this release.\r
-<p>If you are updating your server, you must also update any Gitblit Manager and Federation Client installs to 1.2.0 as well. The data model used by the RPC mechanism has changed slightly for the new permissions infrastructure.</p>\r
+<h4>Update Note 1.2.1</h4>\r
+Because there are now several types of files and folders that must be considered Gitblit data, the default location for data has changed.\r
+<p>You will need to move a few files around when upgrading. Please see the Upgrading section of the <a href="setup.html">setup</a> page for details.</p>\r
+\r
+<b>Express Users</b> make sure to update your web.xml file with the ${baseFolder} values!\r
</div>\r
\r
-### Current Release\r
+#### fixes\r
\r
-**%VERSION%** ([go](http://code.google.com/p/gitblit/downloads/detail?name=%GO%) | [war](http://code.google.com/p/gitblit/downloads/detail?name=%WAR%) | [express](http://code.google.com/p/gitblit/downloads/detail?name=%EXPRESS%) | [fedclient](http://code.google.com/p/gitblit/downloads/detail?name=%FEDCLIENT%) | [manager](http://code.google.com/p/gitblit/downloads/detail?name=%MANAGER%) | [api](http://code.google.com/p/gitblit/downloads/detail?name=%API%)) based on [%JGIT%][jgit] *released %BUILDDATE%*\r
+- Fixed nullpointer on recursively calculating folder sizes when there is a named pipe or symlink in the hierarchy\r
+- Added nullchecking when concurrently forking a repository and trying to display it's fork network (issue-187)\r
+- Fixed bug where permission changes were not visible in the web ui to a logged-in user until the user logged-out and then logged back in again (issue-186)\r
+- Fixed nullpointer on creating a repository with mixed case (issue 185)\r
+- Include missing model classes in api library (issue-184)\r
+- Fixed nullpointer when using *web.allowForking = true* && *git.cacheRepositoryList = false* (issue 182)\r
+- Likely fix for commit and commitdiff page failures when a submodule reference changes (issue 178)\r
+- Build project models from the repository model cache, when possible, to reduce page load time (issue 172)\r
+- Fixed loading of Brazilian Portuguese translation from *nix server (github/inaiat)\r
+\r
+#### additions\r
+\r
+- Fanout PubSub service for self-hosted [Sparkleshare](http://sparkleshare.org) notifications.<br/>\r
+This service is disabled by default.<br/>\r
+ **New:** *fanout.bindInterface = localhost*<br/>\r
+ **New:** *fanout.port = 0*<br/>\r
+ **New:** *fanout.useNio = true*<br/>\r
+ **New:** *fanout.connectionLimit = 0*\r
+- Implemented a simple push log based on a hidden, orphan branch refs/gitblit/pushes (issue 177)<br/>\r
+The push log is not currently visible in the ui, but the data will be collected and it will be exposed to the ui in the next release.\r
+- Support for locally and remotely authenticated accounts in LdapUserService and RedmineUserService (issue 183)\r
+- Added Dutch translation (github/kwoot)\r
+\r
+#### changes\r
+\r
+- Gitblit GO and Gitblit WAR are now both configured by `gitblit.properties`. WAR is no longer configured by `web.xml`.<br/>\r
+However, Express for OpenShift continues to be configured by `web.xml`.\r
+- Support for a *--baseFolder* command-line argument for Gitblit GO and Gitblit Certificate Authority\r
+- Support for specifying a *${baseFolder}* parameter in `gitblit.properties` and `web.xml` for several settings\r
+- Improve history display of a submodule link\r
+- Updated Korean translation (github/ds5apn)\r
+- Updated checkstyle definition (github/mystygage)\r
+\r
+<div class="alert alert-info">\r
+<h4>Update Note 1.2.0</h4>\r
+The permissions model has changed in the 1.2.0 release.\r
+<p>If you are updating your server, you must also update any Gitblit Manager and Federation Client installs to 1.2.0 as well. The data model used by the RPC mechanism has changed slightly for the new permissions infrastructure.</p>\r
+</div>\r
+\r
+**1.2.0** *released 2012-12-31*\r
\r
#### fixes\r
\r
import java.net.URI;\r
import java.net.URISyntaxException;\r
import java.security.GeneralSecurityException;\r
+ import java.util.HashMap;\r
import java.util.List;\r
+ import java.util.Map;\r
+ import java.util.concurrent.TimeUnit;\r
++import java.util.concurrent.atomic.AtomicLong;\r
\r
import org.slf4j.Logger;\r
import org.slf4j.LoggerFactory;\r
public class LdapUserService extends GitblitUserService {\r
\r
public static final Logger logger = LoggerFactory.getLogger(LdapUserService.class);\r
- public static final String LDAP_PASSWORD_KEY = "StoredInLDAP";\r
-\r
- private IStoredSettings settings;\r
- private long lastLdapUserSyncTs = 0L;\r
- private long ldapSyncCachePeriod;\r
\r
+ private IStoredSettings settings;\r
++ private AtomicLong lastLdapUserSync = new AtomicLong(0L);\r
+ \r
public LdapUserService() {\r
super();\r
}\r
\r
- private void initializeLdapCaches() {\r
++ private long getSynchronizationPeriod() {\r
+ final String cacheDuration = settings.getString(Keys.realm.ldap.ldapCachePeriod, "2 MINUTES");\r
- final long duration;\r
- final TimeUnit timeUnit;\r
+ try {\r
+ final String[] s = cacheDuration.split(" ", 2);\r
- duration = Long.parseLong(s[0]);\r
- timeUnit = TimeUnit.valueOf(s[1]);\r
- ldapSyncCachePeriod = timeUnit.toMillis(duration);\r
++ long duration = Long.parseLong(s[0]);\r
++ TimeUnit timeUnit = TimeUnit.valueOf(s[1]);\r
++ return timeUnit.toMillis(duration);\r
+ } catch (RuntimeException ex) {\r
+ throw new IllegalArgumentException(Keys.realm.ldap.ldapCachePeriod + " must have format '<long> <TimeUnit>' where <TimeUnit> is one of 'MILLISECONDS', 'SECONDS', 'MINUTES', 'HOURS', 'DAYS'");\r
+ }\r
+ }\r
-\r
++ \r
@Override\r
public void setup(IStoredSettings settings) {\r
this.settings = settings;\r
- String file = settings.getString(Keys.realm.ldap.backingUserService, "users.conf");\r
+ String file = settings.getString(Keys.realm.ldap.backingUserService, "${baseFolder}/users.conf");\r
File realmFile = GitBlit.getFileOrFolder(file);\r
--\r
- initializeLdapCaches();\r
- \r
++ \r
serviceImpl = createUserService(realmFile);\r
logger.info("LDAP User Service backed by " + serviceImpl.toString());\r
-\r
- synchronizeLdapUsers();\r
- }\r
-\r
- protected synchronized void synchronizeLdapUsers() {\r
++ \r
++ synchronizeLdapUsers();\r
+ }\r
+ \r
++ protected synchronized void synchronizeLdapUsers() {\r
+ final boolean enabled = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.enable, false);\r
+ if (enabled) {\r
- if (lastLdapUserSyncTs + ldapSyncCachePeriod < System.currentTimeMillis()) {\r
++ if (System.currentTimeMillis() > (lastLdapUserSync.get() + getSynchronizationPeriod())) {\r
++ logger.info("Synchronizing with LDAP @ " + settings.getRequiredString(Keys.realm.ldap.server));\r
+ final boolean deleteRemovedLdapUsers = settings.getBoolean(Keys.realm.ldap.synchronizeUsers.removeDeleted, true);\r
+ LDAPConnection ldapConnection = getLdapConnection();\r
+ if (ldapConnection != null) {\r
+ try {\r
+ String accountBase = settings.getString(Keys.realm.ldap.accountBase, "");\r
+ String uidAttribute = settings.getString(Keys.realm.ldap.uid, "uid");\r
+ String accountPattern = settings.getString(Keys.realm.ldap.accountPattern, "(&(objectClass=person)(sAMAccountName=${username}))");\r
+ accountPattern = StringUtils.replace(accountPattern, "${username}", "*");\r
+ \r
+ SearchResult result = doSearch(ldapConnection, accountBase, accountPattern);\r
+ if (result != null && result.getEntryCount() > 0) {\r
+ final Map<String, UserModel> ldapUsers = new HashMap<String, UserModel>();\r
+ \r
+ for (SearchResultEntry loggingInUser : result.getSearchEntries()) {\r
+ \r
+ final String username = loggingInUser.getAttribute(uidAttribute).getValue();\r
+ logger.debug("LDAP synchronizing: " + username);\r
+ \r
+ UserModel user = getUserModel(username);\r
+ if (user == null) {\r
+ user = new UserModel(username);\r
+ }\r
+ \r
+ if (!supportsTeamMembershipChanges())\r
+ getTeamsFromLdap(ldapConnection, username, loggingInUser, user);\r
+ \r
+ // Get User Attributes\r
+ setUserAttributes(user, loggingInUser);\r
+ \r
+ // store in map\r
- ldapUsers.put(username, user);\r
++ ldapUsers.put(username.toLowerCase(), user);\r
+ }\r
+ \r
+ if (deleteRemovedLdapUsers) {\r
+ logger.debug("detecting removed LDAP users...");\r
+ \r
+ for (UserModel userModel : super.getAllUsers()) {\r
- if (LDAP_PASSWORD_KEY.equals(userModel.password)) {\r
++ if (ExternalAccount.equals(userModel.password)) {\r
+ if (! ldapUsers.containsKey(userModel.username)) {\r
+ logger.info("deleting removed LDAP user " + userModel.username + " from backing user service");\r
+ super.deleteUser(userModel.username);\r
+ }\r
+ }\r
+ }\r
+ }\r
+ \r
+ super.updateUserModels(ldapUsers.values());\r
+ \r
+ if (!supportsTeamMembershipChanges()) {\r
+ final Map<String, TeamModel> userTeams = new HashMap<String, TeamModel>();\r
+ for (UserModel user : ldapUsers.values()) {\r
+ for (TeamModel userTeam : user.teams) {\r
+ userTeams.put(userTeam.name, userTeam);\r
+ }\r
+ }\r
+ updateTeamModels(userTeams.values());\r
+ }\r
+ }\r
- lastLdapUserSyncTs = System.currentTimeMillis(); \r
++ lastLdapUserSync.set(System.currentTimeMillis()); \r
+ } finally {\r
+ ldapConnection.close();\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
-\r
- private LDAPConnection getLdapConnection() {\r
- try {\r
++ \r
+ private LDAPConnection getLdapConnection() {\r
+ try {\r
URI ldapUrl = new URI(settings.getRequiredString(Keys.realm.ldap.server));\r
String bindUserName = settings.getString(Keys.realm.ldap.username, "");\r
String bindPassword = settings.getString(Keys.realm.ldap.password, "");\r
if (isAuthenticated(ldapConnection, loggingInUserDN, new String(password))) {\r
logger.debug("LDAP authenticated: " + username);\r
\r
-- UserModel user = getUserModel(simpleUsername);\r
-- if (user == null) // create user object for new authenticated user\r
-- user = new UserModel(simpleUsername);\r
-\r
- // create a user cookie\r
- if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {\r
- user.cookie = StringUtils.getSHA1(user.username + new String(password));\r
++ UserModel user = null;\r
++ synchronized (this) {\r
++ user = getUserModel(simpleUsername);\r
++ if (user == null) // create user object for new authenticated user\r
++ user = new UserModel(simpleUsername);\r
+\r
- // create a user cookie\r
- if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {\r
- user.cookie = StringUtils.getSHA1(user.username + new String(password));\r
- }\r
++ // create a user cookie\r
++ if (StringUtils.isEmpty(user.cookie) && !ArrayUtils.isEmpty(password)) {\r
++ user.cookie = StringUtils.getSHA1(user.username + new String(password));\r
++ }\r
+\r
- if (!supportsTeamMembershipChanges())\r
- getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);\r
++ if (!supportsTeamMembershipChanges())\r
++ getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);\r
+\r
- // Get User Attributes\r
- setUserAttributes(user, loggingInUser);\r
++ // Get User Attributes\r
++ setUserAttributes(user, loggingInUser);\r
+\r
- // Push the ldap looked up values to backing file\r
- super.updateUserModel(user);\r
- if (!supportsTeamMembershipChanges()) {\r
- for (TeamModel userTeam : user.teams)\r
- updateTeamModel(userTeam);\r
++ // Push the ldap looked up values to backing file\r
++ super.updateUserModel(user);\r
++ if (!supportsTeamMembershipChanges()) {\r
++ for (TeamModel userTeam : user.teams)\r
++ updateTeamModel(userTeam);\r
++ }\r
}\r
-\r
- if (!supportsTeamMembershipChanges())\r
- getTeamsFromLdap(ldapConnection, simpleUsername, loggingInUser, user);\r
-\r
- // Get User Attributes\r
- setUserAttributes(user, loggingInUser);\r
-\r
- // Push the ldap looked up values to backing file\r
- super.updateUserModel(user);\r
- if (!supportsTeamMembershipChanges()) {\r
- for (TeamModel userTeam : user.teams)\r
- updateTeamModel(userTeam);\r
- }\r
--\r
++ \r
return user;\r
}\r
}\r
}\r
}\r
\r
-\r
+ @Override\r
+ public List<String> getAllUsernames() {\r
+ synchronizeLdapUsers();\r
+ return super.getAllUsernames();\r
+ }\r
+ \r
+ @Override\r
+ public List<UserModel> getAllUsers() {\r
+ synchronizeLdapUsers();\r
+ return super.getAllUsers();\r
+ }\r
-\r
- /**\r
+ \r
+ /**\r
* Returns a simple username without any domain prefixes.\r
- *\r
- * @param username\r
+ * \r
+ * @param username\r
* @return a simple username\r
*/\r
protected String getSimpleUsername(String username) {\r