James Moger
2014-06-05 1984375641fe0880f64e378e8116ca433b28dac2
Merged #64 "Editable user profile page"
4 files added
21 files modified
773 ■■■■■ changed files
releases.moxie 2 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/ConfigUserService.java 13 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/FederationClient.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/GitBlit.java 15 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/GitblitManager.java 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/INotificationManager.java 8 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/IRuntimeManager.java 27 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/NotificationManager.java 5 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/RuntimeManager.java 39 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/manager/ServicesManager.java 18 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/models/UserPreferences.java 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/tickets/TicketNotifier.java 11 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties 19 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.html 66 ●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/pages/UserPage.java 192 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/BooleanOption.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/ChoiceOption.html 2 ●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html 46 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java 169 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextAreaOption.html 20 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextAreaOption.java 54 ●●●●● patch | view | raw | blame | history
src/main/java/com/gitblit/wicket/panels/TextOption.html 2 ●●● patch | view | raw | blame | history
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java 15 ●●●●● patch | view | raw | blame | history
releases.moxie
@@ -34,6 +34,8 @@
    - Added CRUD functionality for Ticket Milestones (ticket-17)
    - Implemented Ticket migration tool to move between backends (ticket-19)
    - Added extension points for top nav links, root-level pages, repository nav links, user menu links, and http request filters (ticket-23)
    - Added an editor panel in the user profile page to manipulate preferences (issue-108, issue-424, ticket-64)
    - Added an editor panel in the user profile page to manipulate public SSH keys (ticket-64)
    - Add FORK_REPOSITORY RPC request type (issue-371, pr-161, ticket-65)
    - Add object type (ot) parameter for RSS queries to retrieve tag details (pr-165, ticket-66)
    - Add setting to allow STARTTLS without requiring SMTPS (pr-183)
src/main/java/com/gitblit/ConfigUserService.java
@@ -96,6 +96,8 @@
    private static final String LOCALE = "locale";
    private static final String EMAILONMYTICKETCHANGES = "emailMeOnMyTicketChanges";
    private static final String ACCOUNTTYPE = "accountType";
    private static final String DISABLED = "disabled";
@@ -707,9 +709,11 @@
                config.setBoolean(USER, model.username, DISABLED, true);
            }
            if (model.getPreferences() != null) {
                if (!StringUtils.isEmpty(model.getPreferences().locale)) {
                    config.setString(USER, model.username, LOCALE, model.getPreferences().locale);
                if (model.getPreferences().getLocale() != null) {
                    String val = model.getPreferences().getLocale().getLanguage() + "_" + model.getPreferences().getLocale().getCountry();
                    config.setString(USER, model.username, LOCALE, val);
                }
                config.setBoolean(USER, model.username, EMAILONMYTICKETCHANGES, model.getPreferences().isEmailMeOnMyTicketChanges());
            }
            // user roles
@@ -880,11 +884,14 @@
                    user.stateProvince = config.getString(USER, username, STATEPROVINCE);
                    user.countryCode = config.getString(USER, username, COUNTRYCODE);
                    user.cookie = config.getString(USER, username, COOKIE);
                    user.getPreferences().locale = config.getString(USER, username, LOCALE);
                    if (StringUtils.isEmpty(user.cookie) && !StringUtils.isEmpty(user.password)) {
                        user.cookie = StringUtils.getSHA1(user.username + user.password);
                    }
                    // preferences
                    user.getPreferences().setLocale(config.getString(USER, username, LOCALE));
                    user.getPreferences().setEmailMeOnMyTicketChanges(config.getBoolean(USER, username, EMAILONMYTICKETCHANGES, true));
                    // user roles
                    Set<String> roles = new HashSet<String>(Arrays.asList(config.getStringList(
                            USER, username, ROLE)));
src/main/java/com/gitblit/FederationClient.java
@@ -166,6 +166,11 @@
        }
        @Override
        public boolean isSendingMail() {
            return false;
        }
        @Override
        public void sendMailToAdministrators(String subject, String message) {
        }
