]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6579 add support of gravatars
authorStas Vilchik <vilchiks@gmail.com>
Fri, 22 May 2015 13:35:28 +0000 (15:35 +0200)
committerStas Vilchik <vilchiks@gmail.com>
Mon, 25 May 2015 12:53:25 +0000 (14:53 +0200)
15 files changed:
server/sonar-web/Gruntfile.coffee
server/sonar-web/src/main/coffee/apps/issues/models/issues.coffee
server/sonar-web/src/main/coffee/components/issue/collections/issues.coffee
server/sonar-web/src/main/coffee/components/issue/templates/issue.hbs
server/sonar-web/src/main/js/apps/nav/global-navbar-view.js
server/sonar-web/src/main/js/apps/nav/templates/nav-global-navbar.hbs
server/sonar-web/src/main/js/apps/users/templates/users-list-item.hbs
server/sonar-web/src/main/js/components/common/handlebars-extensions.js
server/sonar-web/src/main/js/components/source-viewer/templates/source-viewer.hbs
server/sonar-web/src/main/js/libs/application.js
server/sonar-web/src/main/js/libs/third-party/md5.js [new file with mode: 0644]
server/sonar-web/src/main/less/components/source.less
server/sonar-web/src/main/webapp/WEB-INF/app/views/layouts/_head.html.erb
server/sonar-web/src/test/views/layouts/main.jade
sonar-core/src/main/java/org/sonar/core/config/CorePropertyDefinitions.java

index 82a5708b8f9931914578bc98b8e8d1defd27c021..3f33fd9dc95c89b1853db0863d2a103cd4dc2dfc 100644 (file)
@@ -66,6 +66,7 @@ module.exports = (grunt) ->
             '<%= BUILD_PATH %>/js/libs/third-party/numeral-languages.js'
             '<%= BUILD_PATH %>/js/libs/third-party/bootstrap/tooltip.js'
             '<%= BUILD_PATH %>/js/libs/third-party/bootstrap/dropdown.js'
+            '<%= BUILD_PATH %>/js/libs/third-party/md5.js'
             '<%= BUILD_PATH %>/js/libs/select2-jquery-ui-fix.js'
 
             '<%= BUILD_PATH %>/js/libs/widgets/base.js'
