) will be sorted instead
20 | * of the itself.
21 | */
22 | jQuery.fn.sortElements = (function(){
23 |
24 | var sort = [].sort;
25 |
26 | return function(comparator, getSortable) {
27 |
28 | getSortable = getSortable || function(){return this;};
29 |
30 | var placements = this.map(function(){
31 |
32 | var sortElement = getSortable.call(this),
33 | parentNode = sortElement.parentNode,
34 |
35 | // Since the element itself will change position, we have
36 | // to have some way of storing its original position in
37 | // the DOM. The easiest way is to have a 'flag' node:
38 | nextSibling = parentNode.insertBefore(
39 | document.createTextNode(''),
40 | sortElement.nextSibling
41 | );
42 |
43 | return function() {
44 |
45 | if (parentNode === this) {
46 | throw new Error(
47 | "You can't sort elements if any one is a descendant of another."
48 | );
49 | }
50 |
51 | // Insert before flag:
52 | parentNode.insertBefore(this, nextSibling);
53 | // Remove flag:
54 | parentNode.removeChild(nextSibling);
55 |
56 | };
57 |
58 | });
59 |
60 | return sort.call(this, comparator).each(function(i){
61 | placements[i].call(getSortable.call(this));
62 | });
63 |
64 | };
65 |
66 | })();
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Teamcity Dashboard
2 | ========
3 | :warning: This project has not been updated in 5 years. YMMV.
4 |
5 | Dashboard to display useful information from TeamCity and if available merges it with Sonar results. If somebody broke the build it tries to display their gravatar.
6 |
7 | Installation
8 | ------
9 | * Compile project (.Net 4.0). This will automatically copy the web.example.config => web.config;
10 | * Modify the web.config to point to your teamcity, sonar & github + valid credentials;
11 | * Hook up the site in IIS (.Net 4.0);
12 | * Visit the dashboard on the configured URL.
13 |
14 | Configuration
15 | ------
16 | The site only shows non-archived projects which contain at least one build config where ‘enable status widget’ is checked. If you want to hide specific build steps (i.e. false positives) disable the 'status' widget in the specific build step. When a build is broken the dashboard tries to retrace which user broke the build by traversing back in history until it finds a succesful build. It then will display the user(s) gravatar of the configured email address.
17 |
18 | Sonar integration
19 | ------
20 | The sonar integration is done via configuration in TeamCity. Add the following parameter to your project configuration in TeamCity:
21 | * `sonar.project.key` containing the exact project key as found in Sonar
22 |
23 | Github integration
24 | ------
25 | For displaying events in your organization (pushes?) to github you can authenticate the dashboard to access your github account. You need to generate an Oauth2 token which is quite easy:
26 | `curl -i -u "user:pass" https://api.github.com/authorizations -d '{"scopes":["repo"]}'`
27 | (Documentation: http://developer.github.com/v3/oauth/#create-a-new-authorization). I do not know exactly which `scopes` are required for what but the `repo` value let me access the (private) events of my organization which was what i needed.
28 |
29 | Project logo
30 | ------
31 | If you want to display a logo in the interface you can also configure this in TeamCity. Add the following parameter to your project's configuration in TeamCity:
32 | * `dashboard.project.logo.url` containing the URL to a image of your application.
33 |
34 | Retrieval of the users Gravatar
35 | ------
36 | * The dashboard can only find the email address of a user if the user has mapped its VCS username to its account. If you see thing in Teamcity like 'changes (4) crunchie84@github' it might not be linked. This can be configured in the user his settings in TeamCity.
37 |
38 | About the font: Segoe UI
39 | ========
40 | The font is copyright by Microsoft. Please visit this link http://www.microsoft.com/typography/fonts/family.aspx?FID=331 to determine how you can get a licensed version. Fallback is Serif.
41 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Global.asax.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Reflection;
6 | using System.Web;
7 | using System.Web.Mvc;
8 | using System.Web.Routing;
9 | using ITCloud.Web.Routing;
10 | using log4net;
11 |
12 | namespace TeamCityDashboard
13 | {
14 | public class TeamCityDashboardApplication : System.Web.HttpApplication
15 | {
16 | private static readonly ILog log = LogManager.GetLogger(typeof(TeamCityDashboardApplication));
17 |
18 | public static void RegisterGlobalFilters(GlobalFilterCollection filters)
19 | {
20 | filters.Add(new HandleErrorAttribute());
21 | }
22 |
23 | public static void RegisterRoutes(RouteCollection routes)
24 | {
25 | routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
26 |
27 | // discover UrlRoute attributes on MVC controller classes in the project
28 | routes.DiscoverMvcControllerRoutes();
29 |
30 | //routes.MapRoute(
31 | // "Default", // Route name
32 | // "{controller}/{action}/{id}", // URL with parameters
33 | // new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
34 | //);
35 |
36 | }
37 |
38 | protected void Application_Start()
39 | {
40 | log4net.Config.XmlConfigurator.Configure();
41 |
42 | log.Info("Teamcity dashboard starting...");
43 |
44 | string version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion;
45 | if (string.IsNullOrWhiteSpace(version))
46 | version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
47 | log.InfoFormat("Current version: '{0}'", version);
48 |
49 | AreaRegistration.RegisterAllAreas();
50 |
51 | RegisterGlobalFilters(GlobalFilters.Filters);
52 | RegisterRoutes(RouteTable.Routes);
53 |
54 | log.Info("Application started");
55 | }
56 |
57 | protected void Application_Error(object sender, EventArgs e)
58 | {
59 | if (log.IsErrorEnabled)
60 | {
61 | HttpApplication application = (HttpApplication)sender;
62 | if (application.Context == null)
63 | return;
64 |
65 | var exception = application.Context.Error;
66 |
67 | string logMessage = "An uncaught exception occurred";
68 | try
69 | {
70 | if (Request != null && Request.Url != null)
71 | logMessage += string.Format(" in {0} {1} (referrer={2})", Request.HttpMethod, Request.Url.PathAndQuery, Request.UrlReferrer);
72 | }
73 | catch (HttpException)
74 | {
75 | // an HttpException could occur with message: "Request is not available in this context" but then we ignore it
76 | }
77 | log.Error(logMessage, exception);
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/TeamCityDashboard/Services/AbstractHttpDataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Text;
7 | using System.Web;
8 | using System.Xml;
9 | using TeamCityDashboard.Interfaces;
10 |
11 | namespace TeamCityDashboard.Services
12 | {
13 | public abstract class AbstractHttpDataService
14 | {
15 | protected readonly string BaseUrl;
16 | protected readonly string UserName;
17 | protected readonly string Password;
18 |
19 | protected readonly ICacheService CacheService;
20 |
21 | public AbstractHttpDataService(string baseUrl, string username, string password, ICacheService cacheService)
22 | {
23 | this.BaseUrl = baseUrl;
24 | this.UserName = username;
25 | this.Password = password;
26 | this.CacheService = cacheService;
27 | }
28 |
29 | ///
30 | /// Duration to cache some things which almost never change
31 | ///
32 | protected const int CACHE_DURATION = 3 * 60 * 60;//3 hours
33 |
34 | ///
35 | /// retrieve the content of given url and parse it to xmldocument. throws httpexception if raised
36 | ///
37 | ///
38 | ///
39 | /// original code on http://www.stickler.de/en/information/code-snippets/httpwebrequest-basic-authentication.aspx
40 | protected XmlDocument GetPageContents(string relativeUrl)
41 | {
42 | XmlDocument result = new XmlDocument();
43 | result.LoadXml(GetContents(relativeUrl));
44 | return result;
45 | }
46 |
47 | ///
48 | /// retrieve the content of given url. throws httpexception if raised
49 | ///
50 | ///
51 | ///
52 | protected string GetContents(string relativeUrl)
53 | {
54 | try
55 | {
56 | Uri uri = new Uri(string.Format("{0}{1}", BaseUrl, relativeUrl));
57 | WebRequest myWebRequest = HttpWebRequest.Create(uri);
58 |
59 | HttpWebRequest myHttpWebRequest = (HttpWebRequest)myWebRequest;
60 |
61 | NetworkCredential myNetworkCredential = new NetworkCredential(UserName, Password);
62 | CredentialCache myCredentialCache = new CredentialCache();
63 | myCredentialCache.Add(uri, "Basic", myNetworkCredential);
64 |
65 | myHttpWebRequest.PreAuthenticate = true;
66 | myHttpWebRequest.Credentials = myCredentialCache;
67 |
68 | using (WebResponse myWebResponse = myWebRequest.GetResponse())
69 | {
70 | using (Stream responseStream = myWebResponse.GetResponseStream())
71 | {
72 | StreamReader myStreamReader = new StreamReader(responseStream, Encoding.Default);
73 | return myStreamReader.ReadToEnd();
74 | }
75 | }
76 | }
77 | catch (Exception e)
78 | {
79 | throw new HttpException(string.Format("Error while retrieving url '{0}': {1}", relativeUrl, e.Message), e);
80 | }
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/TeamCityDashboard/parameters.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Web.example.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | false
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/TeamCityDashboard/css/styles.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: Segoe UI;
3 | src: url("../fonts/SegoeWP.ttf"); }
4 |
5 | @font-face {
6 | font-family: Segoe UI Light;
7 | src: url("../fonts/SegoeWP-Light.ttf"); }
8 |
9 | @font-face {
10 | font-family: Segoe UI Semibold;
11 | src: url("../fonts/SegoeWP-Semibold.ttf"); }
12 |
13 | body {
14 | margin: 0;
15 | x-background-image: url(../images/grid-unit.png);
16 | background-color: #000;
17 | color: #fff;
18 | font-family: Segoe UI, Sans-Serif;
19 | -webkit-overflow-scrolling: auto; }
20 |
21 | a {
22 | text-decoration: none;
23 | color: #fff; }
24 |
25 | h1,
26 | .xx-large {
27 | font-family: Segoe UI Light, Sans-Serif;
28 | font-size: 42pt;
29 | line-height: 48pt;
30 | margin: 0; }
31 |
32 | h2,
33 | .x-large {
34 | font-family: Segoe UI Light, Sans-Serif;
35 | font-size: 20pt;
36 | line-height: 24pt;
37 | margin: 0; }
38 |
39 | h3,
40 | .large {
41 | font-family: Segoe UI, Sans-Serif;
42 | font-size: 19px;
43 | line-height: 22px;
44 | margin: 0;
45 | letter-spacing: 1px; }
46 |
47 | p,
48 | .medium {
49 | font-family: Arial, Segoe UI, Sans-Serif;
50 | font-size: 11pt;
51 | line-height: 15pt; }
52 |
53 | h4,
54 | .small {
55 | font-family: Arial, Segoe UI, Sans-Serif;
56 | font-size: 10pt;
57 | font-weight: normal;
58 | line-height: 15pt; }
59 |
60 | #title {
61 | margin: 20px 0 10px 100px; }
62 |
63 | #projectsContainer {
64 | margin-left: 100px;
65 | margin-right: 100px; }
66 |
67 | #pushMessagesContainer {
68 | margin: 20px 0 0 100px; }
69 | #pushMessagesContainer h2 {
70 | margin-bottom: 10px; }
71 | #pushMessagesContainer .item {
72 | margin-right: 10px; }
73 |
74 | .item {
75 | float: left;
76 | margin-bottom: 10px;
77 | width: 250px;
78 | /*default width; we try to make them 120 with JS*/
79 | min-height: 120px;
80 | overflow: hidden; }
81 | .item.event {
82 | background-color: rgba(6, 82, 196, 0.8); }
83 | .item.successful {
84 | background-color: #363;
85 | height: 120px; }
86 | .item.failing {
87 | background: #822; }
88 | .item .item-images {
89 | position: relative;
90 | margin-bottom: 10px; }
91 | .item .item-images.images-uneven-rows {
92 | /* Add extra margin bottom to negate the uneven rows slidly misaligning the blocks*/
93 | overflow: hidden;
94 | margin-bottom: 5px; }
95 | .item .item-images .full-size {
96 | display: block;
97 | width: 250px;
98 | height: 250px; }
99 | .item .item-images .half-size {
100 | display: block;
101 | float: left;
102 | width: 125px;
103 | height: 125px; }
104 | .item .item-text {
105 | position: relative;
106 | clear: left;
107 | display: block;
108 | padding: 10px 10px 5px 10px;
109 | box-sizing: border-box;
110 | height: 120px;
111 | overflow: hidden; }
112 | .item .item-text .details {
113 | float: left;
114 | width: 145px; }
115 | .item .item-text .details .small {
116 | line-height: 14pt;
117 | margin: 5px 0; }
118 | .item .item-text .logo,
119 | .item .item-text .pusher {
120 | margin: 0 15px 5px 0;
121 | max-width: 60px;
122 | max-height: 60px;
123 | float: left; }
124 | .item .item-text .statistics-container .statistic .value {
125 | font-size: 11pt;
126 | float: right; }
127 | .item .extra-text {
128 | position: relative;
129 | display: none;
130 | padding: 0;
131 | box-sizing: border-box; }
132 | .item .extra-text h3 {
133 | margin: 10px 0 0 20px;
134 | font-weight: normal; }
135 | .item .extra-text .chart {
136 | width: 250px;
137 | height: 85px; }
138 |
139 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Services/SonarDataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using System.Linq;
5 | using System.Web;
6 | using TeamCityDashboard.Interfaces;
7 | using TeamCityDashboard.Models;
8 |
9 | namespace TeamCityDashboard.Services
10 | {
11 | public class SonarDataService : AbstractHttpDataService
12 | {
13 | public SonarDataService(string baseUrl, string username, string password, ICacheService cacheService) : base(baseUrl, username, password, cacheService) { }
14 |
15 | //http://sonar.q42.net/api/resources?resource=NegenTwee:PartnerApi&metrics=ncloc,coverage&verbose=true&includetrends=true
16 | private const string PROJECT_TRENDS_URL = @"/api/resources?resource={0}&metrics={1}&verbose=true&includetrends=true&format=xml";
17 | private const string PROJECT_METRICS_CSV = @"coverage,class_complexity,function_complexity,ncloc,comment_lines_density,comment_lines,tests";
18 |
19 | ///
20 | /// create coverage csv report
21 | /// 2013-01-01T00:00:00+0100
22 | ///
23 | private const string PROJECT_TIMEMACHINE_URL = @"/api/timemachine?resource={0}&metrics=coverage&fromDateTime={1}&format=csv";
24 |
25 | public ICodeStatistics GetProjectStatistics(string projectKey)
26 | {
27 | CodeStatistics result = CacheService.Get("sonar-stats-" + projectKey, () => {
28 | var data = GetPageContents(string.Format(CultureInfo.InvariantCulture, PROJECT_TRENDS_URL, projectKey, PROJECT_METRICS_CSV));
29 |
30 | //TODO this code can be greatly improved - error checking etc
31 | var stats = new CodeStatistics
32 | {
33 | AmountOfUnitTests = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='tests']/val").InnerText, CultureInfo.InvariantCulture),
34 | NonCommentingLinesOfCode = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='ncloc']/val").InnerText, CultureInfo.InvariantCulture),
35 | CommentLines = (int)double.Parse(data.SelectSingleNode("resources/resource/msr[key='comment_lines']/val").InnerText, CultureInfo.InvariantCulture),
36 | CommentLinesPercentage = double.Parse(data.SelectSingleNode("resources/resource/msr[key='comment_lines_density']/val").InnerText, CultureInfo.InvariantCulture),
37 | CyclomaticComplexityClass = double.Parse(data.SelectSingleNode("resources/resource/msr[key='class_complexity']/val").InnerText, CultureInfo.InvariantCulture),
38 | CyclomaticComplexityFunction = double.Parse(data.SelectSingleNode("resources/resource/msr[key='function_complexity']/val").InnerText, CultureInfo.InvariantCulture)
39 | };
40 |
41 | if (data.SelectSingleNode("resources/resource/msr[key='coverage']/val") != null)
42 | stats.CodeCoveragePercentage = double.Parse(data.SelectSingleNode("resources/resource/msr[key='coverage']/val").InnerText, CultureInfo.InvariantCulture);
43 |
44 | return stats;
45 | }, 3600);
46 |
47 | return result;
48 | }
49 |
50 | public IEnumerable> GetProjectCoverage(string projectKey)
51 | {
52 | string url = string.Format(CultureInfo.InvariantCulture, PROJECT_TIMEMACHINE_URL, projectKey, DateTime.Now.AddMonths(-2).ToString("s", CultureInfo.InvariantCulture));
53 | string csv = GetContents(url);
54 | var lines = from line
55 | in csv.Split(new char[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries).Skip(1)
56 | let columns = line.Split(',')
57 | select new KeyValuePair(parseDateTime(columns.First()), double.Parse(columns.Skip(1).First(), CultureInfo.InvariantCulture));
58 |
59 | return lines;
60 | }
61 |
62 | private static DateTime parseDateTime(string date)
63 | {
64 | DateTime theDate;
65 | if (DateTime.TryParseExact(date, @"yyyy-MM-dd\THH:mm:sszz\0\0", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out theDate))
66 | {
67 | return theDate;
68 | }
69 | return DateTime.MinValue;
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/TeamCityDashboard/css/styles.scss:
--------------------------------------------------------------------------------
1 | @font-face
2 | {
3 | font-family: Segoe UI;
4 | src: url('../fonts/SegoeWP.ttf');
5 | }
6 | @font-face
7 | {
8 | font-family: Segoe UI Light;
9 | src: url('../fonts/SegoeWP-Light.ttf');
10 | }
11 | @font-face
12 | {
13 | font-family: Segoe UI Semibold;
14 | src: url('../fonts/SegoeWP-Semibold.ttf');
15 | }
16 |
17 | body
18 | {
19 | margin: 0;
20 | x-background-image: url(../images/grid-unit.png);
21 | background-color: #000;
22 | color: #fff;
23 | font-family: Segoe UI, Sans-Serif;
24 | -webkit-overflow-scrolling: auto;
25 | }
26 |
27 | a
28 | {
29 | text-decoration: none;
30 | color: #fff;
31 | }
32 |
33 | h1,
34 | .xx-large
35 | {
36 | font-family: Segoe UI Light, Sans-Serif;
37 | font-size: 42pt;
38 | line-height: 48pt;
39 | margin:0;
40 | }
41 |
42 | h2,
43 | .x-large
44 | {
45 | font-family: Segoe UI Light, Sans-Serif;
46 | font-size: 20pt;
47 | line-height: 24pt;
48 | margin:0;
49 |
50 | }
51 |
52 | h3,
53 | .large
54 | {
55 | font-family: Segoe UI, Sans-Serif;
56 | font-size: 19px;
57 | line-height: 22px;
58 | margin:0;
59 | letter-spacing:1px;
60 | }
61 |
62 | p,
63 | .medium
64 | {
65 | font-family: Arial, Segoe UI, Sans-Serif;
66 | font-size: 11pt;
67 | line-height: 15pt;
68 | }
69 |
70 | h4,
71 | .small
72 | {
73 | font-family: Arial, Segoe UI, Sans-Serif;
74 | font-size: 10pt;
75 | font-weight: normal;
76 | line-height: 15pt;
77 | }
78 |
79 | #title
80 | {
81 | margin: 20px 0 10px 100px;
82 | }
83 |
84 | #projectsContainer
85 | {
86 | margin-left: 100px;
87 | margin-right: 100px;
88 | }
89 | #pushMessagesContainer
90 | {
91 | margin: 20px 0 0 100px;
92 |
93 | h2
94 | {
95 | margin-bottom: 10px;
96 | }
97 | .item
98 | {
99 | margin-right: 10px;
100 | }
101 | }
102 |
103 |
104 | .item
105 | {
106 | float: left;
107 | margin-bottom: 10px;
108 | width: 250px; /*default width; we try to make them 120 with JS*/
109 | min-height: 120px;
110 | overflow: hidden;
111 |
112 | &.event
113 | {
114 | background-color: rgba(6, 82, 196, 0.8);
115 | }
116 |
117 | &.successful
118 | {
119 | background-color: #363;
120 | height: 120px;
121 | }
122 |
123 | &.failing
124 | {
125 | background: #822;
126 | }
127 |
128 | .item-images
129 | {
130 | position: relative;
131 | margin-bottom: 10px;
132 |
133 | &.images-uneven-rows
134 | {
135 | /* Add extra margin bottom to negate the uneven rows slidly misaligning the blocks*/
136 | overflow: hidden;
137 | margin-bottom: 5px;
138 | }
139 |
140 | .full-size
141 | {
142 | display: block;
143 | width: 250px;
144 | height: 250px;
145 | }
146 |
147 | .half-size
148 | {
149 | display: block;
150 | float: left;
151 | width: 125px;
152 | height: 125px;
153 | }
154 | }
155 |
156 | .item-text
157 | {
158 | position: relative;
159 | clear: left;
160 | display: block;
161 | padding: 10px 10px 5px 10px;
162 | box-sizing: border-box;
163 | height: 120px;
164 | overflow:hidden;
165 |
166 | .details
167 | {
168 | float:left;
169 | width:145px;
170 |
171 | .small
172 | {
173 | line-height: 14pt;
174 | margin:5px 0;
175 | }
176 | }
177 |
178 | .logo,
179 | .pusher
180 | {
181 | margin:0 15px 5px 0;
182 | max-width: 60px;
183 | max-height: 60px;
184 | float:left;
185 | }
186 |
187 | .statistics-container
188 | {
189 | .statistic .value
190 | {
191 | font-size: 11pt;
192 | float: right;
193 | }
194 | }
195 | }
196 |
197 | .extra-text
198 | {
199 | position: relative;
200 | display: none;
201 | padding: 0;
202 | box-sizing: border-box;
203 |
204 | h3
205 | {
206 | margin: 10px 0 0 20px;
207 | font-weight: normal;
208 | }
209 |
210 | .chart
211 | {
212 | width: 250px;
213 | height: 85px;
214 | }
215 | }
216 | }
--------------------------------------------------------------------------------
/.nuget/NuGet.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildProjectDirectory)\..\
5 |
6 |
7 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
8 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config"))
9 | $([System.IO.Path]::Combine($(SolutionDir), "packages"))
10 |
11 |
12 | $(SolutionDir).nuget
13 | packages.config
14 | $(SolutionDir)packages
15 |
16 |
17 | $(NuGetToolsPath)\nuget.exe
18 | "$(NuGetExePath)"
19 | mono --runtime=v4.0.30319 $(NuGetExePath)
20 |
21 | $(TargetDir.Trim('\\'))
22 |
23 |
24 | ""
25 |
26 |
27 | false
28 |
29 |
30 | false
31 |
32 |
33 | $(NuGetCommand) install "$(PackagesConfig)" -source $(PackageSources) -o "$(PackagesDir)"
34 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols
35 |
36 |
37 |
38 |
39 | $(PrepareForBuildDependsOn);
40 | RestorePackages;
41 |
42 |
46 |
47 |
48 |
49 | $(PrepareForBuildDependsOn);
50 | BuildPackage;
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
62 |
63 |
66 |
67 |
68 |
69 |
71 |
72 |
75 |
76 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Scripts/jquery.timeago.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Timeago is a jQuery plugin that makes it easy to support automatically
3 | * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
4 | *
5 | * @name timeago
6 | * @version 0.11.4
7 | * @requires jQuery v1.2.3+
8 | * @author Ryan McGeary
9 | * @license MIT License - http://www.opensource.org/licenses/mit-license.php
10 | *
11 | * For usage and examples, visit:
12 | * http://timeago.yarp.com/
13 | *
14 | * Copyright (c) 2008-2012, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
15 | */
16 | (function($) {
17 | $.timeago = function(timestamp) {
18 | if (timestamp instanceof Date) {
19 | return inWords(timestamp);
20 | } else if (typeof timestamp === "string") {
21 | return inWords($.timeago.parse(timestamp));
22 | } else if (typeof timestamp === "number") {
23 | return inWords(new Date(timestamp));
24 | } else {
25 | return inWords($.timeago.datetime(timestamp));
26 | }
27 | };
28 | var $t = $.timeago;
29 |
30 | $.extend($.timeago, {
31 | settings: {
32 | refreshMillis: 60000,
33 | allowFuture: false,
34 | strings: {
35 | prefixAgo: null,
36 | prefixFromNow: null,
37 | suffixAgo: "ago",
38 | suffixFromNow: "from now",
39 | seconds: "less than a minute",
40 | minute: "about a minute",
41 | minutes: "%d minutes",
42 | hour: "about an hour",
43 | hours: "about %d hours",
44 | day: "a day",
45 | days: "%d days",
46 | month: "about a month",
47 | months: "%d months",
48 | year: "about a year",
49 | years: "%d years",
50 | wordSeparator: " ",
51 | numbers: []
52 | }
53 | },
54 | inWords: function(distanceMillis) {
55 | var $l = this.settings.strings;
56 | var prefix = $l.prefixAgo;
57 | var suffix = $l.suffixAgo;
58 | if (this.settings.allowFuture) {
59 | if (distanceMillis < 0) {
60 | prefix = $l.prefixFromNow;
61 | suffix = $l.suffixFromNow;
62 | }
63 | }
64 |
65 | var seconds = Math.abs(distanceMillis) / 1000;
66 | var minutes = seconds / 60;
67 | var hours = minutes / 60;
68 | var days = hours / 24;
69 | var years = days / 365;
70 |
71 | function substitute(stringOrFunction, number) {
72 | var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
73 | var value = ($l.numbers && $l.numbers[number]) || number;
74 | return string.replace(/%d/i, value);
75 | }
76 |
77 | var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
78 | seconds < 90 && substitute($l.minute, 1) ||
79 | minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
80 | minutes < 90 && substitute($l.hour, 1) ||
81 | hours < 24 && substitute($l.hours, Math.round(hours)) ||
82 | hours < 42 && substitute($l.day, 1) ||
83 | days < 30 && substitute($l.days, Math.round(days)) ||
84 | days < 45 && substitute($l.month, 1) ||
85 | days < 365 && substitute($l.months, Math.round(days / 30)) ||
86 | years < 1.5 && substitute($l.year, 1) ||
87 | substitute($l.years, Math.round(years));
88 |
89 | var separator = $l.wordSeparator === undefined ? " " : $l.wordSeparator;
90 | return $.trim([prefix, words, suffix].join(separator));
91 | },
92 | parse: function(iso8601) {
93 | var s = $.trim(iso8601);
94 | s = s.replace(/\.\d+/,""); // remove milliseconds
95 | s = s.replace(/-/,"/").replace(/-/,"/");
96 | s = s.replace(/T/," ").replace(/Z/," UTC");
97 | s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
98 | return new Date(s);
99 | },
100 | datetime: function(elem) {
101 | var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
102 | return $t.parse(iso8601);
103 | },
104 | isTime: function(elem) {
105 | // jQuery's `is()` doesn't play well with HTML5 in IE
106 | return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
107 | }
108 | });
109 |
110 | $.fn.timeago = function() {
111 | var self = this;
112 | self.each(refresh);
113 |
114 | var $s = $t.settings;
115 | if ($s.refreshMillis > 0) {
116 | setInterval(function() { self.each(refresh); }, $s.refreshMillis);
117 | }
118 | return self;
119 | };
120 |
121 | function refresh() {
122 | var data = prepareData(this);
123 | if (!isNaN(data.datetime)) {
124 | $(this).text(inWords(data.datetime));
125 | }
126 | return this;
127 | }
128 |
129 | function prepareData(element) {
130 | element = $(element);
131 | if (!element.data("timeago")) {
132 | element.data("timeago", { datetime: $t.datetime(element) });
133 | var text = $.trim(element.text());
134 | if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
135 | element.attr("title", text);
136 | }
137 | }
138 | return element.data("timeago");
139 | }
140 |
141 | function inWords(date) {
142 | return $t.inWords(distance(date));
143 | }
144 |
145 | function distance(date) {
146 | return (new Date().getTime() - date.getTime());
147 | }
148 |
149 | // fix for IE6 suckage
150 | document.createElement("abbr");
151 | document.createElement("time");
152 | }(jQuery));
153 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Scripts/metro-grid.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Windows 8 Metro Grid implementation by Tom Lokhorst
3 | *
4 | */
5 | function MetroGrid() {
6 | }
7 |
8 | MetroGrid.prototype = {
9 | init: function ($grid) {
10 | this.$grid = $grid;
11 |
12 | // dummy group for space
13 | //this.layout();
14 |
15 | //$(window).bind('resize', this.layout.bind(this));
16 | },
17 |
18 | animate: function () {
19 | var $items = this.$grid.find('.item:has(.item-text)');
20 | var $item = $($items[Math.floor(Math.random() * $items.length)]);
21 |
22 | if ($item != null) {
23 | this.animateStep($item).then(function (step) {
24 | if (step)
25 | setTimeout(this.animateStep.bind(this, $item, step), (Math.random() * 2 + 3) * 1000);
26 | }.bind(this));
27 | }
28 |
29 | setTimeout(this.animate.bind(this), (Math.random() * 3 + 2) * 1000);//2 tot 5 seconds between animations
30 | },
31 |
32 | animateStep: function ($item, gotoStep) {
33 | return $.Deferred(function (dfd) {
34 | var step = gotoStep || $item.data('step') || 'uninitialized';
35 |
36 | //if somehow we are triggert twice at the same time we bail out the second time
37 | if (step == 'animating') return;
38 | $item.data('step', 'animating');
39 |
40 |
41 | if (step == 'uninitialized') {
42 | //hard position the extra text outside of the box and make it visible
43 | $itemText = $item.find('.item-text');
44 | var mtop = $itemText.position()
45 | ? $item.height() - $itemText.position().top - $itemText.height() - 10
46 | : 0;
47 | mtop = Math.floor(mtop / 120) * 120;
48 | $item.find('.extra-text')
49 | .css({ marginTop: mtop })
50 | .show();
51 |
52 | step = 'start';
53 | }
54 |
55 | if (step == 'start') {
56 | //if we have multiple images they will round-robin fade in fade out and change position
57 | var $images = $item.find('.item-images img');
58 | if (!$item.hasClass('failing') || $images.length < 2) {
59 | step = 'up';
60 | }
61 | else {
62 | var fst = Math.floor(Math.random() * $images.length);
63 | var snd = (fst + 1) % $images.length;
64 |
65 | var $fst = $($images[fst]);
66 | var $snd = $($images[snd]);
67 |
68 | $fst.animate({ opacity: 0, }, 'slow', function () {
69 | $snd.animate({ opacity: 0, }, 'slow', function () {
70 | $fst.swap($snd);
71 |
72 | $snd.animate({ opacity: 1, }, 'slow', function () {
73 | $fst.animate({ opacity: 1, }, 'slow', function () {
74 | setTimeout(function () {
75 | $item.data('step', 'up');
76 | dfd.resolve();
77 | }, 1000);
78 | });
79 | });
80 | });
81 | });
82 | }
83 | }
84 |
85 | if (step == '?') {
86 | if (!$item.hasClass('failing'))
87 | step = 'up';
88 | }
89 |
90 | //now animate the extra-text portion to top
91 | if (step == 'up') {
92 | if (!$item.find('.extra-text').length) {
93 | //we dont animate up or down, ready with animation, ready for next cycle
94 | $item.data('step', 'start');
95 | dfd.resolve();
96 | return;
97 | }
98 |
99 | //130 = images should not display margin
100 | $item.children()
101 | .animate({ top: -(($item.hasClass('failing') && $item.find('.item-images img').length) ? 130 : 120) }, 'slow', function () {
102 | setTimeout(function () {
103 | dfd.resolve('down');
104 | }.bind(this), 1000);
105 | }.bind(this));
106 | }
107 |
108 | //now animate back to the bottom part
109 | if (step == 'down') {
110 | $item.children()
111 | .animate({ top: 0 }, 'slow', function () {
112 | setTimeout(function () {
113 | //ready for next animation cycle
114 | $item.data('step', 'start');
115 | dfd.resolve();
116 | }.bind(this), 1000);
117 | }.bind(this));
118 | }
119 | }.bind(this)).promise();
120 | },
121 | };
122 |
123 | if (!Function.prototype.bind) {
124 | Function.prototype.bind = function (oThis) {
125 | if (typeof this !== "function") {
126 | // closest thing possible to the ECMAScript 5 internal IsCallable function
127 | throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
128 | }
129 |
130 | var aArgs = Array.prototype.slice.call(arguments, 1),
131 | fToBind = this,
132 | fNOP = function () { },
133 | fBound = function () {
134 | return fToBind.apply(this instanceof fNOP
135 | ? this
136 | : oThis || window,
137 | aArgs.concat(Array.prototype.slice.call(arguments)));
138 | };
139 |
140 | fNOP.prototype = this.prototype;
141 | fBound.prototype = new fNOP();
142 |
143 | return fBound;
144 | };
145 | }
146 |
147 | jQuery.fn.swap = function (b) {
148 | b = jQuery(b)[0];
149 | var a = this[0];
150 |
151 | var t = a.parentNode.insertBefore(document.createTextNode(""), a);
152 | b.parentNode.insertBefore(a, b);
153 | t.parentNode.insertBefore(b, t);
154 | t.parentNode.removeChild(t);
155 |
156 | return this;
157 | };
158 |
159 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Controllers/DashboardController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 | using System.Web.Mvc;
6 | using ITCloud.Web.Routing;
7 | using TeamCityDashboard.Interfaces;
8 | using TeamCityDashboard.Services;
9 | using System.Configuration;
10 | using System.Diagnostics;
11 | using System.Reflection;
12 |
13 | namespace TeamCityDashboard.Controllers
14 | {
15 | public class DashboardController : Controller
16 | {
17 | private static TeamCityDataService TeamCityDataService = null;
18 | private static SonarDataService SonarDataService = null;
19 | private static GithubDataService GithubDataService = null;
20 |
21 | private static string _version;
22 | public string ProjectVersion
23 | {
24 | get
25 | {
26 | if (string.IsNullOrWhiteSpace(_version))
27 | {
28 | _version = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion;
29 | if (string.IsNullOrWhiteSpace(_version))
30 | _version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
31 | }
32 | return _version;
33 | }
34 | }
35 |
36 | public DashboardController()
37 | {
38 | ICacheService cacheService = new WebCacheService();
39 |
40 | //singletonish ftw
41 | if (TeamCityDataService == null)
42 | {
43 | TeamCityDataService = new TeamCityDataService(
44 | ConfigurationManager.AppSettings["teamcity.baseUrl"],
45 | ConfigurationManager.AppSettings["teamcity.username"],
46 | ConfigurationManager.AppSettings["teamcity.password"],
47 | cacheService
48 | );
49 | }
50 |
51 | //singletonish ftw
52 | if (SonarDataService == null)
53 | {
54 | SonarDataService = new SonarDataService(
55 | ConfigurationManager.AppSettings["sonar.baseUrl"],
56 | ConfigurationManager.AppSettings["sonar.username"],
57 | ConfigurationManager.AppSettings["sonar.password"],
58 | cacheService
59 | );
60 | }
61 |
62 | if (GithubDataService == null)
63 | {
64 | GithubDataService = new TeamCityDashboard.Services.GithubDataService(
65 | (string)ConfigurationManager.AppSettings["github.oauth2token"],
66 | (string)ConfigurationManager.AppSettings["github.api.events.url"]
67 | , cacheService
68 | );
69 | }
70 | }
71 |
72 | [UrlRoute(Name = "VersionString", Path = "version")]
73 | [HttpGet()]
74 | public ActionResult Version()
75 | {
76 | return new JsonResult()
77 | {
78 | JsonRequestBehavior = JsonRequestBehavior.AllowGet,
79 | ContentEncoding = System.Text.Encoding.UTF8,
80 | Data = ProjectVersion
81 | };
82 | }
83 |
84 | [UrlRoute(Name = "Data", Path = "data")]
85 | [HttpGet()]
86 | public ActionResult Data()
87 | {
88 | var projectsWithSonarDataAdded = from proj in TeamCityDataService.GetActiveProjects()
89 | select new TeamCityDashboard.Models.Project
90 | {
91 | Id = proj.Id,
92 | Name = proj.Name,
93 | BuildConfigs = proj.BuildConfigs,
94 | SonarProjectKey = proj.SonarProjectKey,
95 | Url = proj.Url,
96 | LastBuildDate = proj.LastBuildDate,
97 | IconUrl = proj.IconUrl,
98 | Statistics = string.IsNullOrWhiteSpace(proj.SonarProjectKey) ? (ICodeStatistics)null : SonarDataService.GetProjectStatistics(proj.SonarProjectKey),
99 | CoverageGraph = string.IsNullOrWhiteSpace(proj.SonarProjectKey) ? null : getProjectGraph(proj.SonarProjectKey)
100 | };
101 |
102 | return new JsonResult()
103 | {
104 | JsonRequestBehavior = JsonRequestBehavior.AllowGet,
105 | ContentEncoding = System.Text.Encoding.UTF8,
106 | Data = projectsWithSonarDataAdded
107 | };
108 | }
109 |
110 | private object[][] getProjectGraph(string sonarKey)
111 | {
112 | var data = SonarDataService.GetProjectCoverage(sonarKey);
113 | return (from kvp in data select new object[] { kvp.Key, kvp.Value }).ToArray();
114 | }
115 |
116 | [UrlRoute(Name = "projectGraphData", Path = "projectgraph")]
117 | [HttpGet()]
118 | public ActionResult ProjectGraph(string sonarKey)
119 | {
120 | var data = SonarDataService.GetProjectCoverage(sonarKey);
121 |
122 | return new JsonResult()
123 | {
124 | JsonRequestBehavior = JsonRequestBehavior.AllowGet,
125 | ContentEncoding = System.Text.Encoding.UTF8,
126 | Data = (from kvp in data select new object[] { kvp.Key, kvp.Value }).ToArray()
127 | };
128 | }
129 |
130 | [UrlRoute(Name = "Github push events", Path = "pushevents")]
131 | [HttpGet()]
132 | public ActionResult PushEvents()
133 | {
134 | return new JsonResult()
135 | {
136 | JsonRequestBehavior = JsonRequestBehavior.AllowGet,
137 | ContentEncoding = System.Text.Encoding.UTF8,
138 | Data = GithubDataService.GetRecentEvents()
139 | };
140 | }
141 |
142 | [UrlRoute(Name = "Home", Path = "")]
143 | [HttpGet()]
144 | public ActionResult Index()
145 | {
146 | return View();
147 | }
148 | }
149 | }
--------------------------------------------------------------------------------
/TeamCityDashboard/Services/GithubDataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Net;
6 | using System.Text;
7 | using System.Web;
8 | using log4net;
9 | using Newtonsoft.Json.Linq;
10 | using TeamCityDashboard.Interfaces;
11 | using TeamCityDashboard.Models;
12 |
13 | namespace TeamCityDashboard.Services
14 | {
15 | public class GithubDataService
16 | {
17 | private static readonly ILog log = LogManager.GetLogger(typeof(GithubDataService));
18 |
19 | private readonly ICacheService cacheService;
20 | private readonly string oauth2token;
21 | private readonly string eventsurl;
22 |
23 | private const string API_BASE_URL = @"https://api.github.com";
24 |
25 | public GithubDataService(string oauth2token, string eventsurl, ICacheService cacheService)
26 | {
27 | if (cacheService == null)
28 | throw new ArgumentNullException("cacheService");
29 | if (string.IsNullOrWhiteSpace(oauth2token))
30 | throw new ArgumentNullException("oauth2token");
31 | if (string.IsNullOrWhiteSpace(eventsurl))
32 | throw new ArgumentNullException("eventsurl");
33 |
34 | this.cacheService = cacheService;
35 | this.oauth2token = oauth2token;
36 | this.eventsurl = eventsurl;
37 | }
38 |
39 | ///
40 | /// always returns (max) 5 recent events old -> first ordered
41 | ///
42 | ///
43 | public IEnumerable GetRecentEvents()
44 | {
45 | try
46 | {
47 | string response = getEventsApiContents();
48 | if (!string.IsNullOrWhiteSpace(response))
49 | {
50 | //parse result, re-cache it
51 | var latestPushEvents = parseGithubPushEventsJson(response);
52 | cacheService.Set("latest-pushevents", latestPushEvents, WebCacheService.CACHE_PERMANENTLY);
53 | }
54 | return getRecentPushEventsFromCache();
55 | }
56 | catch (Exception ex)
57 | {
58 | log.Error(ex);
59 | return Enumerable.Empty();
60 | }
61 | }
62 |
63 | private static List parseGithubPushEventsJson(string json)
64 | {
65 | JArray events = JArray.Parse(json);
66 | var parsedEvents = (from evt in events
67 | where (string)evt["type"] == "PushEvent"
68 | select parseGithubPushEvent(evt));
69 |
70 | log.DebugFormat("Retrieved {0} push events from github", parsedEvents.Count());
71 |
72 | var latestFivePushEvents = parsedEvents.OrderByDescending(pe => pe.Created).Take(5).OrderBy(pe => pe.Created).ToList();
73 |
74 | return latestFivePushEvents;
75 | }
76 |
77 | private static PushEvent parseGithubPushEvent(JToken evt)
78 | {
79 | string repositoryName = ((string) evt["repo"]["name"]);
80 | if(repositoryName.Contains('/'))
81 | repositoryName = repositoryName.Substring(1 + repositoryName.IndexOf('/'));
82 |
83 | return new PushEvent
84 | {
85 | RepositoryName = repositoryName,
86 | BranchName = ((string)evt["payload"]["ref"]).Replace("refs/heads/", ""),
87 | EventId = evt["id"].ToString(),
88 | ActorUsername = (string)evt["actor"]["login"],
89 | ActorGravatarId = (string)evt["actor"]["gravatar_id"],
90 | AmountOfCommits = (int)evt["payload"]["size"],
91 | Created = (DateTime)evt["created_at"]
92 | };
93 | }
94 |
95 | private IEnumerable getRecentPushEventsFromCache()
96 | {
97 | var latestPushEvents = cacheService.Get>("latest-pushevents");
98 | if (latestPushEvents == null)
99 | {
100 | log.Error("We could not find pushEvents in the cache AND github did not return any. Possible error?");
101 | return Enumerable.Empty();
102 | }
103 |
104 | return latestPushEvents;
105 | }
106 |
107 | private string lastReceivedEventsETAG;
108 | private string getEventsApiContents(bool ignoreCache=false)
109 | {
110 | try
111 | {
112 | Uri uri = new Uri(string.Format("{0}{1}", API_BASE_URL, eventsurl));
113 | HttpWebRequest myHttpWebRequest = (HttpWebRequest)HttpWebRequest.Create(uri);
114 | myHttpWebRequest.UserAgent = "TeamCity CI Dashboard - https://github.com/crunchie84/teamcity-dashboard";
115 | myHttpWebRequest.Headers.Add("Authorization", "bearer " + oauth2token);
116 |
117 | if (!string.IsNullOrWhiteSpace(lastReceivedEventsETAG) && !ignoreCache)
118 | myHttpWebRequest.Headers.Add("If-None-Match", lastReceivedEventsETAG);
119 |
120 | using (HttpWebResponse myWebResponse = (HttpWebResponse)myHttpWebRequest.GetResponse())
121 | {
122 | if (myWebResponse.StatusCode == HttpStatusCode.OK)
123 | {
124 | //if we do not save the returned ETag we will always get the full list with latest changes instead of the real delta since we started polling.
125 | lastReceivedEventsETAG = myWebResponse.Headers.Get("ETag");
126 |
127 | using (Stream responseStream = myWebResponse.GetResponseStream())
128 | {
129 | StreamReader myStreamReader = new StreamReader(responseStream, Encoding.Default);
130 | return myStreamReader.ReadToEnd();
131 | }
132 | }
133 | }
134 | }
135 | catch (HttpException ex)
136 | {
137 | if (ex.GetHttpCode() != (int)HttpStatusCode.NotModified)
138 | throw;
139 | }
140 | catch (WebException ex)
141 | {
142 | var response = ex.Response as HttpWebResponse;
143 | if(response == null || response.StatusCode != HttpStatusCode.NotModified)
144 | throw;
145 | }
146 | catch (Exception ex)
147 | {
148 | throw new HttpException(string.Format("Error while retrieving url '{0}': {1}", eventsurl, ex.Message), ex);
149 | }
150 | return string.Empty;
151 | }
152 | }
153 | }
--------------------------------------------------------------------------------
/TeamCityDashboard/Scripts/jquery.masonry.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Masonry v2.1.07
3 | * A dynamic layout plugin for jQuery
4 | * The flip-side of CSS Floats
5 | * http://masonry.desandro.com
6 | *
7 | * Licensed under the MIT license.
8 | * Copyright 2012 David DeSandro
9 | */ (function (a, b, c) {
10 | "use strict";
11 | var d = b.event,
12 | e = b.event.handle ? "handle" : "dispatch",
13 | f;
14 | d.special.smartresize = {
15 | setup: function () {
16 | b(this).bind("resize", d.special.smartresize.handler)
17 | },
18 | teardown: function () {
19 | b(this).unbind("resize", d.special.smartresize.handler)
20 | },
21 | handler: function (a, b) {
22 | var c = this,
23 | g = arguments;
24 | a.type = "smartresize", f && clearTimeout(f), f = setTimeout(function () {
25 | d[e].apply(c, g)
26 | }, b === "execAsap" ? 0 : 100)
27 | }
28 | }, b.fn.smartresize = function (a) {
29 | return a ? this.bind("smartresize", a) : this.trigger("smartresize", ["execAsap"])
30 | }, b.Mason = function (a, c) {
31 | this.element = b(c), this._create(a), this._init()
32 | }, b.Mason.settings = {
33 | isResizable: !0,
34 | isAnimated: !1,
35 | animationOptions: {
36 | queue: !1,
37 | duration: 500
38 | },
39 | gutterWidth: 0,
40 | isRTL: !1,
41 | isFitWidth: !1,
42 | containerStyle: {
43 | position: "relative"
44 | }
45 | }, b.Mason.prototype = {
46 | _filterFindBricks: function (a) {
47 | var b = this.options.itemSelector;
48 | return b ? a.filter(b).add(a.find(b)) : a
49 | },
50 | _getBricks: function (a) {
51 | var b = this._filterFindBricks(a).css({
52 | position: "absolute"
53 | }).addClass("masonry-brick");
54 | return b
55 | },
56 | _create: function (c) {
57 | this.options = b.extend(!0, {}, b.Mason.settings, c), this.styleQueue = [];
58 | var d = this.element[0].style;
59 | this.originalStyle = {
60 | height: d.height || ""
61 | };
62 | var e = this.options.containerStyle;
63 | for (var f in e) this.originalStyle[f] = d[f] || "";
64 | this.element.css(e), this.horizontalDirection = this.options.isRTL ? "right" : "left";
65 | var g = this.element.css("padding-" + this.horizontalDirection),
66 | h = this.element.css("padding-top");
67 | this.offset = {
68 | x: g ? parseInt(g, 10) : 0,
69 | y: h ? parseInt(h, 10) : 0
70 | }, this.isFluid = this.options.columnWidth && typeof this.options.columnWidth == "function";
71 | var i = this;
72 | setTimeout(function () {
73 | i.element.addClass("masonry")
74 | }, 0), this.options.isResizable && b(a).bind("smartresize.masonry", function () {
75 | i.resize()
76 | }), this.reloadItems()
77 | },
78 | _init: function (a) {
79 | this._getColumns(), this._reLayout(a)
80 | },
81 | option: function (a, c) {
82 | b.isPlainObject(a) && (this.options = b.extend(!0, this.options, a))
83 | },
84 | layout: function (a, b) {
85 | for (var c = 0, d = a.length; c < d; c++) this._placeBrick(a[c]);
86 | var e = {};
87 | e.height = Math.max.apply(Math, this.colYs);
88 | if (this.options.isFitWidth) {
89 | var f = 0;
90 | c = this.cols;
91 | while (--c) {
92 | if (this.colYs[c] !== 0) break;
93 | f++
94 | }
95 | e.width = (this.cols - f) * this.columnWidth - this.options.gutterWidth
96 | }
97 | this.styleQueue.push({
98 | $el: this.element,
99 | style: e
100 | });
101 | var g = this.isLaidOut ? this.options.isAnimated ? "animate" : "css" : "css",
102 | h = this.options.animationOptions,
103 | i;
104 | for (c = 0, d = this.styleQueue.length; c < d; c++) i = this.styleQueue[c], i.$el[g](i.style, h);
105 | this.styleQueue = [], b && b.call(a), this.isLaidOut = !0
106 | },
107 | _getColumns: function () {
108 | var a = this.options.isFitWidth ? this.element.parent() : this.element,
109 | b = a.width();
110 | this.columnWidth = this.isFluid ? this.options.columnWidth(b) : this.options.columnWidth || this.$bricks.outerWidth(!0) || b, this.columnWidth += this.options.gutterWidth, this.cols = Math.floor((b + this.options.gutterWidth) / this.columnWidth), this.cols = Math.max(this.cols, 1)
111 | },
112 | _placeBrick: function (a) {
113 | var c = b(a),
114 | d, e, f, g, h;
115 | d = Math.ceil(c.outerWidth(!0) / this.columnWidth), d = Math.min(d, this.cols);
116 | if (d === 1) f = this.colYs;
117 | else {
118 | e = this.cols + 1 - d, f = [];
119 | for (h = 0; h < e; h++) g = this.colYs.slice(h, h + d), f[h] = Math.max.apply(Math, g)
120 | }
121 | var i = Math.min.apply(Math, f),
122 | j = 0;
123 | for (var k = 0, l = f.length; k < l; k++) if (f[k] === i) {
124 | j = k;
125 | break
126 | }
127 | var m = {
128 | top: i + this.offset.y
129 | };
130 | m[this.horizontalDirection] = this.columnWidth * j + this.offset.x, this.styleQueue.push({
131 | $el: c,
132 | style: m
133 | });
134 | var n = i + c.outerHeight(!0),
135 | o = this.cols + 1 - l;
136 | for (k = 0; k < o; k++) this.colYs[j + k] = n
137 | },
138 | resize: function () {
139 | var a = this.cols;
140 | this._getColumns(), (this.isFluid || this.cols !== a) && this._reLayout()
141 | },
142 | _reLayout: function (a) {
143 | var b = this.cols;
144 | this.colYs = [];
145 | while (b--) this.colYs.push(0);
146 | this.layout(this.$bricks, a)
147 | },
148 | reloadItems: function () {
149 | this.$bricks = this._getBricks(this.element.children())
150 | },
151 | reload: function (a) {
152 | this.reloadItems(), this._init(a)
153 | },
154 | appended: function (a, b, c) {
155 | if (b) {
156 | this._filterFindBricks(a).css({
157 | top: this.element.height()
158 | });
159 | var d = this;
160 | setTimeout(function () {
161 | d._appended(a, c)
162 | }, 1)
163 | } else this._appended(a, c)
164 | },
165 | _appended: function (a, b) {
166 | var c = this._getBricks(a);
167 | this.$bricks = this.$bricks.add(c), this.layout(c, b)
168 | },
169 | remove: function (a) {
170 | this.$bricks = this.$bricks.not(a), a.remove()
171 | },
172 | destroy: function () {
173 | this.$bricks.removeClass("masonry-brick").each(function () {
174 | this.style.position = "", this.style.top = "", this.style.left = ""
175 | });
176 | var c = this.element[0].style;
177 | for (var d in this.originalStyle) c[d] = this.originalStyle[d];
178 | this.element.unbind(".masonry").removeClass("masonry").removeData("masonry"), b(a).unbind(".masonry")
179 | }
180 | }, b.fn.imagesLoaded = function (a) {
181 | function h() {
182 | a.call(c, d)
183 | }
184 | function i(a) {
185 | var c = a.target;
186 | c.src !== f && b.inArray(c, g) === -1 && (g.push(c), --e <= 0 && (setTimeout(h), d.unbind(".imagesLoaded", i)))
187 | }
188 | var c = this,
189 | d = c.find("img").add(c.filter("img")),
190 | e = d.length,
191 | f = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==",
192 | g = [];
193 | return e || h(), d.bind("load.imagesLoaded error.imagesLoaded", i).each(function () {
194 | var a = this.src;
195 | this.src = f, this.src = a
196 | }), c
197 | };
198 | var g = function (b) {
199 | a.console && a.console.error(b)
200 | };
201 | b.fn.masonry = function (a) {
202 | if (typeof a == "string") {
203 | var c = Array.prototype.slice.call(arguments, 1);
204 | this.each(function () {
205 | var d = b.data(this, "masonry");
206 | if (!d) {
207 | g("cannot call methods on masonry prior to initialization; attempted to call method '" + a + "'");
208 | return
209 | }
210 | if (!b.isFunction(d[a]) || a.charAt(0) === "_") {
211 | g("no such method '" + a + "' for masonry instance");
212 | return
213 | }
214 | d[a].apply(d, c)
215 | })
216 | } else this.each(function () {
217 | var c = b.data(this, "masonry");
218 | c ? (c.option(a || {}), c._init()) : b.data(this, "masonry", new b.Mason(a, this))
219 | });
220 | return this
221 | }
222 | })(window, jQuery);
--------------------------------------------------------------------------------
/TeamCityDashboard/TeamCityDashboard.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 |
8 | 0.0.0.0
9 | obj\$(Configuration)\Package\TeamCityDashboard-$(build_number).zip
10 | False
11 |
12 |
13 | 2.0
14 | {B43EC6D7-11D6-4E8D-B943-3918CAF79E63}
15 | {E53F8FEA-EAE0-44A6-8774-FFD645390401};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}
16 | Library
17 | Properties
18 | TeamCityDashboard
19 | TeamCityDashboard
20 | v4.5
21 | false
22 | false
23 | ..\
24 | true
25 |
26 |
27 |
28 |
29 | 4.0
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | true
38 | full
39 | false
40 | bin\
41 | DEBUG;TRACE
42 | prompt
43 | 4
44 | false
45 |
46 |
47 | pdbonly
48 | true
49 | true
50 | bin\
51 | TRACE
52 | prompt
53 | 4
54 | false
55 |
56 |
57 |
58 | ..\libs\ITCloud.Web.Routing.dll
59 |
60 |
61 | ..\packages\log4net.2.0.0\lib\net40-full\log4net.dll
62 |
63 |
64 | ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | Global.asax
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | styles.scss
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Web.config
127 |
128 |
129 | Web.config
130 |
131 |
132 |
133 |
134 | True
135 | False
136 | Nested
137 | False
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | 10.0
156 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
157 |
158 |
159 |
160 |
161 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 | True
175 |
176 |
177 |
178 |
179 |
180 | REM copy .example files to .config files if needed
181 | IF NOT EXIST "$(projectDir)\Web.Config" IF EXIST "$(projectDir)\Web.example.config" COPY "$(projectDir)\Web.example.config" "$(projectDir)\Web.Config"
182 |
183 |
184 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Services/TeamCityDataService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Web;
5 | using System.Web.Mvc;
6 | using TeamCityDashboard.Interfaces;
7 | using System.Net;
8 | using System.IO;
9 | using System.Text;
10 | using System.Xml;
11 | using TeamCityDashboard.Models;
12 | using System.Globalization;
13 |
14 | namespace TeamCityDashboard.Services
15 | {
16 | public class TeamCityDataService : AbstractHttpDataService
17 | {
18 | public TeamCityDataService(string baseUrl, string username, string password, ICacheService cacheService) : base(baseUrl, username, password, cacheService) { }
19 |
20 | ///
21 | /// url to retrieve list of projects in TeamCity
22 | ///
23 | private const string URL_PROJECTS_LIST = @"/httpAuth/app/rest/projects";
24 |
25 | ///
26 | /// url to retrieve details of given {0} project (buildtypes etc)
27 | ///
28 | private const string URL_PROJECT_DETAILS = @"/httpAuth/app/rest/projects/id:{0}";
29 |
30 | ///
31 | /// retrieve the first 100 builds of the given buildconfig and retrieve the status of it
32 | ///
33 | private const string URL_BUILDS_LIST = @"/httpAuth/app/rest/buildTypes/id:{0}/builds";
34 |
35 | ///
36 | /// retrieve details of the given build ({0}) and verify that the /buildType/settings/property[@name='allowExternalStatus'] == 'true'
37 | ///
38 | private const string URL_BUILD_DETAILS = @"/httpAuth/app/rest/buildTypes/id:{0}";
39 |
40 | ///
41 | /// url to retrieve the changes commited in the given {0} buildrunId
42 | ///
43 | private const string URL_BUILD_CHANGES = @"/httpAuth/app/rest/changes?build=id:{0}";
44 |
45 | ///
46 | /// Url to retrieve the details of the given {0} changeId
47 | ///
48 | private const string URL_CHANGE_DETAILS = @"/httpAuth/app/rest/changes/id:{0}";
49 |
50 | ///
51 | /// url to retrieve the emailaddress of the given {0} userId
52 | ///
53 | private const string URL_USER_EMAILADDRESS = @"/httpAuth/app/rest/users/id:{0}/email";
54 |
55 | ///
56 | /// take all failed projects with at least one build config visible AND max 15 successfull build configs
57 | ///
58 | ///
59 | public IEnumerable GetActiveProjects()
60 | {
61 | var projects = getNonArchivedProjects().ToList();
62 |
63 | var failing = projects.Where(p => p.BuildConfigs.Any(c => !c.CurrentBuildIsSuccesfull));
64 | var success = projects.Where(p => p.BuildConfigs.All(c => c.CurrentBuildIsSuccesfull));
65 |
66 | int amountToTake = Math.Max(15, failing.Count());
67 |
68 | //only display the most recent 15 build projects together with the failing ones OR if we have more failing display those
69 | return failing.Concat(success.OrderByDescending(p => p.LastBuildDate)).Take(amountToTake);
70 | }
71 |
72 | private IEnumerable getNonArchivedProjects()
73 | {
74 | XmlDocument projectsPageContent = GetPageContents(URL_PROJECTS_LIST);
75 | if (projectsPageContent == null)
76 | yield break;
77 |
78 | foreach (XmlElement el in projectsPageContent.SelectNodes("//project"))
79 | {
80 | var project = ParseProjectDetails(el.GetAttribute("id"), el.GetAttribute("name"));
81 | if (project == null)
82 | continue;
83 | yield return project;
84 | }
85 | }
86 |
87 | ///
88 | /// only retrieve non-archived projects etc
89 | ///
90 | ///
91 | ///
92 | ///
93 | private IProject ParseProjectDetails(string projectId, string projectName)
94 | {
95 | //determine details, archived? buildconfigs
96 | XmlDocument projectDetails = CacheService.Get("project-details-" + projectId, () => {
97 | return GetPageContents(string.Format(URL_PROJECT_DETAILS, projectId));
98 | }, 15 * 60);
99 |
100 | if (projectDetails == null)
101 | return null;
102 |
103 | if (projectDetails.DocumentElement.GetAttribute("archived") == "true")
104 | return null;//not needed
105 |
106 | List buildConfigs = new List();
107 | foreach (XmlElement buildType in projectDetails.SelectNodes("project/buildTypes/buildType"))
108 | {
109 | var buildConfigDetails = ParseBuildConfigDetails(buildType.GetAttribute("id"), buildType.GetAttribute("name"));
110 | if (buildConfigDetails != null)
111 | buildConfigs.Add(buildConfigDetails);
112 | }
113 |
114 | if (buildConfigs.Count == 0)
115 | return null;//do not report 'empty' projects'
116 |
117 | return new Project
118 | {
119 | Id = projectId,
120 | Name = projectName,
121 | Url = projectDetails.DocumentElement.GetAttribute("webUrl"),
122 | IconUrl = parseProjectProperty(projectDetails, "dashboard.project.logo.url"),
123 | SonarProjectKey = parseProjectProperty(projectDetails, "sonar.project.key"),
124 | BuildConfigs = buildConfigs,
125 | LastBuildDate = (buildConfigs.Where(b => b.CurrentBuildDate.HasValue).Max(b => b.CurrentBuildDate.Value))
126 | };
127 | }
128 |
129 | private static string parseProjectProperty(XmlDocument projectDetails, string propertyName)
130 | {
131 | var propertyElement = projectDetails.SelectSingleNode(string.Format("project/parameters/property[@name='{0}']/@value", propertyName));
132 | if (propertyElement != null)
133 | return propertyElement.Value;
134 |
135 | return null;
136 | }
137 |
138 | ///
139 | /// Retrieve the build configdetails with the given ID. return NULL if the given config is not visible in the widget interface
140 | ///
141 | ///
142 | ///
143 | ///
144 | private IBuildConfig ParseBuildConfigDetails(string id, string name)
145 | {
146 | //do we need to show this buildCOnfig?
147 | bool isVisibleExternally = CacheService.Get("build-visible-widgetinterface-" + id, () => IsBuildVisibleOnExternalWidgetInterface(id), CACHE_DURATION).Visible;
148 | if (!isVisibleExternally)
149 | return null;
150 |
151 | ///retrieve details of last 100 builds and find out if the last (=first row) was succesfull or iterate untill we found the first breaker?
152 | XmlDocument buildResultsDoc = GetPageContents(string.Format(URL_BUILDS_LIST, id));
153 | XmlElement lastBuild = buildResultsDoc.DocumentElement.FirstChild as XmlElement;
154 |
155 |
156 | DateTime? currentBuildDate = null;
157 | bool currentBuildSuccesfull = true;
158 | List buildBreakerEmailaddress = new List();
159 |
160 | if (lastBuild != null)
161 | {
162 | currentBuildSuccesfull = lastBuild.GetAttribute("status") == "SUCCESS";//we default to true
163 |
164 | //try to parse last date
165 | string buildDate = lastBuild.GetAttribute("startDate");
166 | DateTime theDate;
167 |
168 | //TeamCity date format => 20121101T134409+0100
169 | if (DateTime.TryParseExact(buildDate, @"yyyyMMdd\THHmmsszz\0\0", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out theDate))
170 | {
171 | currentBuildDate = theDate;
172 | }
173 |
174 | //if the last build was not successfull iterate back untill we found one which was successfull so we know who might have broke it.
175 | if (!currentBuildSuccesfull)
176 | {
177 | XmlNode lastSuccessfullBuild = buildResultsDoc.DocumentElement.SelectSingleNode("build[@status='SUCCESS']");
178 | if (lastSuccessfullBuild != null)
179 | {
180 | XmlElement breakingBuild = lastSuccessfullBuild.PreviousSibling as XmlElement;
181 | if (breakingBuild == null)
182 | buildBreakerEmailaddress.Add("no-breaking-build-after-succes-should-not-happen");
183 | else
184 | buildBreakerEmailaddress = CacheService.Get>(
185 | "buildbreakers-build-" + breakingBuild.GetAttribute("id"),
186 | () => ParseBuildBreakerDetails(breakingBuild.GetAttribute("id")),
187 | CACHE_DURATION
188 | ).Distinct().ToList();
189 | }
190 | else
191 | {
192 | //IF NO previous pages with older builds available then we can assume this is the first build and it broke. show image of that one.
193 | //TODO we could iterate older builds to find the breaker via above logic
194 | }
195 | }
196 | }
197 |
198 | return new BuildConfig
199 | {
200 | Id = id,
201 | Name = name,
202 | Url = new Uri(string.Format("{0}/viewType.html?buildTypeId={1}&tab=buildTypeStatusDiv", BaseUrl, id)).ToString(),
203 | CurrentBuildIsSuccesfull = currentBuildSuccesfull,
204 | CurrentBuildDate = currentBuildDate,
205 | PossibleBuildBreakerEmailAddresses = buildBreakerEmailaddress
206 | };
207 | }
208 |
209 | private ProjectVisible IsBuildVisibleOnExternalWidgetInterface(string id)
210 | {
211 | XmlDocument buildConfigDetails = GetPageContents(string.Format(URL_BUILD_DETAILS, id));
212 | XmlElement externalStatusEl = buildConfigDetails.SelectSingleNode("buildType/settings/property[@name='allowExternalStatus']") as XmlElement;
213 | return new ProjectVisible
214 | {
215 | Visible = externalStatusEl != null && externalStatusEl.GetAttribute("value") == "true"//no external status visible
216 | };
217 | }
218 |
219 | ///
220 | /// retrieve the emailaddress of the user who changed something in the given buildId because he most likely broke it
221 | ///
222 | ///
223 | ///
224 | private IEnumerable ParseBuildBreakerDetails(string buildId)
225 | {
226 | //retrieve changes
227 | XmlDocument buildChangesDoc = GetPageContents(string.Format(URL_BUILD_CHANGES, buildId));
228 | foreach (XmlElement el in buildChangesDoc.SelectNodes("//change"))
229 | {
230 | //retrieve change details
231 | string changeId = el.GetAttribute("id");
232 | if (string.IsNullOrEmpty(changeId))
233 | throw new ArgumentNullException(string.Format("@id of change within buildId {0} should not be NULL", buildId));
234 |
235 | //retrieve userid who changed something//details
236 | XmlDocument changeDetailsDoc = GetPageContents(string.Format(URL_CHANGE_DETAILS, changeId));
237 | XmlElement userDetails = (changeDetailsDoc.SelectSingleNode("change/user") as XmlElement);
238 | if (userDetails == null)
239 | continue;//sometimes a change is not linked to a user who commited it.. ?
240 |
241 | string userId = userDetails.GetAttribute("id");
242 | if (userId == null)
243 | throw new ArgumentNullException(string.Format("No userId given in changeId {0}", changeId));
244 |
245 | //retrieve email
246 | string email = CacheService.Get("user-email-" + userId, () => GetUserEmailAddress(userId), CACHE_DURATION);
247 | if (!string.IsNullOrEmpty(email))
248 | yield return email.ToLower().Trim();
249 | }
250 | }
251 |
252 | private string GetUserEmailAddress(string userId)
253 | {
254 | return GetContents(string.Format(URL_USER_EMAILADDRESS, userId));
255 | }
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Views/Dashboard/Index.aspx:
--------------------------------------------------------------------------------
1 | <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage" %>
2 |
3 |
4 | Q42 Continuous Integration
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
311 |
312 |
313 | Actual Continuous Integration Status
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 | Pushes to GitHub
322 |
323 |
324 |
325 |
--------------------------------------------------------------------------------
/TeamCityDashboard/Scripts/jquery.crypt.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * jQuery Cryptography Plug-in
3 | * version: 1.0.0 (24 Sep 2008)
4 | * copyright 2008 Scott Thompson http://www.itsyndicate.ca - scott@itsyndicate.ca
5 | * http://www.opensource.org/licenses/mit-license.php
6 | */
7 | /*
8 | * A set of functions to do some basic cryptography encoding/decoding
9 | * I compiled from some javascripts I found into a jQuery plug-in.
10 | * Thanks go out to the original authors.
11 | *
12 | * Also a big thanks to Wade W. Hedgren http://homepages.uc.edu/~hedgreww
13 | * for the 1.1.1 upgrade to conform correctly to RFC4648 Sec5 url save base64
14 | *
15 | * Changelog: 1.1.0
16 | * - rewrote plugin to use only one item in the namespace
17 | *
18 | * Changelog: 1.1.1
19 | * - added code to base64 to allow URL and Filename Safe Alphabet (RFC4648 Sec5)
20 | *
21 | * --- Base64 Encoding and Decoding code was written by
22 | *
23 | * Base64 code from Tyler Akins -- http://rumkin.com
24 | * and is placed in the public domain
25 | *
26 | *
27 | * --- MD5 and SHA1 Functions based upon Paul Johnston's javascript libraries.
28 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
29 | * Digest Algorithm, as defined in RFC 1321.
30 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
31 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
32 | * Distributed under the BSD License
33 | * See http://pajhome.org.uk/crypt/md5 for more info.
34 | *
35 | * xTea Encrypt and Decrypt
36 | * copyright 2000-2005 Chris Veness
37 | * http://www.movable-type.co.uk
38 | *
39 | *
40 | * Examples:
41 | *
42 | var md5 = $().crypt({method:"md5",source:$("#phrase").val()});
43 | var sha1 = $().crypt({method:"sha1",source:$("#phrase").val()});
44 | var b64 = $().crypt({method:"b64enc",source:$("#phrase").val()});
45 | var b64dec = $().crypt({method:"b64dec",source:b64});
46 | var xtea = $().crypt({method:"xteaenc",source:$("#phrase").val(),keyPass:$("#passPhrase").val()});
47 | var xteadec = $().crypt({method:"xteadec",source:xtea,keyPass:$("#passPhrase").val()});
48 | var xteab64 = $().crypt({method:"xteab64enc",source:$("#phrase").val(),keyPass:$("#passPhrase").val()});
49 | var xteab64dec = $().crypt({method:"xteab64dec",source:xteab64,keyPass:$("#passPhrase").val()});
50 |
51 | You can also pass source this way.
52 | var md5 = $("#idOfSource").crypt({method:"md5"});
53 | *
54 | */
55 | (function($){
56 | $.fn.crypt = function(options) {
57 | var defaults = {
58 | b64Str : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
59 | strKey : "123",
60 | method : "md5",
61 | source : "",
62 | chrsz : 8, /* md5 - bits per input character. 8 - ASCII; 16 - Unicode */
63 | hexcase : 0 /* md5 - hex output format. 0 - lowercase; 1 - uppercase */
64 | };
65 |
66 | // code to enable URL and Filename Safe Alphabet (RFC4648 Sec5)
67 | if (typeof(options.urlsafe) == 'undefined'){
68 | defaults.b64Str += '+/=';
69 | options.urlsafe = false;
70 | }else if (options.urlsafe){
71 | defaults.b64Str += '-_=';
72 | }else{
73 | defaults.b64Str += '+/=';
74 | }
75 |
76 | var opts = $.extend(defaults, options);
77 |
78 | // support for $("#name").crypt.....
79 | if (!opts.source) {
80 | var $this = $(this);
81 | // determine if it's a div or a textarea
82 | if ($this.html()) opts.source = $this.html();
83 | else if ($this.val()) opts.source = $this.val();
84 | else {alert("Please provide source text");return false;};
85 | };
86 |
87 | if (opts.method == 'md5') {
88 | return md5(opts);
89 | } else if (opts.method == 'sha1') {
90 | return sha1(opts);
91 | } else if (opts.method == 'b64enc') {
92 | return b64enc(opts);
93 | } else if (opts.method == 'b64dec') {
94 | return b64dec(opts);
95 | } else if (opts.method == 'xteaenc') {
96 | return xteaenc(opts);
97 | } else if (opts.method == 'xteadec') {
98 | return xteadec(opts);
99 | } else if (opts.method == 'xteab64enc') {
100 | var tmpenc = xteaenc(opts);
101 | opts.method = "b64enc";
102 | opts.source = tmpenc;
103 | return b64enc(opts);
104 | } else if (opts.method == 'xteab64dec') {
105 | var tmpdec = b64dec(opts);
106 | opts.method = "xteadec";
107 | opts.source = tmpdec;
108 | return xteadec(opts);
109 | }
110 |
111 |
112 | function b64enc(params) {
113 |
114 | var output = "";
115 | var chr1, chr2, chr3;
116 | var enc1, enc2, enc3, enc4;
117 | var i = 0;
118 |
119 | do {
120 | chr1 = params.source.charCodeAt(i++);
121 | chr2 = params.source.charCodeAt(i++);
122 | chr3 = params.source.charCodeAt(i++);
123 |
124 | enc1 = chr1 >> 2;
125 | enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
126 | enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
127 | enc4 = chr3 & 63;
128 |
129 | if (isNaN(chr2)) {
130 | enc3 = enc4 = 64;
131 | } else if (isNaN(chr3)) {
132 | enc4 = 64;
133 | };
134 |
135 | output += params.b64Str.charAt(enc1)
136 | + params.b64Str.charAt(enc2)
137 | + params.b64Str.charAt(enc3)
138 | + params.b64Str.charAt(enc4);
139 |
140 |
141 | } while (i < params.source.length);
142 |
143 | return output;
144 |
145 | };
146 |
147 | function b64dec(params) {
148 |
149 | var output = "";
150 | var chr1, chr2, chr3;
151 | var enc1, enc2, enc3, enc4;
152 | var i = 0;
153 |
154 | // remove all characters that are not A-Z, a-z, 0-9, !, -, or _
155 |
156 | // remove all characters that are not A-Z, a-z, 0-9, !, -, or _
157 | // params.source = params.source.replace(/[^A-Za-z0-9!_-]/g, "");
158 |
159 | var re = new RegExp ('[^A-Za-z0-9' + params.b64Str.substr(-3) + ']', 'g');
160 | params.source = params.source.replace(re, "");
161 |
162 | do {
163 | enc1 = params.b64Str.indexOf(params.source.charAt(i++));
164 | enc2 = params.b64Str.indexOf(params.source.charAt(i++));
165 | enc3 = params.b64Str.indexOf(params.source.charAt(i++));
166 | enc4 = params.b64Str.indexOf(params.source.charAt(i++));
167 |
168 | chr1 = (enc1 << 2) | (enc2 >> 4);
169 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
170 | chr3 = ((enc3 & 3) << 6) | enc4;
171 |
172 | output = output + String.fromCharCode(chr1);
173 |
174 | if (enc3 != 64) {
175 | output = output + String.fromCharCode(chr2);
176 | }
177 | if (enc4 != 64) {
178 | output = output + String.fromCharCode(chr3);
179 | }
180 | } while (i < params.source.length);
181 |
182 | return output;
183 | };
184 |
185 |
186 | function md5(params) {
187 | /* This is a trimmed version of Paul Johnsons JavaScript
188 | *
189 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
190 | * Digest Algorithm, as defined in RFC 1321.
191 | * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002.
192 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
193 | * Distributed under the BSD License
194 | * See http://pajhome.org.uk/crypt/md5 for more info.
195 | */
196 |
197 | //var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
198 | //var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
199 |
200 | return binl2hex(core_md5(str2binl(params.source), params.source.length * params.chrsz));
201 |
202 | /*
203 | * Convert an array of little-endian words to a hex string.
204 | */
205 | function binl2hex(binarray)
206 | {
207 | var hex_tab = params.hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
208 | var str = "";
209 | for(var i = 0; i < binarray.length * 4; i++)
210 | {
211 | str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) +
212 | hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF);
213 | };
214 | return str;
215 | };
216 |
217 | /*
218 | * Calculate the HMAC-MD5, of a key and some data
219 | */
220 | function core_hmac_md5(key, data)
221 | {
222 | var bkey = str2binl(key);
223 | if(bkey.length > 16) bkey = core_md5(bkey, key.length * params.chrsz);
224 |
225 | var ipad = Array(16), opad = Array(16);
226 | for(var i = 0; i < 16; i++)
227 | {
228 | ipad[i] = bkey[i] ^ 0x36363636;
229 | opad[i] = bkey[i] ^ 0x5C5C5C5C;
230 | };
231 |
232 | var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * params.chrsz);
233 | return core_md5(opad.concat(hash), 512 + 128);
234 | };
235 |
236 | /*
237 | * Convert a string to an array of little-endian words
238 | * If chrsz is ASCII, characters >255 have their hi-byte silently ignored.
239 | */
240 | function str2binl(str)
241 | {
242 | var bin = Array();
243 | var mask = (1 << params.chrsz) - 1;
244 | for(var i = 0; i < str.length * params.chrsz; i += params.chrsz)
245 | bin[i>>5] |= (str.charCodeAt(i / params.chrsz) & mask) << (i%32);
246 | return bin;
247 | }
248 |
249 |
250 | /*
251 | * Bitwise rotate a 32-bit number to the left.
252 | */
253 | function bit_rol(num, cnt)
254 | {
255 | return (num << cnt) | (num >>> (32 - cnt));
256 | }
257 |
258 |
259 | /*
260 | * These functions implement the four basic operations the algorithm uses.
261 | */
262 | function md5_cmn(q, a, b, x, s, t)
263 | {
264 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
265 | }
266 | function md5_ff(a, b, c, d, x, s, t)
267 | {
268 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
269 | }
270 | function md5_gg(a, b, c, d, x, s, t)
271 | {
272 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
273 | }
274 | function md5_hh(a, b, c, d, x, s, t)
275 | {
276 | return md5_cmn(b ^ c ^ d, a, b, x, s, t);
277 | }
278 | function md5_ii(a, b, c, d, x, s, t)
279 | {
280 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
281 | }
282 |
283 | /*
284 | * Calculate the MD5 of an array of little-endian words, and a bit length
285 | */
286 | function core_md5(x, len)
287 | {
288 | /* append padding */
289 | x[len >> 5] |= 0x80 << ((len) % 32);
290 | x[(((len + 64) >>> 9) << 4) + 14] = len;
291 |
292 | var a = 1732584193;
293 | var b = -271733879;
294 | var c = -1732584194;
295 | var d = 271733878;
296 |
297 | for(var i = 0; i < x.length; i += 16)
298 | {
299 | var olda = a;
300 | var oldb = b;
301 | var oldc = c;
302 | var oldd = d;
303 |
304 | a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
305 | d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
306 | c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819);
307 | b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
308 | a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
309 | d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426);
310 | c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
311 | b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
312 | a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
313 | d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
314 | c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
315 | b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
316 | a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682);
317 | d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
318 | c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
319 | b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329);
320 |
321 | a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
322 | d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
323 | c = md5_gg(c, d, a, b, x[i+11], 14, 643717713);
324 | b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
325 | a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
326 | d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083);
327 | c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
328 | b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
329 | a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438);
330 | d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
331 | c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
332 | b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501);
333 | a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
334 | d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
335 | c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473);
336 | b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);
337 |
338 | a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
339 | d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
340 | c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562);
341 | b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
342 | a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
343 | d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353);
344 | c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
345 | b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
346 | a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174);
347 | d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
348 | c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
349 | b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189);
350 | a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
351 | d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
352 | c = md5_hh(c, d, a, b, x[i+15], 16, 530742520);
353 | b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);
354 |
355 | a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
356 | d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415);
357 | c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
358 | b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
359 | a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571);
360 | d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
361 | c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
362 | b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
363 | a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
364 | d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
365 | c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
366 | b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649);
367 | a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
368 | d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
369 | c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259);
370 | b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);
371 |
372 | a = safe_add(a, olda);
373 | b = safe_add(b, oldb);
374 | c = safe_add(c, oldc);
375 | d = safe_add(d, oldd);
376 | };
377 | return Array(a, b, c, d);
378 |
379 | };
380 |
381 | };
382 |
383 | /*
384 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
385 | * to work around bugs in some JS interpreters. (used by md5 and sha1)
386 | */
387 | function safe_add(x, y)
388 | {
389 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
390 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
391 | return (msw << 16) | (lsw & 0xFFFF);
392 | };
393 |
394 | function sha1(params) {
395 | return binb2hex(core_sha1(str2binb(params.source),params.source.length * params.chrsz));
396 |
397 | /*
398 | * Calculate the SHA-1 of an array of big-endian words, and a bit length
399 | */
400 | function core_sha1(x, len)
401 | {
402 | /* append padding */
403 | x[len >> 5] |= 0x80 << (24 - len % 32);
404 | x[((len + 64 >> 9) << 4) + 15] = len;
405 |
406 | var w = Array(80);
407 | var a = 1732584193;
408 | var b = -271733879;
409 | var c = -1732584194;
410 | var d = 271733878;
411 | var e = -1009589776;
412 |
413 | for(var i = 0; i < x.length; i += 16)
414 | {
415 | var olda = a;
416 | var oldb = b;
417 | var oldc = c;
418 | var oldd = d;
419 | var olde = e;
420 |
421 | for(var j = 0; j < 80; j++)
422 | {
423 | if(j < 16) w[j] = x[i + j];
424 | else w[j] = rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1);
425 | var t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)),
426 | safe_add(safe_add(e, w[j]), sha1_kt(j)));
427 | e = d;
428 | d = c;
429 | c = rol(b, 30);
430 | b = a;
431 | a = t;
432 | }
433 |
434 | a = safe_add(a, olda);
435 | b = safe_add(b, oldb);
436 | c = safe_add(c, oldc);
437 | d = safe_add(d, oldd);
438 | e = safe_add(e, olde);
439 | }
440 | return Array(a, b, c, d, e);
441 |
442 | }
443 | /*
444 | * Bitwise rotate a 32-bit number to the left.
445 | */
446 | function rol(num, cnt)
447 | {
448 | return (num << cnt) | (num >>> (32 - cnt));
449 | }
450 |
451 | /*
452 | * Determine the appropriate additive constant for the current iteration
453 | */
454 | function sha1_kt(t)
455 | {
456 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 :
457 | (t < 60) ? -1894007588 : -899497514;
458 | }
459 | /*
460 | * Perform the appropriate triplet combination function for the current
461 | * iteration
462 | */
463 | function sha1_ft(t, b, c, d)
464 | {
465 | if(t < 20) return (b & c) | ((~b) & d);
466 | if(t < 40) return b ^ c ^ d;
467 | if(t < 60) return (b & c) | (b & d) | (c & d);
468 | return b ^ c ^ d;
469 | }
470 |
471 | /*
472 | * Convert an array of big-endian words to a hex string.
473 | */
474 | function binb2hex(binarray)
475 | {
476 | var hex_tab = params.hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
477 | var str = "";
478 | for(var i = 0; i < binarray.length * 4; i++)
479 | {
480 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
481 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
482 | }
483 | return str;
484 | }
485 |
486 |
487 | /*
488 | * Convert an 8-bit or 16-bit string to an array of big-endian words
489 | * In 8-bit function, characters >255 have their hi-byte silently ignored.
490 | */
491 | function str2binb(str)
492 | {
493 | var bin = Array();
494 | var mask = (1 << params.chrsz) - 1;
495 | for(var i = 0; i < str.length * params.chrsz; i += params.chrsz)
496 | bin[i>>5] |= (str.charCodeAt(i / params.chrsz) & mask) << (32 - params.chrsz - i%32);
497 | return bin;
498 | }
499 |
500 | };
501 |
502 | function xteaenc(params) {
503 | var v = new Array(2), k = new Array(4), s = "", i;
504 |
505 | params.source = escape(params.source); // use escape() so only have single-byte chars to encode
506 |
507 | // build key directly from 1st 16 chars of strKey
508 | for (var i=0; i<4; i++) k[i] = Str4ToLong(params.strKey.slice(i*4,(i+1)*4));
509 |
510 | for (i=0; i>>5)+z ^ sum+k[sum & 3];
529 | sum += delta;
530 | z += (y<<4 ^ y>>>5)+y ^ sum+k[sum>>>11 & 3];
531 | // note: unsigned right-shift '>>>' is used in place of original '>>', due to lack
532 | // of 'unsigned' type declaration in JavaScript (thanks to Karsten Kraus for this)
533 | }
534 | v[0] = y;v[1] = z;
535 | }
536 | };
537 |
538 | function xteadec(params) {
539 | var v = new Array(2), k = new Array(4), s = "", i;
540 |
541 | for (var i=0; i<4; i++) k[i] = Str4ToLong(params.strKey.slice(i*4,(i+1)*4));
542 |
543 | ciphertext = unescCtrlCh(params.source);
544 | for (i=0; i>>5)+y ^ sum+k[sum>>>11 & 3];
563 | sum -= delta;
564 | y -= (z<<4 ^ z>>>5)+z ^ sum+k[sum & 3];
565 | }
566 | v[0] = y;v[1] = z;
567 | }
568 |
569 | };
570 |
571 | // xtea supporting functions
572 | function Str4ToLong(s) { // convert 4 chars of s to a numeric long
573 | var v = 0;
574 | for (var i=0; i<4; i++) v |= s.charCodeAt(i) << i*8;
575 | return isNaN(v) ? 0 : v;
576 | };
577 |
578 | function LongToStr4(v) { // convert a numeric long to 4 char string
579 | var s = String.fromCharCode(v & 0xFF, v>>8 & 0xFF, v>>16 & 0xFF, v>>24 & 0xFF);
580 | return s;
581 | };
582 |
583 | function escCtrlCh(str) { // escape control chars which might cause problems with encrypted texts
584 | return str.replace(/[\0\t\n\v\f\r\xa0'"!]/g, function(c) {return '!' + c.charCodeAt(0) + '!';});
585 | };
586 |
587 | function unescCtrlCh(str) { // unescape potentially problematic nulls and control characters
588 | return str.replace(/!\d\d?\d?!/g, function(c) {return String.fromCharCode(c.slice(1,-1));});
589 | };
590 |
591 |
592 |
593 | };
594 | })(jQuery);
595 |
596 |
597 |
--------------------------------------------------------------------------------
|