src/main/java/com/gitblit/GitBlit.java
@@ -117,6 +117,21 @@
        return servicesManager.isServingRepositories();
    }
    @Override
    public boolean isServingHTTP() {
        return servicesManager.isServingHTTP();
    }
    @Override
    public boolean isServingGIT() {
        return servicesManager.isServingGIT();
    }
    @Override
    public boolean isServingSSH() {
        return servicesManager.isServingSSH();
    }
    protected Object [] getModules() {
        return new Object [] { new GitBlitModule()};
    }
src/main/java/com/gitblit/manager/GitblitManager.java
@@ -602,6 +602,21 @@
    }
    @Override
    public boolean isServingHTTP() {
        return runtimeManager.isServingHTTP();
    }
    @Override
    public boolean isServingGIT() {
        return runtimeManager.isServingGIT();
    }
    @Override
    public boolean isServingSSH() {
        return runtimeManager.isServingSSH();
    }
    @Override
    public TimeZone getTimezone() {
        return runtimeManager.getTimezone();
    }
@@ -646,6 +661,11 @@
     */
    @Override
    public boolean isSendingMail() {
        return notificationManager.isSendingMail();
    }
    @Override
    public void sendMailToAdministrators(String subject, String message) {
        notificationManager.sendMailToAdministrators(subject, message);
    }