index 08926040335bedd8022e3301eaae5bd29d00941d..d98b263212067996a7e8ead00e1def881f3f8731 100644 (file)
@@ -43,6 +43,7 @@ define [
         project = find r.projects, issue.project
         subProject = find r.components, issue.subProject
         rule = find r.rules, issue.rule
+        assignee = find r.users, issue.assignee, 'login'
 
         _.extend issue,
           index: index
@@ -67,25 +68,9 @@ define [
           _.extend issue,
             ruleName: rule.name
 
-        if _.isArray(issue.sources) && issue.sources.length > 0
-          source = ''
-          issue.sources.forEach (line) ->
-            source = line[1] if line[0] == issue.line
-          _.extend issue, source: source
-
-
-        if _.isArray(issue.scm) && issue.scm.length > 0
-          scmAuthor = ''
-          scmDate = ''
-
-          issue.scm.forEach (line) ->
-            if line[0] == issue.line
-              scmAuthor = line[1]
-              scmDate = line[2]
-
+        if assignee
           _.extend issue,
-            scmAuthor: scmAuthor
-            scmDate: scmDate
+            assigneeEmail: assignee.email
 
         issue
 
index ddce4332d2ebfddf68cefea0435255cca3c04568..208a06ffb3b8b762bc23808958909d17a643d5a2 100644 (file)
@@ -47,6 +47,7 @@ define [
         component = find r.components, issue.component
         project = find r.projects, issue.project
         rule = find r.rules, issue.rule
+        assignee = find r.users, issue.assignee, 'login'
 
         if component
           _.extend issue,
@@ -62,4 +63,8 @@ define [
           _.extend issue,
             ruleName: rule.name
 
+        if assignee
+          _.extend issue,
+            assigneeEmail: assignee.email
+
         issue
index dc47a49ddb96b329bbf9b3d0f5b5ec104d45461c..b27f6b53c81f61db8a25d99391025a0d9987afea 100644 (file)
           <div class="issue-meta">
             {{#inArray actions "assign"}}
               <a class="issue-action issue-action-with-options js-issue-assign">
-          <span
-              class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span>&nbsp;<i
-                  class="icon-dropdown"></i>
+                {{#if assignee}}
+                  {{#ifShowAvatars}}
+                    <span class="text-top">{{avatarHelper assigneeEmail 16}}</span>
+                  {{/ifShowAvatars}}
+                {{/if}}
+                <span class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span>&nbsp;<i class="icon-dropdown"></i>
               </a>
             {{else}}
-              <span
-                  class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span>
+              {{#if assignee}}
+                {{#ifShowAvatars}}
+                  <span class="text-top">{{avatarHelper assigneeEmail 16}}</span>
+                {{/ifShowAvatars}}
+              {{/if}}
+              <span class="issue-meta-label">{{#if assignee}}{{default assigneeName assignee}}{{else}}{{t 'unassigned'}}{{/if}}</span>
             {{/inArray}}
           </div>
 
index 9ee43d9fdfe8272bcb38e85848af38838738d375..5a0915e7b20061066c74864d993ff6869c360c19 100644 (file)
@@ -85,6 +85,7 @@ define([
       return _.extend(Marionette.Layout.prototype.serializeData.apply(this, arguments), {
         user: window.SS.user,
         userName: window.SS.userName,
+        userEmail: window.SS.userEmail,
         isUserAdmin: window.SS.isUserAdmin,
 
         canManageGlobalDashboards: !!window.SS.user,
index fcaf9cc2672906b3fe026f3f6cc064201c2477c7..11284eee4a9050f6614ff0fc651846c44fc74ed1 100644 (file)
@@ -71,7 +71,7 @@
     {{#if user}}
       <li class="dropdown">
         <a class="dropdown-toggle" data-toggle="dropdown" href="#">
-          {{userName}}&nbsp;<span class="icon-dropdown"></span>
+          {{#ifShowAvatars}}<span class="little-spacer-right">{{avatarHelper userEmail 20}}&nbsp;{{/ifShowAvatars}}</span>{{userName}}&nbsp;<span class="icon-dropdown"></span>
         </a>
         <ul class="dropdown-menu dropdown-menu-right">
           <li>
index 1cd782dd9ed657edbd63dec74d6c480978bbc8de..c3d34d75f535d013e64ed1faaf0a76fdec540683 100644 (file)
@@ -5,6 +5,12 @@
   <a class="js-user-deactivate icon-delete" title="Deactivate" data-toggle="tooltip" href="#"></a>
 </div>
 
+{{#ifShowAvatars}}
+  <div class="display-inline-block text-top big-spacer-right">
+    {{avatarHelper email 36}}
+  </div>
+{{/ifShowAvatars}}
+
 <div class="display-inline-block text-top width-30">
   <div>
     <strong class="js-user-name">{{name}}</strong>
index 610d5425dc2fe580ea08d6f11bab1bcd41635783..a92f2d1902621e2b09a1d0f13144c158f210ed41 100644 (file)
     }
   });
 
+  Handlebars.registerHelper('ifShowAvatars', function (options) {
+    var cond = window.SS && window.SS.lf && window.SS.lf.enableGravatar;
+    return cond ? options.fn(this) : options.inverse(this);
+  });
+
+  Handlebars.registerHelper('avatarHelper', function (email, size) {
+    var emailHash = window.md5((email || '').trim()),
+        url = ('' + window.SS.lf.gravatarServerUrl)
+            .replace('{EMAIL_MD5}', emailHash)
+            .replace('{SIZE}', size);
+    return new Handlebars.SafeString(
+        '<img src="' + url + '" width="' + size + '" height="' + size + '" alt="' + email + '">'
+    );
+  });
+
 })();
index 123d21e86a29c81fcccc2f955c6f0f1d9b2f5649..1f4c3ffeb190127da4a2ea7d6da89f31cbc7bd15 100644 (file)
 
         <td class="source-meta source-line-scm" {{#if line}}data-line-number="{{line}}"{{/if}}>
           {{#ifSCMChanged2 ../source line}}
-            <div class="source-line-scm-inner" data-author="{{scmAuthor}}"></div>
+            <div class="source-line-scm-inner" data-author="{{scmAuthor}}">
+              {{#gt line 0}}
+                {{avatarHelper scmAuthor 16}}
+              {{/gt}}
+            </div>
           {{/ifSCMChanged2}}
         </td>
 
index 2d16dd4ff5ddba6474d2f601ffde3729501304b6..6b7fc9134886425fe9969e2655d1f4dac3387321 100644 (file)
@@ -549,6 +549,20 @@ function closeModalWindow () {
     return params;
   };
 
+
+  /**
+   * Return an md5 hash of a string
+   * @param s
+   * @returns {*}
+   */
+  window.getMD5Hash = function (s) {
+    if (typeof s === 'string') {
+      return window.md5(s.trim());
+    } else {
+      return null;
+    }
+  };
+
 })();
 
 (function () {
diff --git a/server/sonar-web/src/main/js/libs/third-party/md5.js b/server/sonar-web/src/main/js/libs/third-party/md5.js
new file mode 100644 (file)
index 0000000..f92ba37
--- /dev/null
@@ -0,0 +1,274 @@
+/*
+ * JavaScript MD5 1.0.1
+ * https://github.com/blueimp/JavaScript-MD5
+ *
+ * Copyright 2011, Sebastian Tschan
+ * https://blueimp.net
+ *
+ * Licensed under the MIT license:
+ * http://www.opensource.org/licenses/MIT
+ * 
+ * Based on
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
+ * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
+ * Distributed under the BSD License
+ * See http://pajhome.org.uk/crypt/md5 for more info.
+ */
+
+/*jslint bitwise: true */
+/*global unescape, define */
+
+(function ($) {
+    'use strict';
+
+    /*
+    * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+    * to work around bugs in some JS interpreters.
+    */
+    function safe_add(x, y) {
+        var lsw = (x & 0xFFFF) + (y & 0xFFFF),
+            msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+        return (msw << 16) | (lsw & 0xFFFF);
+    }
+
+    /*
+    * Bitwise rotate a 32-bit number to the left.
+    */
+    function bit_rol(num, cnt) {
+        return (num << cnt) | (num >>> (32 - cnt));
+    }
+
+    /*
+    * These functions implement the four basic operations the algorithm uses.
+    */
+    function md5_cmn(q, a, b, x, s, t) {
+        return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b);
+    }
+    function md5_ff(a, b, c, d, x, s, t) {
+        return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
+    }
+    function md5_gg(a, b, c, d, x, s, t) {
+        return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
+    }
+    function md5_hh(a, b, c, d, x, s, t) {
+        return md5_cmn(b ^ c ^ d, a, b, x, s, t);
+    }
+    function md5_ii(a, b, c, d, x, s, t) {
+        return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
+    }
+
+    /*
+    * Calculate the MD5 of an array of little-endian words, and a bit length.
+    */
+    function binl_md5(x, len) {
+        /* append padding */
+        x[len >> 5] |= 0x80 << (len % 32);
+        x[(((len + 64) >>> 9) << 4) + 14] = len;
+
+        var i, olda, oldb, oldc, oldd,
+            a =  1732584193,
+            b = -271733879,
+            c = -1732584194,
+            d =  271733878;
+
+        for (i = 0; i < x.length; i += 16) {
+            olda = a;
+            oldb = b;
+            oldc = c;
+            oldd = d;
+
+            a = md5_ff(a, b, c, d, x[i],       7, -680876936);
+            d = md5_ff(d, a, b, c, x[i +  1], 12, -389564586);
+            c = md5_ff(c, d, a, b, x[i +  2], 17,  606105819);
+            b = md5_ff(b, c, d, a, x[i +  3], 22, -1044525330);
+            a = md5_ff(a, b, c, d, x[i +  4],  7, -176418897);
+            d = md5_ff(d, a, b, c, x[i +  5], 12,  1200080426);
+            c = md5_ff(c, d, a, b, x[i +  6], 17, -1473231341);
+            b = md5_ff(b, c, d, a, x[i +  7], 22, -45705983);
+            a = md5_ff(a, b, c, d, x[i +  8],  7,  1770035416);
+            d = md5_ff(d, a, b, c, x[i +  9], 12, -1958414417);
+            c = md5_ff(c, d, a, b, x[i + 10], 17, -42063);
+            b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162);
+            a = md5_ff(a, b, c, d, x[i + 12],  7,  1804603682);
+            d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101);
+            c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290);
+            b = md5_ff(b, c, d, a, x[i + 15], 22,  1236535329);
+
+            a = md5_gg(a, b, c, d, x[i +  1],  5, -165796510);
+            d = md5_gg(d, a, b, c, x[i +  6],  9, -1069501632);
+            c = md5_gg(c, d, a, b, x[i + 11], 14,  643717713);
+            b = md5_gg(b, c, d, a, x[i],      20, -373897302);
+            a = md5_gg(a, b, c, d, x[i +  5],  5, -701558691);
+            d = md5_gg(d, a, b, c, x[i + 10],  9,  38016083);
+            c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335);
+            b = md5_gg(b, c, d, a, x[i +  4], 20, -405537848);
+            a = md5_gg(a, b, c, d, x[i +  9],  5,  568446438);
+            d = md5_gg(d, a, b, c, x[i + 14],  9, -1019803690);
+            c = md5_gg(c, d, a, b, x[i +  3], 14, -187363961);
+            b = md5_gg(b, c, d, a, x[i +  8], 20,  1163531501);
+            a = md5_gg(a, b, c, d, x[i + 13],  5, -1444681467);
+            d = md5_gg(d, a, b, c, x[i +  2],  9, -51403784);
+            c = md5_gg(c, d, a, b, x[i +  7], 14,  1735328473);
+            b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734);
+
+            a = md5_hh(a, b, c, d, x[i +  5],  4, -378558);
+            d = md5_hh(d, a, b, c, x[i +  8], 11, -2022574463);
+            c = md5_hh(c, d, a, b, x[i + 11], 16,  1839030562);
+            b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556);
+            a = md5_hh(a, b, c, d, x[i +  1],  4, -1530992060);
+            d = md5_hh(d, a, b, c, x[i +  4], 11,  1272893353);
+            c = md5_hh(c, d, a, b, x[i +  7], 16, -155497632);
+            b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640);
+            a = md5_hh(a, b, c, d, x[i + 13],  4,  681279174);
+            d = md5_hh(d, a, b, c, x[i],      11, -358537222);
+            c = md5_hh(c, d, a, b, x[i +  3], 16, -722521979);
+            b = md5_hh(b, c, d, a, x[i +  6], 23,  76029189);
+            a = md5_hh(a, b, c, d, x[i +  9],  4, -640364487);
+            d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835);
+            c = md5_hh(c, d, a, b, x[i + 15], 16,  530742520);
+            b = md5_hh(b, c, d, a, x[i +  2], 23, -995338651);
+
+            a = md5_ii(a, b, c, d, x[i],       6, -198630844);
+            d = md5_ii(d, a, b, c, x[i +  7], 10,  1126891415);
+            c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905);
+            b = md5_ii(b, c, d, a, x[i +  5], 21, -57434055);
+            a = md5_ii(a, b, c, d, x[i + 12],  6,  1700485571);
+            d = md5_ii(d, a, b, c, x[i +  3], 10, -1894986606);
+            c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523);
+            b = md5_ii(b, c, d, a, x[i +  1], 21, -2054922799);
+            a = md5_ii(a, b, c, d, x[i +  8],  6,  1873313359);
+            d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744);
+            c = md5_ii(c, d, a, b, x[i +  6], 15, -1560198380);
+            b = md5_ii(b, c, d, a, x[i + 13], 21,  1309151649);
+            a = md5_ii(a, b, c, d, x[i +  4],  6, -145523070);
+            d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379);
+            c = md5_ii(c, d, a, b, x[i +  2], 15,  718787259);
+            b = md5_ii(b, c, d, a, x[i +  9], 21, -343485551);
+
+            a = safe_add(a, olda);
+            b = safe_add(b, oldb);
+            c = safe_add(c, oldc);
+            d = safe_add(d, oldd);
+        }
+        return [a, b, c, d];
+    }
+
+    /*
+    * Convert an array of little-endian words to a string
+    */
+    function binl2rstr(input) {
+        var i,
+            output = '';
+        for (i = 0; i < input.length * 32; i += 8) {
+            output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF);
+        }
+        return output;
+    }
+
+    /*
+    * Convert a raw string to an array of little-endian words
+    * Characters >255 have their high-byte silently ignored.
+    */
+    function rstr2binl(input) {
+        var i,
+            output = [];
+        output[(input.length >> 2) - 1] = undefined;
+        for (i = 0; i < output.length; i += 1) {
+            output[i] = 0;
+        }
+        for (i = 0; i < input.length * 8; i += 8) {
+            output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32);
+        }
+        return output;
+    }
+
+    /*
+    * Calculate the MD5 of a raw string
+    */
+    function rstr_md5(s) {
+        return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
+    }
+
+    /*
+    * Calculate the HMAC-MD5, of a key and some data (raw strings)
+    */
+    function rstr_hmac_md5(key, data) {
+        var i,
+            bkey = rstr2binl(key),
+            ipad = [],
+            opad = [],
+            hash;
+        ipad[15] = opad[15] = undefined;
+        if (bkey.length > 16) {
+            bkey = binl_md5(bkey, key.length * 8);
+        }
+        for (i = 0; i < 16; i += 1) {
+            ipad[i] = bkey[i] ^ 0x36363636;
+            opad[i] = bkey[i] ^ 0x5C5C5C5C;
+        }
+        hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
+        return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
+    }
+
+    /*
+    * Convert a raw string to a hex string
+    */
+    function rstr2hex(input) {
+        var hex_tab = '0123456789abcdef',
+            output = '',
+            x,
+            i;
+        for (i = 0; i < input.length; i += 1) {
+            x = input.charCodeAt(i);
+            output += hex_tab.charAt((x >>> 4) & 0x0F) +
+                hex_tab.charAt(x & 0x0F);
+        }
+        return output;
+    }
+
+    /*
+    * Encode a string as utf-8
+    */
+    function str2rstr_utf8(input) {
+        return unescape(encodeURIComponent(input));
+    }
+
+    /*
+    * Take string arguments and return either raw or hex encoded strings
+    */
+    function raw_md5(s) {
+        return rstr_md5(str2rstr_utf8(s));
+    }
+    function hex_md5(s) {
+        return rstr2hex(raw_md5(s));
+    }
+    function raw_hmac_md5(k, d) {
+        return rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d));
+    }
+    function hex_hmac_md5(k, d) {
+        return rstr2hex(raw_hmac_md5(k, d));
+    }
+
+    function md5(string, key, raw) {
+        if (!key) {
+            if (!raw) {
+                return hex_md5(string);
+            }
+            return raw_md5(string);
+        }
+        if (!raw) {
+            return hex_hmac_md5(key, string);
+        }
+        return raw_hmac_md5(key, string);
+    }
+
+    if (typeof define === 'function' && define.amd) {
+        define(function () {
+            return md5;
+        });
+    } else {
+        $.md5 = md5;
+    }
+}(this));
index 6e02fe6e7adcc2333948e6adfd926b7fa017d5c1..c2f8237c6380522c935b0fa4e31849cb9c76476b 100644 (file)
 
 .source-line-scm-inner {
   max-width: 40px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-
-  &:before {
-    content: attr(data-author);
-  }
 }
 
 .source-line-bar {
index 7c8dd530efa2834262b350ce9877587349b533ba..322c7bcbbf879c3c78d7c510db1af80c6eec2c10 100644 (file)
   <script>
     var pageLang = '<%= I18n.locale.to_s.gsub(/-/, '_') -%>';
     <%# The two lines below mean that before full removal of Rails, we have to find a way to handle config properties %>
-    window.SS = typeof window.SS === 'object' ? window.SS : {};
-    window.SS.hoursInDay = <%= configuration('sonar.technicalDebt.hoursInDay', 8) %>;
-    window.SS.user = '<%= current_user.login if current_user -%>';
-    window.SS.userName = '<%= current_user.name if current_user -%>';
+    window.SS = {
+      hoursInDay: <%= configuration('sonar.technicalDebt.hoursInDay', 8) %>,
+      user: '<%= current_user.login if current_user -%>',
+      userName: '<%= current_user.name if current_user -%>',
+      userEmail: '<%= current_user.email if current_user -%>',
+      lf: {
+        enableGravatar: <%= configuration('sonar.lf.enableGravatar', true) %>,
+        gravatarServerUrl: '<%= configuration('sonar.lf.gravatarServerUrl') %>'
+      }
+    };
   </script>
   <script src="<%= ApplicationController.root_context -%>/js/sonar.js?v=<%= sonar_version -%>"></script>
   <script>
index 5e4f16cff1987b90f8874641bc64e3ccd2bcb66b..e1891e03b55bcdfa153a1d536d3c876a350a5e04 100644 (file)
@@ -20,6 +20,7 @@ html
     script(src='/js/libs/third-party/numeral-languages.js')
     script(src='/js/libs/third-party/bootstrap/tooltip.js')
     script(src='/js/libs/third-party/bootstrap/dropdown.js')
+    script(src='/js/libs/third-party/md5.js')
     script(src='/js/libs/select2-jquery-ui-fix.js')
     script(src='/js/libs/widgets/base.js')
     script(src='/js/libs/widgets/widget.js')
@@ -53,6 +54,16 @@ html
       jQuery.mockjaxSettings.contentType = 'text/json';
       jQuery.mockjaxSettings.responseTime = 50;
       jQuery(document).ready(function () { $j('.open-modal').modal(); });
+      window.SS = {
+        hoursInDay: 8,
+        user: '',
+        userName: '',
+        userEmail: '',
+        lf: {
+          enableGravatar: false,
+          gravatarServerUrl: ''
+        }
+      };
 
     script.
       requirejs.config({ baseUrl: baseUrl + '/js' });
index a6711488e2ddea8dad9f4713a6f97ad4fc531515..8a19cd775c7ba6dbafbfb79def8bdd24e6911bdb 100644 (file)
@@ -59,6 +59,21 @@ public class CorePropertyDefinitions {
         .category(CoreProperties.CATEGORY_GENERAL)
         .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL)
         .build(),
+      PropertyDefinition.builder("sonar.lf.enableGravatar")
+        .name("Enable support of gravatars")
+        .description("Gravatars are profile pictures of users based on their email.")
+        .type(PropertyType.BOOLEAN)
+        .defaultValue("true")
+        .category(CoreProperties.CATEGORY_GENERAL)
+        .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL)
+        .build(),
+      PropertyDefinition.builder("sonar.lf.gravatarServerUrl")
+        .name("Gravatar URL")
+        .description("Optional URL of custom Gravatar service. Accepted variables are {EMAIL_MD5} for MD5 hash of email and {SIZE} for the picture size in pixels.")
+        .defaultValue("https://secure.gravatar.com/avatar/{EMAIL_MD5}.jpg?s={SIZE}&d=identicon")
+        .category(CoreProperties.CATEGORY_GENERAL)
+        .subCategory(CoreProperties.SUBCATEGORY_LOOKNFEEL)
+        .build(),
 
       // ISSUES
       PropertyDefinition.builder(CoreProperties.DEFAULT_ISSUE_ASSIGNEE)