src/main/java/com/gitblit/manager/INotificationManager.java
@@ -22,6 +22,14 @@
public interface INotificationManager extends IManager {
    /**
     * Returns true if the email service is configured and ready to send notifications.
     *
     * @return true if the email service is operational
     * @since 1.6.0
     */
    boolean isSendingMail();
    /**
     * Notify the administrators by email.
     *
     * @param subject
src/main/java/com/gitblit/manager/IRuntimeManager.java
@@ -57,6 +57,33 @@
    boolean isServingRepositories();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over HTTP.
     *
     * @return true if Gitblit is serving repositories over HTTP
      * @since 1.6.0
     */
    boolean isServingHTTP();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the GIT Daemon protocol.
     *
     * @return true if Gitblit is serving repositories over the GIT Daemon protocol
      * @since 1.6.0
     */
    boolean isServingGIT();
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the SSH protocol.
     *
     * @return true if Gitblit is serving repositories over the SSH protocol
      * @since 1.6.0
     */
    boolean isServingSSH();
    /**
     * Determine if this Gitblit instance is running in debug mode
     *
     * @return true if Gitblit is running in debug mode
src/main/java/com/gitblit/manager/NotificationManager.java
@@ -71,6 +71,11 @@
        return this;
    }
    @Override
    public boolean isSendingMail() {
        return mailService.isReady();
    }
    /**
     * Notify the administrators by email.
     *
src/main/java/com/gitblit/manager/RuntimeManager.java
@@ -119,9 +119,42 @@
     */
    @Override
    public boolean isServingRepositories() {
        return settings.getBoolean(Keys.git.enableGitServlet, true)
                || (settings.getInteger(Keys.git.daemonPort, 0) > 0)
                || (settings.getInteger(Keys.git.sshPort, 0) > 0);
        return isServingHTTP()
                || isServingGIT()
                || isServingSSH();
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the HTTP protocol.
     *
     * @return true if Gitblit is serving repositories over the HTTP protocol
     */
    @Override
    public boolean isServingHTTP() {
        return settings.getBoolean(Keys.git.enableGitServlet, true);
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the Git Daemon protocol.
     *
     * @return true if Gitblit is serving repositories over the Git Daemon protocol
     */
    @Override
    public boolean isServingGIT() {
        return settings.getInteger(Keys.git.daemonPort, 0) > 0;
    }
    /**
     * Determine if this Gitblit instance is actively serving git repositories
     * over the SSH protocol.
     *
     * @return true if Gitblit is serving repositories over the SSH protocol
     */
    @Override
    public boolean isServingSSH() {
        return settings.getInteger(Keys.git.sshPort, 0) > 0;
    }
    /**
src/main/java/com/gitblit/manager/ServicesManager.java
@@ -112,9 +112,21 @@
    }
    public boolean isServingRepositories() {
        return settings.getBoolean(Keys.git.enableGitServlet, true)
                || (gitDaemon != null && gitDaemon.isRunning())
                || (sshDaemon != null && sshDaemon.isRunning());
        return isServingHTTP()
                || isServingGIT()
                || isServingSSH();
    }
    public boolean isServingHTTP() {
        return settings.getBoolean(Keys.git.enableGitServlet, true);
    }
    public boolean isServingGIT() {
        return gitDaemon != null && gitDaemon.isRunning();
    }
    public boolean isServingSSH() {
        return sshDaemon != null && sshDaemon.isRunning();
    }
    protected void configureFederation() {
src/main/java/com/gitblit/models/UserPreferences.java
@@ -37,7 +37,9 @@
    public final String username;
    public String locale;
    private String locale;
    private Boolean emailMeOnMyTicketChanges;
    private final Map<String, UserRepositoryPreferences> repositoryPreferences = new TreeMap<String, UserRepositoryPreferences>();
@@ -56,6 +58,10 @@
            return new Locale(lang, cc);
        }
        return new Locale(locale);
    }
    public void setLocale(String locale) {
        this.locale = locale;
    }
    public UserRepositoryPreferences getRepositoryPreferences(String repositoryName) {
@@ -96,4 +102,15 @@
        Collections.sort(list);
        return list;
    }
    public boolean isEmailMeOnMyTicketChanges() {
        if (emailMeOnMyTicketChanges == null) {
            return true;
        }
        return emailMeOnMyTicketChanges;
    }
    public void setEmailMeOnMyTicketChanges(boolean value) {
        this.emailMeOnMyTicketChanges = value;
    }
}
src/main/java/com/gitblit/tickets/TicketNotifier.java
@@ -545,7 +545,6 @@
                }
            }
        }
        mailing.setRecipients(toAddresses);
        //
        // CC recipients
@@ -554,7 +553,7 @@
        // repository owners
        if (!ArrayUtils.isEmpty(repository.owners)) {
            tos.addAll(repository.owners);
            ccs.addAll(repository.owners);
        }
        // cc users mentioned in last comment
@@ -595,6 +594,14 @@
        }
        ccAddresses.addAll(settings.getStrings(Keys.mail.mailingLists));
        // respect the author's email preference
        UserModel lastAuthor = userManager.getUserModel(lastChange.author);
        if (!lastAuthor.getPreferences().isEmailMeOnMyTicketChanges()) {
            toAddresses.remove(lastAuthor.emailAddress);
            ccAddresses.remove(lastAuthor.emailAddress);
        }
        mailing.setRecipients(toAddresses);
        mailing.setCCs(ccAddresses);
    }
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties
@@ -722,4 +722,21 @@
gb.gc = GC 
gb.garbageCollection = Garbage Collection
gb.garbageCollectionDescription = The garbage collector will pack loose objects pushed from clients and will remove unreferenced objects from the repository.
gb.commitMessageRendererDescription = Commit messages can be displayed as plaintext or as rendered markup.
gb.commitMessageRendererDescription = Commit messages can be displayed as plaintext or as rendered markup.
gb.preferences = preferences
gb.accountPreferences = Account Preferences
gb.accountPreferencesDescription = Specify your account preferences
gb.languagePreference = Language Preference
gb.languagePreferenceDescription = Select your preferred translation for Gitblit
gb.emailMeOnMyTicketChanges = Email me on my ticket changes
gb.emailMeOnMyTicketChangesDescription  = Send me an email notification for changes that I make to a ticket
gb.displayNameDescription = The preferred name for display
gb.emailAddressDescription = The primary email address for receiving notifications
gb.sshKeys = SSH Keys
gb.sshKeysDescription = SSH public key authentication is a secure alternative to password authentication
gb.addSshKey = Add SSH Key
gb.key = Key
gb.comment = Comment
gb.sshKeyCommentDescription = Enter an optional comment. If blank, the comment will be extracted from the key data.
gb.permission = Permission
gb.sshKeyPermissionDescription = Specify the access permission for the SSH key
src/main/java/com/gitblit/wicket/pages/UserPage.html
@@ -7,27 +7,20 @@
<body>
<wicket:extend>
<div class="container">
    <div class="row" style="padding-top:10px;">
        <div class="span4">
            <div wicket:id="gravatar"></div>
            <div style="text-align: left;">
                <h2><span wicket:id="userDisplayName"></span></h2>
                <div><i class="icon-user"></i> <span wicket:id="userUsername"></span></div>
                <div><i class="icon-envelope"></i><span wicket:id="userEmail"></span></div>
            </div>
    <div class="row" style="padding-top:15px;">
        <div class="span3">
            <div wicket:id="userTitlePanel"></div>
        </div>
        <div class="span8">
            <div class="pull-right">
                <a class="btn-small" wicket:id="newRepository" style="padding-right:0px;">
                    <i class="icon icon-plus-sign"></i>
                    <wicket:message key="gb.newRepository"></wicket:message>
                </a>
            </div>
            <div class="tabbable">
    </div>
    <div class="row" style="padding-top:10px;">
        <div class="span12">
            <div class="tabbable tabs-left">
                <!-- tab titles -->
                <ul class="nav nav-tabs">
                    <li class="active"><a href="#repositories" data-toggle="tab"><wicket:message key="gb.repositories"></wicket:message></a></li>
                    <div wicket:id="preferencesLink"></div>
                    <div wicket:id="sshKeysLink"></div>
                </ul>
    
                <!-- tab content -->
@@ -41,11 +34,50 @@
                            </tbody>
                        </table>
                    </div>
                    <!-- preferences tab -->
                    <div wicket:id="preferencesTab"></div>
                    <!-- ssh keys tab -->
                    <div wicket:id="sshKeysTab"></div>
                </div>
            </div>
        </div>
    </div>
</div>
<wicket:fragment wicket:id="preferencesLinkFragment">
    <li><a href="#preferences" data-toggle="tab"><wicket:message key="gb.preferences"></wicket:message></a></li>
</wicket:fragment>
<wicket:fragment wicket:id="sshKeysLinkFragment">
    <li><a href="#ssh" data-toggle="tab"><wicket:message key="gb.sshKeys"></wicket:message></a></li>
</wicket:fragment>
<wicket:fragment wicket:id="preferencesTabFragment">
    <div class="tab-pane" id="preferences">
        <h4><wicket:message key="gb.accountPreferences"></wicket:message></h4>
        <p><wicket:message key="gb.accountPreferencesDescription"></wicket:message></p>
        <hr />
        <form wicket:id="prefsForm">
            <div wicket:id="displayName"></div>
            <div wicket:id="emailAddress"></div>
            <div wicket:id="language"></div>
            <div wicket:id="emailMeOnMyTicketChanges"></div>
            <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Save" wicket:message="value:gb.save" wicket:id="save" /></div>
        </form>
    </div>
</wicket:fragment>
<wicket:fragment wicket:id="sshKeysTabFragment">
    <div class="tab-pane" id="ssh">
        <div wicket:id="sshKeysPanel"></div>
    </div>
</wicket:fragment>
</wicket:extend>
</body>
</html>
src/main/java/com/gitblit/wicket/pages/UserPage.java
@@ -15,19 +15,27 @@
 */
package com.gitblit.wicket.pages;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import org.apache.wicket.PageParameters;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.Fragment;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.eclipse.jgit.lib.PersonIdent;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.Menu.ParameterMenuItem;
import com.gitblit.models.NavLink;
@@ -40,9 +48,12 @@
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.GitblitRedirectException;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.GravatarImage;
import com.gitblit.wicket.panels.LinkPanel;
import com.gitblit.wicket.panels.BooleanOption;
import com.gitblit.wicket.panels.ChoiceOption;
import com.gitblit.wicket.panels.ProjectRepositoryPanel;
import com.gitblit.wicket.panels.SshKeysPanel;
import com.gitblit.wicket.panels.TextOption;
import com.gitblit.wicket.panels.UserTitlePanel;
public class UserPage extends RootPage {
@@ -83,21 +94,30 @@
            user = new UserModel(userName);
        }
        add(new Label("userDisplayName", user.getDisplayName()));
        add(new Label("userUsername", user.username));
        LinkPanel email = new LinkPanel("userEmail", null, user.emailAddress, "mailto:#");
        email.setRenderBodyOnly(true);
        add(email.setVisible(app().settings().getBoolean(Keys.web.showEmailAddresses, true) && !StringUtils.isEmpty(user.emailAddress)));
        PersonIdent person = new PersonIdent(user.getDisplayName(), user.emailAddress == null ? user.getDisplayName() : user.emailAddress);
        add(new GravatarImage("gravatar", person, 210));
        add(new UserTitlePanel("userTitlePanel", user, user.username));
        UserModel sessionUser = GitBlitWebSession.get().getUser();
        if (sessionUser != null && user.canCreate() && sessionUser.equals(user)) {
            // user can create personal repositories
            add(new BookmarkablePageLink<Void>("newRepository", app().getNewRepositoryPage()));
        boolean isMyProfile = sessionUser != null && sessionUser.equals(user);
        if (isMyProfile) {
            addPreferences(user);
            if (app().gitblit().isServingSSH()) {
                // show the SSH key management tab
                addSshKeys(user);
            } else {
                // SSH daemon is disabled, hide keys tab
                add(new Label("sshKeysLink").setVisible(false));
                add(new Label("sshKeysTab").setVisible(false));
            }
        } else {
            add(new Label("newRepository").setVisible(false));
            // visiting user
            add(new Label("preferencesLink").setVisible(false));
            add(new Label("preferencesTab").setVisible(false));
            add(new Label("sshKeysLink").setVisible(false));
            add(new Label("sshKeysTab").setVisible(false));
        }
        List<RepositoryModel> repositories = getRepositories(params);
@@ -145,4 +165,146 @@
        navLinks.add(menu);
    }
    private void addPreferences(UserModel user) {
        // add preferences
        Form<Void> prefs = new Form<Void>("prefsForm");
        List<Language> languages = Arrays.asList(
                new Language("English","en"),
                new Language("Español", "es"),
                new Language("Français", "fr"),
                new Language("日本語", "ja"),
                new Language("한국말", "ko"),
                new Language("Nederlands", "nl"),
                new Language("Norsk", "no"),
                new Language("Język Polski", "pl"),
                new Language("Português", "pt_BR"),
                new Language("中文", "zh_CN"));
        Locale locale = user.getPreferences().getLocale();
        if (locale == null) {
            // user has not specified language preference
            // try server default preference
            String lc = app().settings().getString(Keys.web.forceDefaultLocale, null);
            if (StringUtils.isEmpty(lc)) {
                // server default language is not configured
                // try browser preference
                Locale sessionLocale = GitBlitWebSession.get().getLocale();
                if (sessionLocale != null) {
                    locale = sessionLocale;
                }
            } else {
            }
        }
        Language preferredLanguage = null;
        if (locale != null) {
            String localeCode = locale.getLanguage();
            if (!StringUtils.isEmpty(locale.getCountry())) {
                localeCode += "_" + locale.getCountry();
            }
            for (Language language : languages) {
                if (language.code.equals(localeCode)) {
                    // language_COUNTRY match
                    preferredLanguage = language;
                } else if (preferredLanguage != null && language.code.startsWith(locale.getLanguage())) {
                    // language match
                    preferredLanguage = language;
                }
            }
        }
        final IModel<String> displayName = Model.of(user.getDisplayName());
        final IModel<String> emailAddress = Model.of(user.emailAddress == null ? "" : user.emailAddress);
        final IModel<Language> language = Model.of(preferredLanguage);
        final IModel<Boolean> emailMeOnMyTicketChanges = Model.of(user.getPreferences().isEmailMeOnMyTicketChanges());
        prefs.add(new TextOption("displayName",
                getString("gb.displayName"),
                getString("gb.displayNameDescription"),
                displayName).setVisible(app().authentication().supportsDisplayNameChanges(user)));
        prefs.add(new TextOption("emailAddress",
                getString("gb.emailAddress"),
                getString("gb.emailAddressDescription"),
                emailAddress).setVisible(app().authentication().supportsEmailAddressChanges(user)));
        prefs.add(new ChoiceOption<Language>("language",
                getString("gb.languagePreference"),
                getString("gb.languagePreferenceDescription"),
                language,
                languages));
        prefs.add(new BooleanOption("emailMeOnMyTicketChanges",
                getString("gb.emailMeOnMyTicketChanges"),
                getString("gb.emailMeOnMyTicketChangesDescription"),
                emailMeOnMyTicketChanges).setVisible(app().notifier().isSendingMail()));
        prefs.add(new AjaxButton("save") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                UserModel user = GitBlitWebSession.get().getUser();
                user.displayName = displayName.getObject();
                user.emailAddress = emailAddress.getObject();
                Language lang = language.getObject();
                if (lang != null) {
                    user.getPreferences().setLocale(lang.code);
                }
                user.getPreferences().setEmailMeOnMyTicketChanges(emailMeOnMyTicketChanges.getObject());
                try {
                    app().gitblit().reviseUser(user.username, user);
                    setRedirect(true);
                    setResponsePage(UserPage.class, WicketUtils.newUsernameParameter(user.username));
                } catch (GitBlitException e) {
                    // logger.error("Failed to update user " + user.username, e);
                    // error(getString("gb.failedToUpdateUser"), false);
                }
            }
        });
        // add the preferences tab
        add(new Fragment("preferencesLink", "preferencesLinkFragment", this).setRenderBodyOnly(true));
        Fragment fragment = new Fragment("preferencesTab", "preferencesTabFragment", this);
        fragment.add(prefs);
        add(fragment.setRenderBodyOnly(true));
    }
    private void addSshKeys(final UserModel user) {
        Fragment keysTab = new Fragment("sshKeysTab", "sshKeysTabFragment", this);
        keysTab.add(new SshKeysPanel("sshKeysPanel", user));
        // add the SSH keys tab
        add(new Fragment("sshKeysLink", "sshKeysLinkFragment", this).setRenderBodyOnly(true));
        add(keysTab.setRenderBodyOnly(true));
    }
    private class Language implements Serializable {
        private static final long serialVersionUID = 1L;
        final String name;
        final String code;
        public Language(String name, String code) {
            this.name = name;
            this.code = code;
        }
        @Override
        public String toString() {
            return name + " (" + code +")";
        }
    }
}
src/main/java/com/gitblit/wicket/panels/AccessPolicyPanel.html
@@ -13,7 +13,7 @@
    <div wicket:id="policiesGroup">
        <div wicket:id="policies" style="padding-top:4px;">
            <div>
                <label style="font-weight:bold;"><input type="radio" wicket:id="radio" /> <img wicket:id="image"></img> <span wicket:id="name"></span></label>
                <label style="font-weight:bold;margin-bottom:1px;"><input type="radio" wicket:id="radio" /> <img wicket:id="image"></img> <span wicket:id="name"></span></label>
            </div>
            <label class="checkbox" style="color:#777;" wicket:id="description"></label>
        </div>
src/main/java/com/gitblit/wicket/panels/BooleanChoiceOption.html
@@ -8,7 +8,7 @@
<wicket:panel>
    <div style="padding-top:4px;">
        <div>
            <label style="font-weight:bold;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
            <label style="font-weight:bold;margin-bottom:1px;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><select class="span3" wicket:id="choice" /></p>
src/main/java/com/gitblit/wicket/panels/BooleanOption.html
@@ -8,7 +8,7 @@
<wicket:panel>
    <div style="padding-top:4px;">
        <div>
            <label style="font-weight:bold;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
            <label style="font-weight:bold;margin-bottom:1px;" class="checkbox"><input type="checkbox" wicket:id="checkbox" /> <span wicket:id="name"></span></label>
        </div>
        <label class="checkbox" style="color:#777;" wicket:id="description"></label>
    </div>
src/main/java/com/gitblit/wicket/panels/ChoiceOption.html
@@ -7,7 +7,7 @@
<body>
<wicket:panel>
    <div style="padding-top:4px;">
        <div>
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.html
New file
@@ -0,0 +1,46 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:panel>
    <h4><wicket:message key="gb.sshKeys"></wicket:message></h4>
    <p><wicket:message key="gb.sshKeysDescription"></wicket:message></p>
    <hr />
    <div wicket:id="keys">
        <div style="display:inline-block;font-size:2em;padding:10px;">
            <i class="fa fa-key"></i>
        </div>
        <div style="display:inline-block;">
            <div wicket:id="comment" style="font-weight:bold;"></div>
            <pre wicket:id="fingerprint"></pre>
        </div>
        <div style="display:inline-block;padding: 0px 20px">
            <div wicket:id="permission" style="font-weight:bold;"></div>
            <div wicket:id="algorithm"></div>
        </div>
        <div style="display:inline-block;vertical-align:text-bottom;">
            <button class="btn btn-danger" wicket:id="delete"><wicket:message key="gb.delete"></wicket:message></button>
        </div>
        <hr />
    </div>
    <div class="well">
        <form wicket:id="addKeyForm">
            <h4><wicket:message key="gb.addSshKey"></wicket:message></h4>
            <div wicket:id="addKeyData"></div>
            <div wicket:id="addKeyPermission"></div>
            <div wicket:id="addKeyComment"></div>
            <div class="form-actions"><input class="btn btn-appmenu" type="submit" value="Add" wicket:message="value:gb.add" wicket:id="addKeyButton" /></div>
        </form>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/SshKeysPanel.java
New file
@@ -0,0 +1,169 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.panels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxButton;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import com.gitblit.Constants.AccessPermission;
import com.gitblit.models.UserModel;
import com.gitblit.transport.ssh.SshKey;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
/**
 * A panel that enumerates and manages SSH public keys using AJAX.
 *
 * @author James Moger
 *
 */
public class SshKeysPanel extends BasePanel {
    private static final long serialVersionUID = 1L;
    private final UserModel user;
    public SshKeysPanel(String wicketId, UserModel user) {
        super(wicketId);
        this.user = user;
    }
    @Override
    protected void onInitialize() {
        super.onInitialize();
        setOutputMarkupId(true);
        final List<SshKey> keys = new ArrayList<SshKey>(app().keys().getKeys(user.username));
        final ListDataProvider<SshKey> dp = new ListDataProvider<SshKey>(keys);
        final DataView<SshKey> keysView = new DataView<SshKey>("keys", dp) {
            private static final long serialVersionUID = 1L;
            @Override
            public void populateItem(final Item<SshKey> item) {
                final SshKey key = item.getModelObject();
                item.add(new Label("comment", key.getComment()));
                item.add(new Label("fingerprint", key.getFingerprint()));
                item.add(new Label("permission", key.getPermission().toString()));
                item.add(new Label("algorithm", key.getAlgorithm()));
                AjaxLink<Void> delete = new AjaxLink<Void>("delete") {
                    private static final long serialVersionUID = 1L;
                    @Override
                    public void onClick(AjaxRequestTarget target) {
                        if (app().keys().removeKey(user.username, key)) {
                            // reset the keys list
                            keys.clear();
                            keys.addAll(app().keys().getKeys(user.username));
                            // update the panel
                            target.addComponent(SshKeysPanel.this);
                        }
                    }
                };
                item.add(delete);
            }
        };
        add(keysView);
        Form<Void> addKeyForm = new Form<Void>("addKeyForm");
        final IModel<String> keyData = Model.of("");
        addKeyForm.add(new TextAreaOption("addKeyData",
                getString("gb.key"),
                null,
                "span5",
                keyData));
        final IModel<AccessPermission> keyPermission = Model.of(AccessPermission.PUSH);
        addKeyForm.add(new ChoiceOption<AccessPermission>("addKeyPermission",
                getString("gb.permission"),
                getString("gb.sshKeyPermissionDescription"),
                keyPermission,
                Arrays.asList(AccessPermission.SSHPERMISSIONS)));
        final IModel<String> keyComment = Model.of("");
        addKeyForm.add(new TextOption("addKeyComment",
                getString("gb.comment"),
                getString("gb.sshKeyCommentDescription"),
                "span5",
                keyComment));
        addKeyForm.add(new AjaxButton("addKeyButton") {
            private static final long serialVersionUID = 1L;
            @Override
            protected void onSubmit(AjaxRequestTarget target, Form<?> form) {
                UserModel user = GitBlitWebSession.get().getUser();
                String data = keyData.getObject();
                if (StringUtils.isEmpty(data)) {
                    // do not submit empty key
                    return;
                }
                SshKey key = new SshKey(data);
                try {
                    key.getPublicKey();
                } catch (Exception e) {
                    // failed to parse the key
                    return;
                }
                AccessPermission permission = keyPermission.getObject();
                key.setPermission(permission);
                String comment  = keyComment.getObject();
                if (!StringUtils.isEmpty(comment)) {
                    key.setComment(comment);
                }
                if (app().keys().addKey(user.username, key)) {
                    // reset add key fields
                    keyData.setObject("");
                    keyPermission.setObject(AccessPermission.PUSH);
                    keyComment.setObject("");
                    // reset the keys list
                    keys.clear();
                    keys.addAll(app().keys().getKeys(user.username));
                    // update the panel
                    target.addComponent(SshKeysPanel.this);
                }
            }
        });
        add(addKeyForm);
    }
}
src/main/java/com/gitblit/wicket/panels/TextAreaOption.html
New file
@@ -0,0 +1,20 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:wicket="http://wicket.apache.org/dtds.data/wicket-xhtml1.3-strict.dtd"
      xml:lang="en"
      lang="en">
<body>
<wicket:panel>
    <div style="padding-top:4px;">
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
        <p style="padding-top:5px;"><textarea rows="12" class="span5" wicket:id="text"></textarea></p>
        </label>
    </div>
</wicket:panel>
</body>
</html>
src/main/java/com/gitblit/wicket/panels/TextAreaOption.java
New file
@@ -0,0 +1,54 @@
/*
 * Copyright 2014 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit.wicket.panels;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.model.IModel;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.WicketUtils;
/**
 * A re-usable textarea option panel.
 *
 * title
 *     description
 *     [text
 *           area]
 *
 * @author James Moger
 *
 */
public class TextAreaOption extends BasePanel {
    private static final long serialVersionUID = 1L;
    public TextAreaOption(String wicketId, String title, String description, IModel<String> model) {
        this(wicketId, title, description, null, model);
    }
    public TextAreaOption(String wicketId, String title, String description, String css, IModel<String> model) {
        super(wicketId);
        add(new Label("name", title));
        add(new Label("description", description).setVisible(!StringUtils.isEmpty(description)));
        TextArea<String> tf = new TextArea<String>("text", model);
        if (!StringUtils.isEmpty(css)) {
            WicketUtils.setCssClass(tf, css);
        }
        add(tf);
    }
}
src/main/java/com/gitblit/wicket/panels/TextOption.html
@@ -7,7 +7,7 @@
<body>
<wicket:panel>
    <div style="padding-top:4px;">
        <div>
        <div style="margin-bottom:1px;">
            <b><span wicket:id="name"></span></b>
        </div>
        <label class="checkbox" style="color:#777;"> <span wicket:id="description"></span>
src/test/java/com/gitblit/tests/mock/MockRuntimeManager.java
@@ -82,6 +82,21 @@
    }
    @Override
    public boolean isServingHTTP() {
        return true;
    }
    @Override
    public boolean isServingGIT() {
        return true;
    }
    @Override
    public boolean isServingSSH() {
        return true;
    }
    @Override
    public boolean isDebugMode() {
        return true;
    }