initial import for Open Source 🎉

This commit is contained in:
Jimmy Zelinskie 2019-11-12 11:09:47 -05:00
parent 1898c361f3
commit 9c0dd3b722
2048 changed files with 218743 additions and 0 deletions

42
static/js/services/angular-helper.js vendored Normal file
View file

@ -0,0 +1,42 @@
/**
* Helper code for working with angular.
*/
angular.module('quay').factory('AngularHelper', [function($routeProvider) {
var helper = {};
helper.buildConditionalLinker = function($animate, name, evaluator) {
// Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
return function ($scope, $element, $attr, ctrl, $transclude) {
var block;
var childScope;
var roles;
$attr.$observe(name, function (value) {
if (evaluator($scope.$eval(value))) {
if (!childScope) {
childScope = $scope.$new();
$transclude(childScope, function (clone) {
block = {
startNode: clone[0],
endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ')
};
$animate.enter(clone, $element.parent(), $element);
});
}
} else {
if (childScope) {
childScope.$destroy();
childScope = null;
}
if (block) {
$animate.leave(getBlockElements(block));
block = null;
}
}
});
}
};
return helper;
}]);

View file

@ -0,0 +1,107 @@
/**
* Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
*/
angular.module('quay').factory('AngularPollChannel',
['ApiService', '$timeout', 'DocumentVisibilityService', 'CORE_EVENT', '$rootScope',
function(ApiService, $timeout, DocumentVisibilityService, CORE_EVENT, $rootScope) {
var _PollChannel = function(scope, requester, opt_sleeptime) {
this.scope_ = scope;
this.requester_ = requester;
this.sleeptime_ = opt_sleeptime || (60 * 1000 /* 60s */);
this.timer_ = null;
this.working = false;
this.polling = false;
this.skipping = false;
var that = this;
var visibilityHandler = $rootScope.$on(CORE_EVENT.DOC_VISIBILITY_CHANGE, function() {
// If the poll channel was skipping because the visibility was hidden, call it immediately.
if (that.skipping && !DocumentVisibilityService.isHidden()) {
that.call_();
}
});
scope.$on('$destroy', function() {
that.stop();
visibilityHandler();
});
};
_PollChannel.prototype.setSleepTime = function(sleepTime) {
this.sleeptime_ = sleepTime;
this.stop();
this.start(true);
};
_PollChannel.prototype.stop = function() {
if (this.timer_) {
$timeout.cancel(this.timer_);
this.timer_ = null;
this.polling = false;
}
this.skipping = false;
this.working = false;
};
_PollChannel.prototype.start = function(opt_skipFirstCall) {
// Make sure we invoke call outside the normal digest cycle, since
// we'll call $scope.$apply ourselves.
var that = this;
setTimeout(function() {
if (opt_skipFirstCall) {
that.setupTimer_();
return;
}
that.call_();
}, 0);
};
_PollChannel.prototype.call_ = function() {
if (this.working) { return; }
// If the document is currently hidden, skip the call.
if (DocumentVisibilityService.isHidden()) {
this.skipping = true;
this.setupTimer_();
return;
}
var that = this;
this.working = true;
$timeout(function() {
that.requester_(function(status) {
if (status) {
that.working = false;
that.skipping = false;
that.setupTimer_();
} else {
that.stop();
}
});
}, 0);
};
_PollChannel.prototype.setupTimer_ = function() {
if (this.timer_) { return; }
var that = this;
this.polling = true;
this.timer_ = $timeout(function() {
that.timer_ = null;
that.call_();
}, this.sleeptime_)
};
var service = {
'create': function(scope, requester, opt_sleeptime) {
return new _PollChannel(scope, requester, opt_sleeptime);
}
};
return service;
}]);

View file

@ -0,0 +1,328 @@
var urlParseURL = require('url-parse');
/**
* Service which exposes the server-defined API as a nice set of helper methods and automatic
* callbacks. Any method defined on the server is exposed here as an equivalent method. Also
* defines some helper functions for working with API responses.
*/
angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService', function(Restangular, $q, UtilService) {
var apiService = {};
var getResource = function(getMethod, operation, opt_parameters, opt_background) {
var resource = {};
resource.withOptions = function(options) {
this.options = options;
return this;
};
resource.get = function(processor, opt_errorHandler) {
var options = this.options;
var result = {
'loading': true,
'value': null,
'hasError': false
};
getMethod(options, opt_parameters, opt_background, true).then(function(resp) {
result.value = processor(resp);
result.loading = false;
}, function(resp) {
result.hasError = true;
result.loading = false;
if (opt_errorHandler) {
opt_errorHandler(resp);
}
});
return result;
};
return resource;
};
var buildUrl = function(path, parameters) {
// We already have /api/v1/ on the URLs, so remove them from the paths.
path = path.substr('/api/v1/'.length, path.length);
// Build the path, adjusted with the inline parameters.
var used = {};
var urlPath = '';
for (var i = 0; i < path.length; ++i) {
var c = path[i];
if (c == '{') {
var end = path.indexOf('}', i);
var varName = path.substr(i + 1, end - i - 1);
if (!parameters[varName]) {
throw new Error('Missing parameter: ' + varName);
}
used[varName] = true;
urlPath += encodeURI(parameters[varName]);
i = end;
continue;
}
urlPath += c;
}
// Append any query parameters.
var url = new urlParseURL(urlPath, '/');
url.query = {};
for (var paramName in parameters) {
if (!parameters.hasOwnProperty(paramName)) { continue; }
if (used[paramName]) { continue; }
var value = parameters[paramName];
if (value != null) {
url.query[paramName] = value
}
}
return url.toString();
};
var getGenericOperationName = function(userOperationName) {
return userOperationName.replace('User', '');
};
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
if (userRelatedResource) {
if (userRelatedResource[method.toLowerCase()]) {
return userRelatedResource[method.toLowerCase()]['operationId'];
}
}
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
};
var freshLoginInProgress = [];
var reject = function(msg) {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].deferred.reject({'data': {'message': msg}});
}
freshLoginInProgress = [];
};
var retry = function() {
for (var i = 0; i < freshLoginInProgress.length; ++i) {
freshLoginInProgress[i].retry();
}
freshLoginInProgress = [];
};
var freshLoginFailCheck = function(opName, opArgs) {
return function(resp) {
var deferred = $q.defer();
// If the error is a fresh login required, show the dialog.
// TODO: remove error_type (old style error)
var fresh_login_required = resp.data['title'] == 'fresh_login_required' || resp.data['error_type'] == 'fresh_login_required';
if (resp.status == 401 && fresh_login_required) {
var retryOperation = function() {
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
}, function(resp) {
deferred.reject(resp);
});
};
var verifyNow = function() {
if (!$('#freshPassword').val()) {
return;
}
var info = {
'password': $('#freshPassword').val()
};
$('#freshPassword').val('');
// Conduct the sign in of the user.
apiService.verifyUser(info).then(function() {
// On success, retry the operations. if it succeeds, then resolve the
// deferred promise with the result. Otherwise, reject the same.
retry();
}, function(resp) {
// Reject with the sign in error.
reject('Invalid verification credentials');
});
};
// Add the retry call to the in progress list. If there is more than a single
// in progress call, we skip showing the dialog (since it has already been
// shown).
freshLoginInProgress.push({
'deferred': deferred,
'retry': retryOperation
})
if (freshLoginInProgress.length > 1) {
return deferred.promise;
}
var box = bootbox.dialog({
"message": 'It has been more than a few minutes since you last logged in, ' +
'so please verify your password to perform this sensitive operation:' +
'<form style="margin-top: 10px" action="javascript:$(\'.btn-continue\').click();void(0)">' +
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"title": 'Please Verify',
"buttons": {
"verify": {
"label": "Verify",
"className": "btn-success btn-continue",
"callback": verifyNow
},
"close": {
"label": "Cancel",
"className": "btn-default",
"callback": function() {
reject('Verification canceled')
}
}
}
});
box.bind('shown.bs.modal', function(){
box.find("input").focus();
box.find("form").submit(function() {
if (!$('#freshPassword').val()) { return; }
box.modal('hide');
verifyNow();
});
});
// Return a new promise. We'll accept or reject it based on the result
// of the login.
return deferred.promise;
}
// Otherwise, we just 'raise' the error via the reject method on the promise.
return $q.reject(resp);
};
};
var buildMethodsForOperation = function(operation, method, path, resourceMap) {
var operationName = operation['operationId'];
var urlPath = path['x-path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forceget) {
var one = Restangular.one(buildUrl(urlPath, opt_parameters));
if (opt_background) {
one.withHttpConfig({
'ignoreLoadingBar': true
});
}
var opObj = one[opt_forceget ? 'get' : 'custom' + method.toUpperCase()](opt_options);
// If the operation requires_fresh_login, then add a specialized error handler that
// will defer the operation's result if sudo is requested.
if (operation['x-requires-fresh-login']) {
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
}
return opObj;
};
// If the method for the operation is a GET, add an operationAsResource method.
if (method == 'get') {
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
var getMethod = apiService[operationName];
return getResource(getMethod, operation, opt_parameters, opt_background);
};
}
// If the operation has a user-related operation, then make a generic operation for this operation
// that can call both the user and the organization versions of the operation, depending on the
// parameters given.
if (path['x-user-related']) {
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['x-user-related']]);
var genericOperationName = getGenericOperationName(userOperationName);
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
if (orgname) {
if (orgname.name) {
orgname = orgname.name;
}
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
return apiService[operationName](opt_options, params);
} else {
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
}
};
}
};
if (!window.__endpoints) {
return apiService;
}
var allowedMethods = ['get', 'post', 'put', 'delete'];
var resourceMap = {};
var forEachOperation = function(callback) {
for (var path in window.__endpoints) {
if (!window.__endpoints.hasOwnProperty(path)) {
continue;
}
for (var method in window.__endpoints[path]) {
if (!window.__endpoints[path].hasOwnProperty(method)) {
continue;
}
if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; }
callback(window.__endpoints[path][method], method, window.__endpoints[path]);
}
}
};
// Build the map of resource names to their objects.
forEachOperation(function(operation, method, path) {
resourceMap[path['x-name']] = path;
});
// Construct the methods for each API endpoint.
forEachOperation(function(operation, method, path) {
buildMethodsForOperation(operation, method, path, resourceMap);
});
apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage;
if (resp && resp['data']) {
//TODO: remove error_message and error_description (old style error)
message = resp['data']['detail'] || resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
}
return message;
};
apiService.errorDisplay = function(defaultMessage, opt_handler) {
return function(resp) {
var message = apiService.getErrorMessage(resp, defaultMessage);
if (opt_handler) {
var handlerMessage = opt_handler(resp);
if (handlerMessage) {
message = handlerMessage;
}
}
message = UtilService.stringToHTML(message);
bootbox.dialog({
"message": message,
"title": defaultMessage || 'Request Failure',
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
};
return apiService;
}]);

View file

@ -0,0 +1,97 @@
import { AvatarServiceImpl } from './avatar.service.impl';
describe("AvatarServiceImpl", () => {
var avatarServiceImpl: AvatarServiceImpl;
var configMock: any;
var md5Mock: any;
beforeEach(() => {
configMock = {AVATAR_KIND: 'local'};
md5Mock = jasmine.createSpyObj('md5Mock', ['createHash']);
avatarServiceImpl = new AvatarServiceImpl(configMock, md5Mock);
});
describe("getAvatar", () => {
var hash: string;
beforeEach(() => {
hash = "a1b2c3d4e5f6";
});
it("returns a local avatar URL if given config has avatar kind set to local", () => {
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`/avatar/${hash}?size=16`);
});
it("returns a Gravatar URL if given config has avatar kind set to Gravatar", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=404&size=16`);
});
it("uses 16 as default size query parameter if not provided", () => {
var size: number = 16;
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`/avatar/${hash}?size=${size}`);
});
it("uses 404 as default not found query parameter for Gravatar URL if not provided", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var notFound: string = '404';
var avatarURL: string = avatarServiceImpl.getAvatar(hash);
expect(avatarURL).toEqual(`//www.gravatar.com/avatar/${hash}?d=${notFound}&size=16`);
});
});
describe("computeHash", () => {
var email: string;
var name: string;
var expectedHash: string;
beforeEach(() => {
email = "some_example@gmail.com";
name = "example";
expectedHash = "a1b2c3d4e5f6";
md5Mock.createHash = jasmine.createSpy('createHashSpy').and.returnValue(expectedHash);
});
it("returns hash from cache if it exists", () => {
// Call once to set the cache
avatarServiceImpl.computeHash(email, name);
md5Mock.createHash.calls.reset();
avatarServiceImpl.computeHash(email, name);
expect(md5Mock.createHash).not.toHaveBeenCalled();
});
it("calls MD5 service to create hash using given email if cache is not set", () => {
avatarServiceImpl.computeHash(email, name);
expect(md5Mock.createHash.calls.argsFor(0)[0]).toEqual(email.toString().toLowerCase());
});
it("adds first character of given name to hash if config has avatar kind set to local", () => {
var hash: string = avatarServiceImpl.computeHash(email, name);
expect(hash[0]).toEqual(name[0]);
});
it("adds first character of given email to hash if config has avatar kind set to local and not given name", () => {
var hash: string = avatarServiceImpl.computeHash(email);
expect(hash[0]).toEqual(email[0]);
});
it("adds nothing to hash if config avatar kind is not set to local", () => {
configMock['AVATAR_KIND'] = 'gravatar';
var hash: string = avatarServiceImpl.computeHash(email);
expect(hash).toEqual(expectedHash);
});
});
});

View file

@ -0,0 +1,50 @@
import { AvatarService } from './avatar.service';
import { Injectable, Inject } from 'ng-metadata/core';
@Injectable(AvatarService.name)
export class AvatarServiceImpl implements AvatarService {
private cache: {[cacheKey: string]: string} = {};
constructor(@Inject('Config') private Config: any,
@Inject('md5') private md5: any) {
}
public getAvatar(hash: string, size: number = 16, notFound: string = '404'): string {
var avatarURL: string;
switch (this.Config['AVATAR_KIND']) {
case 'local':
avatarURL = `/avatar/${hash}?size=${size}`;
break;
case 'gravatar':
avatarURL = `//www.gravatar.com/avatar/${hash}?d=${notFound}&size=${size}`;
break;
}
return avatarURL;
}
public computeHash(email: string = '', name: string = ''): string {
const cacheKey: string = email + ':' + name;
if (this.cache[cacheKey]) {
return this.cache[cacheKey];
}
var hash: string = this.md5.createHash(email.toString().toLowerCase());
switch (this.Config['AVATAR_KIND']) {
case 'local':
if (name) {
hash = name[0] + hash;
} else if (email) {
hash = email[0] + hash;
}
break;
}
return this.cache[cacheKey] = hash;
}
}

View file

@ -0,0 +1,22 @@
/**
* Service which provides helper methods for retrieving the avatars displayed in the app.
*/
export abstract class AvatarService {
/**
* Retrieve URL for avatar image with given hash.
* @param hash Avatar image hash.
* @param size Avatar image size.
* @param notFound URL parameter if avatar image is not found.
* @return avatarURL The URL for the avatar image.
*/
public abstract getAvatar(hash: string, size?: number, notFound?: string): string;
/**
* Compute the avatar image hash.
* @param email Email for avatar user.
* @param name Username for avatar user.
* @return hash The hash for the avatar image.
*/
public abstract computeHash(email?: string, name?: string): string;
}

View file

@ -0,0 +1,43 @@
/**
* Helper service for working with the registry's container. Only works in enterprise.
*/
angular.module('quay').factory('ContainerService', ['ApiService', '$timeout', 'Restangular',
function(ApiService, $timeout, Restangular) {
var containerService = {};
containerService.restartContainer = function(callback) {
ApiService.scShutdownContainer(null, null).then(function(resp) {
$timeout(callback, 2000);
}, ApiService.errorDisplay('Cannot restart container. Please report this to support.'))
};
containerService.scheduleStatusCheck = function(callback, opt_config) {
$timeout(function() {
containerService.checkStatus(callback, opt_config);
}, 2000);
};
containerService.checkStatus = function(callback, opt_config) {
var errorHandler = function(resp) {
if (resp.status == 404 || resp.status == 502 || resp.status == -1) {
// Container has not yet come back up, so we schedule another check.
containerService.scheduleStatusCheck(callback, opt_config);
return;
}
return ApiService.errorDisplay('Cannot load status. Please report this to support')(resp);
};
// If config is specified, override the API base URL from this point onward.
if (opt_config && opt_config['SERVER_HOSTNAME']) {
var scheme = opt_config['PREFERRED_URL_SCHEME'] || 'http';
var baseUrl = scheme + '://' + opt_config['SERVER_HOSTNAME'] + '/api/v1/';
Restangular.setBaseUrl(baseUrl);
}
ApiService.scRegistryStatus(null, null, /* background */true)
.then(callback, errorHandler);
};
return containerService;
}]);

View file

@ -0,0 +1,23 @@
/**
* Helper service for working with cookies.
*/
angular.module('quay').factory('CookieService', ['$cookies', function($cookies) {
var cookieService = {};
cookieService.putPermanent = function(name, value) {
document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
};
cookieService.putSession = function(name, value) {
$cookies.put(name, value);
};
cookieService.clear = function(name) {
$cookies.remove(name);
};
cookieService.get = function(name) {
return $cookies.get(name);
};
return cookieService;
}]);

View file

@ -0,0 +1,124 @@
import { DataFileServiceImpl } from './datafile.service.impl';
import { Mock } from 'ts-mocks';
import Spy = jasmine.Spy;
describe("DataFileServiceImpl", () => {
var dataFileServiceImpl: DataFileServiceImpl;
var fileReaderMock: Mock<FileReader>;
var fileReader: FileReader;
beforeEach(() => {
fileReaderMock = new Mock<FileReader>();
fileReader = fileReaderMock.Object;
dataFileServiceImpl = new DataFileServiceImpl(() => fileReader);
});
describe("blobToString", () => {
var data: any;
var blob: Blob;
beforeEach(() => {
data = {hello: "world"};
blob = new Blob([JSON.stringify(data)]);
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: data}});
});
});
it("calls file reader to read given blob", (done) => {
dataFileServiceImpl.blobToString(blob, (result) => {
expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob);
done();
});
});
it("calls given callback with null if file reader errors", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onerror(new Mock<ProgressEvent<FileReader>>().Object);
});
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(null);
done();
});
});
it("calls given callback with null if file reader aborts", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onabort(new Mock<ProgressEvent<FileReader>>().Object);
});
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(null);
done();
});
});
it("calls given callback with result when file reader successfully loads", (done) => {
dataFileServiceImpl.blobToString(blob, (result) => {
expect(result).toBe(data);
done();
});
});
});
describe("arrayToString", () => {
var blob: Blob;
var data: any;
beforeEach(() => {
data = JSON.stringify({hello: "world"});
blob = new Blob([data], {type: 'application/octet-binary'});
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: data}});
});
});
it("calls file reader to read blob created from given buffer", (done) => {
dataFileServiceImpl.arrayToString(data, (result) => {
expect((<Spy>fileReader.readAsText).calls.argsFor(0)[0]).toEqual(blob);
done();
});
});
it("calls given callback with null if file reader errors", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onerror(new Mock<ProgressEvent<FileReader>>().Object);
});
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(null);
done();
});
});
it("calls given callback with null if file reader aborts", (done) => {
fileReaderMock.setup(mock => mock.readAsText).is((blob: Blob) => {
fileReaderMock.Object.onabort(new Mock<ProgressEvent<FileReader>>().Object);
});
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(null);
done();
});
});
it("calls given callback with result when file reader successfully loads", (done) => {
dataFileServiceImpl.arrayToString(data, (result) => {
expect(result).toEqual(data);
done();
});
});
});
describe("readDataArrayAsPossibleArchive", () => {
// TODO
});
describe("downloadDataFileAsArrayBuffer", () => {
// TODO
});
});

View file

@ -0,0 +1,182 @@
import { DataFileService } from './datafile.service';
import { Injectable, Inject } from 'ng-metadata/core';
declare const JSZip: (buf: any) => void;
declare const Zlib: any;
declare const Untar: (uint8Array: Uint8Array) => void;
@Injectable(DataFileService.name)
export class DataFileServiceImpl implements DataFileService {
constructor(@Inject('fileReaderFactory') private fileReaderFactory: () => FileReader) {
}
public blobToString(blob: Blob, callback: (result: string) => void): void {
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: Event) => callback(event.target['result']);
reader.onerror = (event: Event) => callback(null);
reader.onabort = (event: Event) => callback(null);
reader.readAsText(blob);
}
public arrayToString(buf: any, callback: (result: string) => void): void {
const blob: Blob = new Blob([buf], {type: 'application/octet-binary'});
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: Event) => callback(event.target['result']);
reader.onerror = (event: Event) => callback(null);
reader.onabort = (event: Event) => callback(null);
reader.readAsText(blob);
}
public readDataArrayAsPossibleArchive(buf: any,
success: (result: any) => void,
failure: (error: any) => void): void {
this.tryAsZip(buf, success, () => {
this.tryAsTarGz(buf, success, () => {
this.tryAsTar(buf, success, failure);
});
});
}
public downloadDataFileAsArrayBuffer($scope: ng.IScope,
url: string,
progress: (percent: number) => void,
error: () => void,
loaded: (uint8array: Uint8Array) => void): void {
var request: XMLHttpRequest = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onprogress = (e) => {
$scope.$apply(() => {
var percentLoaded;
if (e.lengthComputable) {
progress(e.loaded / e.total);
}
});
};
request.onerror = () => {
$scope.$apply(() => {
error();
});
};
request.onload = function() {
if (request.status == 200) {
$scope.$apply(() => {
var uint8array = new Uint8Array(request.response);
loaded(uint8array);
});
return;
}
};
request.send();
}
private getName(filePath: string): string {
var parts: string[] = filePath.split('/');
return parts[parts.length - 1];
}
private tryAsZip(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var zip = null;
var zipFiles = null;
try {
zip = new JSZip(buf);
zipFiles = zip.files;
} catch (e) {
failure();
return;
}
var files = [];
for (var filePath in zipFiles) {
if (zipFiles.hasOwnProperty(filePath)) {
files.push({
'name': this.getName(filePath),
'path': filePath,
'canRead': true,
'toBlob': (function(fp) {
return function() {
return new Blob([zip.file(fp).asArrayBuffer()]);
};
}(filePath))
});
}
}
success(files);
}
private tryAsTarGz(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var gunzip = new Zlib.Gunzip(new Uint8Array(buf));
var plain = null;
try {
plain = gunzip.decompress();
} catch (e) {
failure();
return;
}
if (plain.byteLength == 0) {
plain = buf;
}
this.tryAsTar(plain, success, failure);
}
private tryAsTar(buf: any, success: (result: any) => void, failure: (error?: any) => void): void {
var collapsePath = function(originalPath) {
// Tar files can contain entries of the form './', so we need to collapse
// those paths down.
var parts = originalPath.split('/');
for (var i = parts.length - 1; i >= 0; i--) {
var part = parts[i];
if (part == '.') {
parts.splice(i, 1);
}
}
return parts.join('/');
};
try {
var handler = new Untar(new Uint8Array(buf));
handler.process((status, read, files, err) => {
switch (status) {
case 'error':
failure(err);
break;
case 'done':
var processed = [];
for (var i = 0; i < files.length; ++i) {
var currentFile = files[i];
var path = collapsePath(currentFile.meta.filename);
if (path == '' || path == 'pax_global_header') { continue; }
processed.push({
'name': this.getName(path),
'path': path,
'canRead': true,
'toBlob': (function(file) {
return function() {
return new Blob([file.buffer], {type: 'application/octet-binary'});
};
}(currentFile))
});
}
success(processed);
break;
}
});
} catch (e) {
failure();
}
}
}

View file

@ -0,0 +1,48 @@
/**
* Service which provides helper methods for downloading a data file from a URL, and extracting
* its contents as .tar, .tar.gz, or .zip file. Note that this service depends on external
* library code in the lib/ directory:
* - jszip.min.js
* - Blob.js
* - zlib.js
*/
export abstract class DataFileService {
/**
* Convert a blob to a string.
* @param blob The blob to convert.
* @param callback The success callback given converted blob.
*/
public abstract blobToString(blob: Blob, callback: (result: string) => void): void;
/**
* Convert array to string.
* @param buf The array buffer to convert.
* @param callback The success callback given converted array buffer.
*/
public abstract arrayToString(buf: any, callback: (result: string) => void): void;
/**
* Determine if a given data array is an archive file.
* @param buf The data array to check.
* @param success The success callback if the given array is an archive file, given the file contents.
* @param failure The failure callback if the given array is not an archive file, given the error message.
*/
public abstract readDataArrayAsPossibleArchive(buf: any,
success: (result: any) => void,
failure: (error: any) => void): void;
/**
* Download a file into an array buffer while tracking progress.
* @param $scope An AngularJS $scope instance.
* @param url The URL of the file to be downloaded.
* @param progress The callback for download progress.
* @param error The error callback.
* @param loaded The success callback given the downloaded array buffer.
*/
public abstract downloadDataFileAsArrayBuffer($scope: ng.IScope,
url: string,
progress: (percent: number) => void,
error: () => void,
loaded: (uint8array: Uint8Array) => void): void;
}

View file

@ -0,0 +1,293 @@
import { DockerfileServiceImpl, DockerfileInfoImpl } from './dockerfile.service.impl';
import { DataFileService } from '../datafile/datafile.service';
import Spy = jasmine.Spy;
import { Mock } from 'ts-mocks';
describe("DockerfileServiceImpl", () => {
var dockerfileServiceImpl: DockerfileServiceImpl;
var dataFileServiceMock: Mock<DataFileService>;
var dataFileService: DataFileService;
var configMock: any;
var fileReaderMock: Mock<FileReader>;
beforeEach(() => {
dataFileServiceMock = new Mock<DataFileService>();
dataFileService = dataFileServiceMock.Object;
configMock = jasmine.createSpyObj('configMock', ['getDomain']);
fileReaderMock = new Mock<FileReader>();
dockerfileServiceImpl = new DockerfileServiceImpl(dataFileService, configMock, () => fileReaderMock.Object);
});
describe("getDockerfile", () => {
var file: any;
var invalidArchiveFile: any[];
var validArchiveFile: any[];
var forDataSpy: Spy;
beforeEach(() => {
file = "FROM quay.io/coreos/nginx:latest";
validArchiveFile = [{name: 'Dockerfile', path: 'Dockerfile', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue(file)}];
invalidArchiveFile = [{name: 'main.exe', path: 'main.exe', toBlob: jasmine.createSpy('toBlobSpy').and.returnValue("")}];
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
failure([]);
});
dataFileServiceMock.setup(mock => mock.arrayToString).is((buf, callback) => callback(""));
dataFileServiceMock.setup(mock => mock.blobToString).is((blob, callback) => callback(blob.toString()));
forDataSpy = spyOn(DockerfileInfoImpl, "forData").and.returnValue(new DockerfileInfoImpl(file, configMock));
fileReaderMock.setup(mock => mock.readAsArrayBuffer).is((blob: Blob) => {
fileReaderMock.Object.onload(<any>{target: {result: file}});
});
});
it("calls datafile service to read given file as possible archive file", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect((<Spy>fileReaderMock.Object.readAsArrayBuffer).calls.argsFor(0)[0]).toEqual(file);
expect(dataFileService.readDataArrayAsPossibleArchive).toHaveBeenCalled();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("calls datafile service to convert file to string if given file is not an archive", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect((<Spy>dataFileService.arrayToString).calls.argsFor(0)[0]).toEqual(file);
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given non-archive file that is not a valid Dockerfile", (done) => {
forDataSpy.and.returnValue(null);
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail("Promise should be rejected");
done();
})
.catch((error: string) => {
expect(error).toEqual('File chosen is not a valid Dockerfile');
done();
});
});
it("returns resolved promise with new DockerfileInfoImpl instance if given valid Dockerfile", (done) => {
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(dockerfile).toBeDefined();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given archive file with no Dockerfile present in root directory", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(invalidArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail('Promise should be rejected');
done();
})
.catch((error: string) => {
expect(error).toEqual('No Dockerfile found in root of archive');
done();
});
});
it("calls datafile service to convert blob to string if given file is an archive", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(validArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(validArchiveFile[0].toBlob).toHaveBeenCalled();
expect((<Spy>dataFileService.blobToString).calls.argsFor(0)[0]).toEqual(validArchiveFile[0].toBlob());
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
it("returns rejected promise if given archive file with invalid Dockerfile", (done) => {
forDataSpy.and.returnValue(null);
invalidArchiveFile[0].name = 'Dockerfile';
invalidArchiveFile[0].path = 'Dockerfile';
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(invalidArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
fail('Promise should be rejected');
done();
})
.catch((error: string) => {
expect(error).toEqual('Dockerfile inside archive is not a valid Dockerfile');
done();
});
});
it("returns resolved promise of new DockerfileInfoImpl instance if given archive with valid Dockerfile", (done) => {
dataFileServiceMock.setup(mock => mock.readDataArrayAsPossibleArchive).is((buf, success, failure) => {
success(validArchiveFile);
});
dockerfileServiceImpl.getDockerfile(file)
.then((dockerfile: DockerfileInfoImpl) => {
expect(dockerfile).toBeDefined();
done();
})
.catch((error: string) => {
fail('Promise should be resolved');
done();
});
});
});
});
describe("DockerfileInfoImpl", () => {
var dockerfileInfoImpl: DockerfileInfoImpl;
var contents: string;
var configMock: any;
beforeEach(() => {
contents = "";
configMock = jasmine.createSpyObj('configMock', ['getDomain']);
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
});
describe("forData", () => {
it("returns null if given contents do not contain a 'FROM' command", () => {
expect(DockerfileInfoImpl.forData(contents, configMock)).toBe(null);
});
it("returns a new DockerfileInfoImpl instance if given contents are valid", () => {
contents = "FROM quay.io/coreos/nginx";
expect(DockerfileInfoImpl.forData(contents, configMock) instanceof DockerfileInfoImpl).toBe(true);
});
});
describe("getRegistryBaseImage", () => {
var domain: string;
var baseImage: string;
beforeEach(() => {
domain = "quay.io";
baseImage = "coreos/nginx";
configMock.getDomain.and.returnValue(domain);
});
it("returns null if instance's contents do not contain a 'FROM' command", () => {
var getBaseImageSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(null);
expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null);
expect(getBaseImageSpy).toHaveBeenCalled();
});
it("returns null if the domain of the instance's config does not match that of the base image", () => {
configMock.getDomain.and.returnValue(domain);
spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue('host.com');
expect(dockerfileInfoImpl.getRegistryBaseImage()).toBe(null);
expect(configMock.getDomain).toHaveBeenCalled();
});
it("returns the registry base image", () => {
spyOn(dockerfileInfoImpl, "getBaseImage").and.returnValue(`${domain}/${baseImage}`);
expect(dockerfileInfoImpl.getRegistryBaseImage()).toEqual(baseImage);
});
});
describe("getBaseImage", () => {
var host: string;
var port: number;
var tag: string;
var image: string;
beforeEach(() => {
host = 'quay.io';
port = 80;
tag = 'latest';
image = 'coreos/nginx';
});
it("returns null if instance's contents do not contain a 'FROM' command", () => {
var getBaseImageAndTagSpy: Spy = spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(null);
expect(dockerfileInfoImpl.getBaseImage()).toBe(null);
expect(getBaseImageAndTagSpy).toHaveBeenCalled();
});
it("returns the image name if in the format 'someimage'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(image);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(image);
});
it("returns the image name if in the format 'someimage:tag'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${image}:${tag}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(image);
});
it("returns the host, port, and image name if in the format 'host:port/someimage'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`);
});
it("returns the host, port, and image name if in the format 'host:port/someimage:tag'", () => {
spyOn(dockerfileInfoImpl, "getBaseImageAndTag").and.returnValue(`${host}:${port}/${image}:${tag}`);
expect(dockerfileInfoImpl.getBaseImage()).toEqual(`${host}:${port}/${image}`);
});
});
describe("getBaseImageAndTag", () => {
it("returns null if instance's contents do not contain a 'FROM' command", () => {
expect(dockerfileInfoImpl.getBaseImageAndTag()).toBe(null);
});
it("returns a string containing the base image and tag from the instance's contents", () => {
contents = "FROM quay.io/coreos/nginx";
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag();
expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.length).trim());
});
it("handles the presence of newlines", () => {
contents = "FROM quay.io/coreos/nginx\nRUN echo $0";
dockerfileInfoImpl = new DockerfileInfoImpl(contents, configMock);
var baseImageAndTag: string = dockerfileInfoImpl.getBaseImageAndTag();
expect(baseImageAndTag).toEqual(contents.substring('FROM '.length, contents.indexOf('\n')).trim());
});
});
});

View file

@ -0,0 +1,155 @@
import { DockerfileService, DockerfileInfo } from './dockerfile.service';
import { Injectable, Inject } from 'ng-metadata/core';
import { DataFileService } from '../datafile/datafile.service';
@Injectable(DockerfileService.name)
export class DockerfileServiceImpl implements DockerfileService {
constructor(@Inject(DataFileService.name) private DataFileService: DataFileService,
@Inject('Config') private Config: any,
@Inject('fileReaderFactory') private fileReaderFactory: () => FileReader) {
}
public getDockerfile(file: any): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
var reader: FileReader = this.fileReaderFactory();
reader.onload = (event: any) => {
this.DataFileService.readDataArrayAsPossibleArchive(event.target.result,
(files: any[]) => {
if (files.length > 0) {
this.processFiles(files)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
}
// Not an archive. Read directly as a single file.
else {
this.processFile(event.target.result)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
}
},
() => {
// Not an archive. Read directly as a single file.
this.processFile(event.target.result)
.then((dockerfileInfo: DockerfileInfoImpl) => resolve(dockerfileInfo))
.catch((error: string) => reject(error));
});
};
reader.onerror = (event: any) => reject(event);
reader.readAsArrayBuffer(file);
});
}
private processFile(dataArray: any): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
this.DataFileService.arrayToString(dataArray, (contents: string) => {
var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config);
if (result == null) {
reject('File chosen is not a valid Dockerfile');
}
else {
resolve(result);
}
});
});
}
private processFiles(files: any[]): Promise<DockerfileInfoImpl | string> {
return new Promise((resolve, reject) => {
var found: boolean = false;
files.forEach((file) => {
if (file['path'] == 'Dockerfile' || file['path'] == '/Dockerfile') {
this.DataFileService.blobToString(file.toBlob(), (contents: string) => {
var result: DockerfileInfoImpl | null = DockerfileInfoImpl.forData(contents, this.Config);
if (result == null) {
reject('Dockerfile inside archive is not a valid Dockerfile');
}
else {
resolve(result);
}
});
found = true;
}
});
if (!found) {
reject('No Dockerfile found in root of archive');
}
});
}
}
export class DockerfileInfoImpl implements DockerfileInfo {
constructor(private contents: string, private config: any) {
}
public static forData(contents: string, config: any): DockerfileInfoImpl | null {
var dockerfileInfo: DockerfileInfoImpl = null;
if (contents.indexOf('FROM ') != -1) {
dockerfileInfo = new DockerfileInfoImpl(contents, config);
}
return dockerfileInfo;
}
public getRegistryBaseImage(): string | null {
var baseImage = this.getBaseImage();
if (!baseImage) {
return null;
}
if (baseImage.indexOf(`${this.config.getDomain()}/`) != 0) {
return null;
}
return baseImage.substring(<number>this.config.getDomain().length + 1);
}
public getBaseImage(): string | null {
const imageAndTag = this.getBaseImageAndTag();
if (!imageAndTag) {
return null;
}
// Note, we have to handle a few different cases here:
// 1) someimage
// 2) someimage:tag
// 3) host:port/someimage
// 4) host:port/someimage:tag
const lastIndex: number = imageAndTag.lastIndexOf(':');
if (lastIndex == -1) {
return imageAndTag;
}
// Otherwise, check if there is a / in the portion after the split point. If so,
// then the latter is part of the path (and not a tag).
const afterColon: string = imageAndTag.substring(lastIndex + 1);
if (afterColon.indexOf('/') != -1) {
return imageAndTag;
}
return imageAndTag.substring(0, lastIndex);
}
public getBaseImageAndTag(): string | null {
var baseImageAndTag: string = null;
const fromIndex: number = this.contents.indexOf('FROM ');
if (fromIndex != -1) {
var newlineIndex: number = this.contents.indexOf('\n', fromIndex);
if (newlineIndex == -1) {
newlineIndex = this.contents.length;
}
baseImageAndTag = this.contents.substring(fromIndex + 'FROM '.length, newlineIndex).trim();
}
return baseImageAndTag;
}
}

View file

@ -0,0 +1,38 @@
/**
* Service which provides helper methods for extracting information out from a Dockerfile
* or an archive containing a Dockerfile.
*/
export abstract class DockerfileService {
/**
* Retrieve Dockerfile from given file.
* @param file Dockerfile or archive file containing Dockerfile.
* @return promise Promise which resolves to new DockerfileInfo instance or rejects with error message.
*/
public abstract getDockerfile(file: any): Promise<DockerfileInfo | string>;
}
/**
* Model representing information about a specific Dockerfile.
*/
export abstract class DockerfileInfo {
/**
* Extract the registry base image from the Dockerfile contents.
* @return registryBaseImage The registry base image.
*/
public abstract getRegistryBaseImage(): string | null;
/**
* Extract the base image from the Dockerfile contents.
* @return baseImage The base image.
*/
public abstract getBaseImage(): string | null;
/**
* Extract the base image and tag from the Dockerfile contents.
* @return baseImageAndTag The base image and tag.
*/
public abstract getBaseImageAndTag(): string | null;
}

View file

@ -0,0 +1,60 @@
/**
* Helper service which fires off events when the document's visibility changes, as well as allowing
* other Angular code to query the state of the document's visibility directly.
*/
angular.module('quay').constant('CORE_EVENT', {
DOC_VISIBILITY_CHANGE: 'core.event.doc_visibility_change'
});
angular.module('quay').factory('DocumentVisibilityService', ['$rootScope', '$document', 'CORE_EVENT',
function($rootScope, $document, CORE_EVENT) {
var document = $document[0],
features,
detectedFeature;
function broadcastChangeEvent() {
$rootScope.$broadcast(CORE_EVENT.DOC_VISIBILITY_CHANGE,
document[detectedFeature.propertyName]);
}
features = {
standard: {
eventName: 'visibilitychange',
propertyName: 'hidden'
},
moz: {
eventName: 'mozvisibilitychange',
propertyName: 'mozHidden'
},
ms: {
eventName: 'msvisibilitychange',
propertyName: 'msHidden'
},
webkit: {
eventName: 'webkitvisibilitychange',
propertyName: 'webkitHidden'
}
};
Object.keys(features).some(function(feature) {
if (document[features[feature].propertyName] !== undefined) {
detectedFeature = features[feature];
return true;
}
});
if (detectedFeature) {
$document.on(detectedFeature.eventName, broadcastChangeEvent);
}
return {
/**
* Is the window currently hidden or not.
*/
isHidden: function() {
if (detectedFeature) {
return document[detectedFeature.propertyName];
}
}
};
}]);

View file

@ -0,0 +1,43 @@
/**
* Service which exposes the supported external logins.
*/
angular.module('quay').factory('ExternalLoginService', ['Features', 'Config', 'ApiService',
function(Features, Config, ApiService) {
var externalLoginService = {};
externalLoginService.EXTERNAL_LOGINS = window.__external_login || [];
externalLoginService.getLoginUrl = function(loginService, action, callback) {
var errorDisplay = ApiService.errorDisplay('Could not load external login service ' +
'information. Please contact your service ' +
'administrator.')
var params = {
'service_id': loginService['id']
};
var data = {
'kind': action
};
ApiService.retrieveExternalLoginAuthorizationUrl(data, params).then(function(resp) {
callback(resp['auth_url']);
}, errorDisplay);
};
externalLoginService.hasSingleSignin = function() {
return externalLoginService.EXTERNAL_LOGINS.length == 1 && !Features.DIRECT_LOGIN;
};
externalLoginService.getSingleSigninUrl = function(callback) {
if (!externalLoginService.hasSingleSignin()) {
return callback(null);
}
// If there is a single external login service and direct login is disabled,
// then redirect to the external login directly.
externalLoginService.getLoginUrl(externalLoginService.EXTERNAL_LOGINS[0], 'login', callback);
};
return externalLoginService;
}]);

View file

@ -0,0 +1,274 @@
/**
* Service which defines the various kinds of external notification and provides methods for
* easily looking up information about those kinds.
*/
angular.module('quay').factory('ExternalNotificationData', ['Config', 'Features','VulnerabilityService',
function(Config, Features, VulnerabilityService) {
var externalNotificationData = {};
var events = [
{
'id': 'repo_push',
'title': 'Push to Repository',
'icon': 'fa-upload'
}
];
if (Features.REPO_MIRROR) {
var repoMirrorEvents = [
{
'id': 'repo_mirror_sync_started',
'title': 'Repository Mirror Started',
'icon': 'fa-circle-o-notch'
},
{
'id': 'repo_mirror_sync_success',
'title': 'Repository Mirror Success',
'icon': 'fa-check-circle-o'
},
{
'id': 'repo_mirror_sync_failed',
'title': 'Repository Mirror Unsuccessful',
'icon': 'fa-times-circle-o'
}
];
for (var i = 0; i < repoMirrorEvents.length; ++i) {
events.push(repoMirrorEvents[i]);
}
}
if (Features.BUILD_SUPPORT) {
var buildEvents = [
{
'id': 'build_queued',
'title': 'Dockerfile Build Queued',
'icon': 'fa-tasks',
'fields': [
{
'name': 'ref-regex',
'type': 'regex',
'title': 'matching ref(s)',
'help_text': 'An optional regular expression for matching the git branch or tag ' +
'git ref. If left blank, the notification will fire for all builds.',
'optional': true,
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
}
]
},
{
'id': 'build_start',
'title': 'Dockerfile Build Started',
'icon': 'fa-circle-o-notch',
'fields': [
{
'name': 'ref-regex',
'type': 'regex',
'title': 'matching ref(s)',
'help_text': 'An optional regular expression for matching the git branch or tag ' +
'git ref. If left blank, the notification will fire for all builds.',
'optional': true,
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
}
]
},
{
'id': 'build_success',
'title': 'Dockerfile Build Successfully Completed',
'icon': 'fa-check-circle-o',
'fields': [
{
'name': 'ref-regex',
'type': 'regex',
'title': 'matching ref(s)',
'help_text': 'An optional regular expression for matching the git branch or tag ' +
'git ref. If left blank, the notification will fire for all builds.',
'optional': true,
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
}
]
},
{
'id': 'build_failure',
'title': 'Dockerfile Build Failed',
'icon': 'fa-times-circle-o',
'fields': [
{
'name': 'ref-regex',
'type': 'regex',
'title': 'matching ref(s)',
'help_text': 'An optional regular expression for matching the git branch or tag ' +
'git ref. If left blank, the notification will fire for all builds.',
'optional': true,
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
}
]
},
{
'id': 'build_cancelled',
'title': 'Docker Build Cancelled',
'icon': 'fa-minus-circle',
'fields': [
{
'name': 'ref-regex',
'type': 'regex',
'title': 'matching ref(s)',
'help_text': 'An optional regular expression for matching the git branch or tag ' +
'git ref. If left blank, the notification will fire for all builds.',
'optional': true,
'placeholder': '(refs/heads/somebranch)|(refs/tags/sometag)'
}
]
}];
for (var i = 0; i < buildEvents.length; ++i) {
events.push(buildEvents[i]);
}
}
if (Features.SECURITY_SCANNER) {
events.push({
'id': 'vulnerability_found',
'title': 'Package Vulnerability Found',
'icon': 'fa-bug',
'fields': [
{
'name': 'level',
'type': 'enum',
'title': 'minimum severity level',
'values': VulnerabilityService.LEVELS,
'help_text': 'A vulnerability must have a severity of the chosen level (or higher) ' +
'for this notification to fire. Defcon 1 is a special severity level ' +
'manually tagged by the ' + Config.REGISTRY_TITLE_SHORT + ' team for ' +
'above-critical issues',
}
]
});
}
var methods = [
{
'id': 'quay_notification',
'title': Config.REGISTRY_TITLE_SHORT + ' Notification',
'icon': 'quay-icon',
'fields': [
{
'name': 'target',
'type': 'entity',
'title': 'Recipient',
'help_text': 'The ' + Config.REGISTRY_TITLE_SHORT + ' user to notify'
}
]
},
{
'id': 'email',
'title': 'E-mail',
'icon': 'fa-envelope',
'fields': [
{
'name': 'email',
'type': 'email',
'title': 'E-mail address'
}
],
'enabled': Features.MAILING
},
{
'id': 'webhook',
'title': 'Webhook POST',
'icon': 'fa-link',
'fields': [
{
'name': 'url',
'type': 'url',
'title': 'Webhook URL'
}
]
},
{
'id': 'flowdock',
'title': 'Flowdock Team Notification',
'icon': 'flowdock-icon',
'fields': [
{
'name': 'flow_api_token',
'type': 'string',
'title': 'Flow API Token',
'help_url': 'https://www.flowdock.com/account/tokens'
}
]
},
{
'id': 'hipchat',
'title': 'HipChat Room Notification',
'icon': 'hipchat-icon',
'fields': [
{
'name': 'room_id',
'type': 'pattern',
'title': 'Room ID #',
'pattern': '^[0-9]+$',
'help_url': 'https://hipchat.com/admin/rooms',
'pattern_fail_message': 'We require the HipChat room <b>number</b>, not name.'
},
{
'name': 'notification_token',
'type': 'string',
'title': 'Room Notification Token',
'help_url': 'https://hipchat.com/rooms/tokens/{room_id}'
}
]
},
{
'id': 'slack',
'title': 'Slack Room Notification',
'icon': 'slack-icon',
'fields': [
{
'name': 'url',
'type': 'pattern',
'title': 'Webhook URL',
'pattern': '^https://hooks\\.slack\\.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+$',
'help_url': 'https://slack.com/services/new/incoming-webhook',
'placeholder': 'https://hooks.slack.com/service/{some}/{token}/{here}'
}
]
}
];
var methodMap = {};
var eventMap = {};
for (var i = 0; i < methods.length; ++i) {
methodMap[methods[i].id] = methods[i];
}
for (var i = 0; i < events.length; ++i) {
eventMap[events[i].id] = events[i];
}
externalNotificationData.getSupportedEvents = function() {
return events;
};
externalNotificationData.getSupportedMethods = function() {
var filtered = [];
for (var i = 0; i < methods.length; ++i) {
if (methods[i].enabled !== false) {
filtered.push(methods[i]);
}
}
return filtered;
};
externalNotificationData.getEventInfo = function(event) {
return eventMap[event];
};
externalNotificationData.getMethodInfo = function(method) {
return methodMap[method];
};
return externalNotificationData;
}]);

View file

@ -0,0 +1,81 @@
/**
* Feature flags.
*/
angular.module('quay').factory('Features', [function() {
if (!window.__features) {
return {};
}
var features = window.__features;
features.getFeature = function(name, opt_defaultValue) {
var value = features[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
features.hasFeature = function(name) {
return !!features.getFeature(name);
};
features.matchesFeatures = function(list) {
for (var i = 0; i < list.length; ++i) {
var value = features.getFeature(list[i]);
if (!value) {
return false;
}
}
return true;
};
return features;
}]);
/**
* Application configuration.
*/
angular.module('quay').factory('Config', ['Features', function(Features) {
if (!window.__config) {
return {};
}
var config = window.__config;
config.getDomain = function() {
return config['SERVER_HOSTNAME'];
};
config.getHost = function(opt_auth) {
var auth = opt_auth;
if (auth) {
auth = auth + '@';
}
return config['PREFERRED_URL_SCHEME'] + '://' + auth + config['SERVER_HOSTNAME'];
};
config.getHttp = function() {
return config['PREFERRED_URL_SCHEME'];
};
config.getUrl = function(opt_path) {
var path = opt_path || '';
return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path;
};
config.getValue = function(name, opt_defaultValue) {
var value = config[name];
if (value == null) {
return opt_defaultValue;
}
return value;
};
config.getEnterpriseLogo = function(opt_defaultValue) {
return config.BRANDING && config.BRANDING.logo
? config.BRANDING.logo
: opt_defaultValue
};
return config;
}]);

View file

@ -0,0 +1,110 @@
/**
* Helper service for tracking images needed by tags and caching them.
*/
angular.module('quay').factory('ImageLoaderService', ['ApiService', function(ApiService) {
var imageLoader = function(namespace, name) {
this.namespace = namespace;
this.name = name;
this.tagCache = {};
this.images = [];
this.imageMap = {};
this.imageTagMap = {};
};
imageLoader.prototype.getTagSpecificImages = function(tag, callback) {
var errorDisplay = ApiService.errorDisplay('Could not load tag specific images', function() {
callback([]);
});
var params = {
'repository': this.namespace + '/' + this.name,
'tag': tag,
'owned': true
};
ApiService.listTagImages(null, params).then(function(resp) {
callback(resp['images']);
}, errorDisplay);
};
imageLoader.prototype.getTagsForImage = function(image) {
return this.imageTagMap[image.id] || [];
};
imageLoader.prototype.registerTagImages_ = function(tag, images) {
this.tagCache[tag] = images;
if (!images.length) {
return;
}
var that = this;
images.forEach(function(image) {
if (!that.imageMap[image.id]) {
that.imageMap[image.id] = image;
that.images.push(image);
}
});
var rootImage = images[0];
if (!this.imageTagMap[rootImage.id]) {
this.imageTagMap[rootImage.id] = [];
}
this.imageTagMap[rootImage.id].push(tag);
}
imageLoader.prototype.loadImages = function(tags, callback) {
var toLoad = [];
var that = this;
tags.forEach(function(tag) {
if (that.tagCache[tag]) {
return;
}
toLoad.push(tag);
});
if (!toLoad.length) {
callback();
return;
}
var loadImages = function(index) {
if (index >= toLoad.length) {
callback();
return;
}
var tag = toLoad[index];
var params = {
'repository': that.namespace + '/' + that.name,
'tag': tag,
};
ApiService.listTagImages(null, params).then(function(resp) {
that.registerTagImages_(tag, resp['images']);
loadImages(index + 1);
}, function() {
loadImages(index + 1);
})
};
loadImages(0);
};
imageLoader.prototype.reset = function() {
this.tagCache = {};
this.images = [];
this.imageMap = {};
this.imageTagMap = {};
};
var imageLoaderService = {};
imageLoaderService.getLoader = function(namespace, name) {
return new imageLoader(namespace, name);
};
return imageLoaderService
}]);

View file

@ -0,0 +1,31 @@
/**
* Helper service for returning information extracted from repository image metadata.
*/
angular.module('quay').factory('ImageMetadataService', [function() {
var metadataService = {};
metadataService.getImageCommand = function(image, imageId) {
if (!image) {
return null;
}
if (!image.__imageMap) {
image.__imageMap = {};
image.__imageMap[image.id] = image;
for (var i = 0; i < image.history.length; ++i) {
var cimage = image.history[i];
image.__imageMap[cimage.id] = cimage;
}
}
var found = image.__imageMap[imageId];
if (!found) {
return null;
}
return found.command;
};
return metadataService;
}]);

View file

@ -0,0 +1,42 @@
/**
* Service which provides access to the various keys defined in configuration, and working with
* external services that rely on those keys.
*/
angular.module('quay').factory('KeyService', ['$location', 'Config', function($location, Config) {
var keyService = {}
var oauth = window.__oauth;
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['gitlabTriggerClientId'] = oauth['GITLAB_TRIGGER_CONFIG']['CLIENT_ID'];
keyService['githubTriggerClientId'] = oauth['GITHUB_TRIGGER_CONFIG']['CLIENT_ID'];
keyService['gitlabRedirectUri'] = Config.getUrl('/oauth2/gitlab/callback');
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['githubTriggerEndpoint'] = oauth['GITHUB_TRIGGER_CONFIG']['GITHUB_ENDPOINT'];
keyService['githubTriggerAuthorizeUrl'] = oauth['GITHUB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService['gitlabTriggerEndpoint'] = oauth['GITLAB_TRIGGER_CONFIG']['GITLAB_ENDPOINT'];
keyService['gitlabTriggerAuthorizeUrl'] = oauth['GITLAB_TRIGGER_CONFIG']['AUTHORIZE_ENDPOINT'];
keyService.getConfiguration = function(parent, key) {
return oauth[parent][key];
};
keyService.isEnterprise = function(service) {
switch (service) {
case 'github':
var loginUrl = oauth['GITHUB_LOGIN_CONFIG']['AUTHORIZE_ENDPOINT'];
return loginUrl.indexOf('https://github.com/') < 0;
case 'github-trigger':
return keyService['githubTriggerAuthorizeUrl'].indexOf('https://github.com/') < 0;
}
return false;
};
return keyService;
}]);

View file

@ -0,0 +1,48 @@
/**
* Service which helps set the contents of the <meta> tags (and the <title> of a page).
*/
angular.module('quay').factory('MetaService', ['$interpolate', '$timeout', function($interpolate, $timeout) {
var metaService = {};
var interpolate = function(page, expr) {
if (!expr) {
return null;
}
var inter = $interpolate(expr, true, null, true);
if (!inter) {
return expr.toString();
}
return inter(page.scope);
};
var interpolationPromise = function(page, fieldGetter) {
return new Promise(function(resolve, reject) {
if (!page || !page.$$route) {
resolve(null);
return;
}
if (page.scope) {
resolve(interpolate(page, fieldGetter()));
return;
}
// Timeout needed because page.scope is initially undefined.
$timeout(function() {
resolve(interpolationPromise(page, fieldGetter));
}, 10);
});
};
metaService.getTitle = function(page) {
return interpolationPromise(page, () => page.$$route.title);
};
metaService.getDescription = function(page) {
return interpolationPromise(page, () => page.$$route.description);
};
return metaService;
}]);

View file

@ -0,0 +1,381 @@
/**
* Service which defines the supported kinds of application notifications (those items that appear
* in the sidebar) and provides helper methods for working with them.
*/
angular.module('quay').factory('NotificationService',
['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'CookieService', 'Features', 'Config', '$location', 'VulnerabilityService', 'UtilService',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, CookieService, Features, Config, $location, VulnerabilityService, UtilService) {
var notificationService = {
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': [],
'expiringAppTokens': [],
'additionalNotifications': false
};
var pollTimerHandle = null;
var notificationKinds = {
'test_notification': {
'level': 'primary',
'message': 'This notification is a long message for testing: {obj}',
'page': '/about/',
'dismissable': true
},
'org_team_invite': {
'level': 'primary',
'message': '{inviter} is inviting you to join team {team} under organization {org}',
'actions': [
{
'title': 'Join team',
'kind': 'primary',
'handler': function(notification) {
window.location = '/confirminvite?code=' + notification.metadata['code'];
}
},
{
'title': 'Decline',
'kind': 'default',
'handler': function(notification) {
ApiService.declineOrganizationTeamInvite(null, {'code': notification.metadata['code']}).then(function() {
notificationService.update();
});
}
}
]
},
'password_required': {
'level': 'error',
'message': 'In order to begin pushing and pulling repositories, a password must be set for your account',
'page': function(metadata) {
return '/user/' + UserService.currentUser()['username'] + '?tab=settings';
}
},
'over_private_usage': {
'level': 'error',
'message': 'Namespace {namespace} is over its allowed private repository count. ' +
'<br><br>Please upgrade your plan to avoid disruptions in service.',
'page': function(metadata) {
var organization = UserService.getOrganization(metadata['namespace']);
if (organization) {
return '/organization/' + metadata['namespace'] + '?tab=billing';
} else {
return '/user/' + metadata['namespace'] + '?tab=billing';
}
}
},
'maintenance': {
'level': 'warning',
'message': 'We will be down for schedule maintenance from {from_date} to {to_date} ' +
'for {reason}. We are sorry about any inconvenience.',
'page': 'http://status.quay.io/'
},
'repo_push': {
'level': 'info',
'message': function(metadata) {
if (metadata.updated_tags && Object.getOwnPropertyNames(metadata.updated_tags).length) {
return 'Repository {repository} has been pushed with the following tags updated: {updated_tags}';
} else {
return 'Repository {repository} has been pushed';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'repo_mirror_sync_started': {
'level': 'info',
'message': function(metadata) {
if (metadata.message && Object.getOwnPropertyNames(metadata.message)) {
return 'Repository Mirror started for {message}';
} else {
return 'Repository Mirror started for {repository}';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'repo_mirror_sync_success': {
'level': 'info',
'message': function(metadata) {
if (metadata.message && Object.getOwnPropertyNames(metadata.message)) {
return 'Repository Mirror successful for {message}';
} else {
return 'Repository Mirror successful for {repository}';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'repo_mirror_sync_failed': {
'level': 'info',
'message': function(metadata) {
if (metadata.message && Object.getOwnPropertyNames(metadata.message)) {
return 'Repository Mirror unsuccessful for {message}';
} else {
return 'Repository Mirror unsuccessful for {repository}';
}
},
'page': function(metadata) {
return '/repository/' + metadata.repository;
},
'dismissable': true
},
'build_queued': {
'level': 'info',
'message': 'A build has been queued for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build/' + metadata.build_id;
},
'dismissable': true
},
'build_start': {
'level': 'info',
'message': 'A build has been started for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build/' + metadata.build_id;
},
'dismissable': true
},
'build_success': {
'level': 'info',
'message': 'A build has succeeded for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build/' + metadata.build_id;
},
'dismissable': true
},
'build_failure': {
'level': 'error',
'message': 'A build has failed for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build/' + metadata.build_id;
},
'dismissable': true
},
'build_cancelled': {
'level': 'info',
'message': 'A build was cancelled for repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '/build/' + metadata.build_id;
},
'dismissable': true
},
'vulnerability_found': {
'level': function(metadata) {
var priority = metadata['vulnerability']['priority'];
return VulnerabilityService.LEVELS[priority].level;
},
'message': 'A {vulnerability.priority} vulnerability was detected in repository {repository}',
'page': function(metadata) {
return '/repository/' + metadata.repository + '?tab=tags';
},
'dismissable': true
},
'service_key_submitted': {
'level': 'primary',
'message': 'Service key {kid} for service {service} requests approval<br><br>Key was created on {created_date}',
'actions': [
{
'title': 'Approve Key',
'kind': 'primary',
'handler': function(notification) {
var params = {
'kid': notification.metadata.kid
};
ApiService.approveServiceKey({}, params).then(function(resp) {
notificationService.update();
window.location = '/superuser/?tab=servicekeys';
}, ApiService.errorDisplay('Could not approve service key'));
}
},
{
'title': 'Delete Key',
'kind': 'default',
'handler': function(notification) {
var params = {
'kid': notification.metadata.kid
};
ApiService.deleteServiceKey(null, params).then(function(resp) {
notificationService.update();
}, ApiService.errorDisplay('Could not delete service key'));
}
}
],
'page': function(metadata) {
return '/superuser/?tab=servicekeys';
},
}
};
notificationService.dismissNotification = function(notification) {
notification.dismissed = true;
var params = {
'uuid': notification.id
};
ApiService.updateUserNotification(notification, params).then(function(resp) {
var index = $.inArray(notification, notificationService.notifications);
if (index >= 0) {
notificationService.notifications.splice(index, 1);
}
notificationService.update();
}, ApiService.errorDisplay('Could not update notification'));
};
notificationService.getActions = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return [];
}
return kindInfo['actions'] || [];
};
notificationService.canDismiss = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return false;
}
return !!kindInfo['dismissable'];
};
notificationService.getPage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return null;
}
var page = kindInfo['page'];
if (page != null && typeof page != 'string') {
page = page(notification['metadata']);
}
return page || '';
};
notificationService.getMessage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return '(Unknown notification kind: ' + notification['kind'] + ')';
}
return StringBuilderService.buildTrustedString(kindInfo['message'], notification['metadata']);
};
notificationService.getBrowserNotificationMessage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return '(Unknown notification kind: ' + notification['kind'] + ')';
}
const unsafeHtml = StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
return UtilService.removeHtmlTags(unsafeHtml);
}
notificationService.getClass = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
if (!kindInfo) {
return 'notification-info';
}
var level = kindInfo['level'];
if (level != null && typeof level != 'string') {
level = level(notification['metadata']);
}
return 'notification-' + level;
};
notificationService.getClasses = function(notifications) {
if (!notifications.length) {
return '';
}
var classes = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
classes.push(notificationService.getClass(notification));
}
return classes.join(' ');
};
notificationService.update = function() {
var user = UserService.currentUser();
if (!user || user.anonymous) {
return;
}
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.additionalNotifications = resp['additional'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
if (notificationService.notifications.length > 0 && CookieService.get('quay.enabledDesktopNotifications') === 'on') {
notificationService.sendBrowserNotifications();
}
});
if (Features.APP_SPECIFIC_TOKENS) {
var params = {
'expiring': true
};
ApiService.listAppTokens(null, params).then(function(resp) {
notificationService.expiringAppTokens = resp['tokens'];
});
}
};
notificationService.sendBrowserNotifications = () => {
let mostRecentTimestamp = parseInt(CookieService.get('quay.notifications.mostRecentTimestamp'), 10);
if (!mostRecentTimestamp) {
mostRecentTimestamp = new Date(notificationService.notifications[0].created).getTime();
}
const newNotifications = notificationService.notifications
.filter(obj => new Date(obj.created).getTime() > mostRecentTimestamp);
if (newNotifications.length > 0) {
let message = 'You have unread notifications';
if (newNotifications.length === 1) {
message = notificationService.getBrowserNotificationMessage(newNotifications[0]);
}
new Notification(message, {
// Chrome doesn't display SVGs for notifications, so we'll use a default if we don't have an enterprise logo
icon: window.location.origin + Config.getEnterpriseLogo('/static/img/quay-logo.png'),
image: window.location.origin + Config.getEnterpriseLogo('/static/img/quay-logo.png'),
});
const newTimestamp = new Date(newNotifications[0].created).getTime();
CookieService.putPermanent('quay.notifications.mostRecentTimestamp', newTimestamp.toString());
}
};
notificationService.reset = function() {
$interval.cancel(pollTimerHandle);
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
};
// Watch for plan changes and update.
PlanService.registerListener(this, function(plan) {
notificationService.reset();
notificationService.update();
});
// Watch for user changes and update.
$rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
notificationService.reset();
notificationService.update();
});
return notificationService;
}]);

View file

@ -0,0 +1,8 @@
/**
* Service which provides the OAuth scopes defined.
*/
angular.module('quay').factory('OAuthService', ['$location', 'Config', function($location, Config) {
var oauthService = {};
oauthService.SCOPES = window.__auth_scopes;
return oauthService;
}]);

View file

@ -0,0 +1,22 @@
import { PageServiceImpl } from './page.service.impl';
describe("Service: PageServiceImpl", () => {
var pageServiceImpl: PageServiceImpl;
beforeEach(() => {
pageServiceImpl = new PageServiceImpl();
});
describe("create", () => {
// TODO
});
describe("get", () => {
// TODO
});
describe("$get", () => {
// TODO
});
});

View file

@ -0,0 +1,41 @@
import { Injectable } from 'ng-metadata/core';
import { PageService, QuayPage, QuayPageProfile } from './page.service';
@Injectable(PageService.name)
export class PageServiceImpl implements ng.IServiceProvider {
private pages: {[pageName: string]: QuayPage} = {};
public create(pageName: string,
templateName: string,
controller?: any,
flags: any = {},
profiles: string[] = ['old-layout', 'layout']): void {
for (var i = 0; i < profiles.length; ++i) {
this.pages[profiles[i] + ':' + pageName] = {
'name': pageName,
'controller': controller,
'templateName': templateName,
'flags': flags
};
}
}
public get(pageName: string, profiles: QuayPageProfile[]): [QuayPageProfile, QuayPage] | null {
for (let i = 0; i < profiles.length; ++i) {
var current = profiles[i];
var key = current.id + ':' + pageName;
var page = this.pages[key];
if (page) {
return [current, page];
}
}
return null;
}
public $get(): PageService {
return this;
}
}

View file

@ -0,0 +1,53 @@
/**
* Manages the creation and retrieval of pages (route + controller)
*/
export abstract class PageService implements ng.IServiceProvider {
/**
* Create a page.
* @param pageName The name of the page.
* @param templateName The file name of the template.
* @param controller Controller for the page.
* @param flags Additional flags passed to route provider.
* @param profiles Available profiles.
*/
public abstract create(pageName: string,
templateName: string,
controller?: any,
flags?: any,
profiles?: string[]): void;
/**
* Retrieve a registered page.
* @param pageName The name of the page.
* @param profiles Available profiles to search.
*/
public abstract get(pageName: string, profiles: QuayPageProfile[]): [QuayPageProfile, QuayPage] | null;
/**
* Provide the service instance.
* @return pageService The singleton service instance.
*/
public abstract $get(): PageService;
}
/**
* A type representing a registered application page.
*/
export type QuayPage = {
name: string;
controller: ng.IController;
templateName: string,
flags: {[key: string]: any};
};
/**
* Represents a page profile type.
*/
export type QuayPageProfile = {
id: string;
templatePath: string;
};

View file

@ -0,0 +1,380 @@
/**
* Helper service for loading, changing and working with subscription plans.
*/
angular.module('quay')
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', '$location',
function(KeyService, UserService, CookieService, ApiService, Features, Config, $location) {
var plans = null;
var planDict = {};
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback});
};
planService.unregisterListener = function(obj) {
for (var i = 0; i < listeners.length; ++i) {
if (listeners[i].obj == obj) {
listeners.splice(i, 1);
break;
}
}
};
planService.notePlan = function(planId) {
if (Features.BILLING) {
CookieService.putSession('quay.notedplan', planId);
}
};
planService.isOrgCompatible = function(plan) {
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId || !Features.BILLING) { return false; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
return;
}
planService.getPlan(planId, function(plan) {
if (planService.isOrgCompatible(plan)) {
$location.path('/organizations/new').search('plan', planId);
} else {
$location.path('/user').search('plan', planId);
}
});
});
return true;
};
planService.getAndResetNotedPlan = function() {
var planId = CookieService.get('quay.notedplan');
CookieService.clear('quay.notedplan');
return planId;
};
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) {
if (!Features.BILLING) { return; }
if (plans && plans.length) {
callback(plans);
return;
}
ApiService.listPlans().then(function(data) {
plans = data.plans || [];
for(var i = 0; i < plans.length; i++) {
planDict[plans[i].stripeId] = plans[i];
}
callback(plans);
}, function() { callback([]); });
};
planService.getPlans = function(callback, opt_includePersonal) {
planService.verifyLoaded(function(plans) {
var filtered = [];
for (var i = 0; i < plans.length; ++i) {
var plan = plans[i];
if (plan['deprecated']) { continue; }
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
filtered.push(plan);
}
callback(filtered);
});
};
planService.getPlan = function(planId, callback) {
planService.getPlanIncludingDeprecated(planId, function(plan) {
if (!plan['deprecated']) {
callback(plan);
}
});
};
planService.getPlanIncludingDeprecated = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
planService.getPlanImmediately = function(planId) {
// Get the plan by name, without bothering to check if the plans are loaded.
// This method will return undefined if planId is undefined or null, or if
// the planDict has not yet been loaded.
return planDict[planId];
};
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.getPlans(function(plans) {
for (var i = 0; i < plans.length; i++) {
var plan = plans[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
}, /* include personal */!isBusiness);
};
planService.getSubscription = function(orgname, success, failure) {
if (!Features.BILLING) { return; }
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
if (!Features.BILLING) { return; }
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
if (!Features.BILLING) { return; }
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
callback({'is_valid': false});
});
};
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
callbacks['started']();
}
planService.getSubscription(orgname, function(sub) {
planService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
planService.changePlanInternal($scope, orgname, planId, callbacks, opt_async,
subscribedPlan.price > 0);
});
}, function() {
planService.changePlanInternal($scope, orgname, planId, callbacks, opt_async, false);
});
};
planService.changePlanInternal = function($scope, orgname, planId, callbacks, opt_async,
opt_reuseCard) {
if (!Features.BILLING) { return; }
planService.getPlan(planId, function(plan) {
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4 || !opt_reuseCard)) {
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
});
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (!Features.BILLING) { return; }
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
var cardInfo = {
'token': token.id
};
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
};
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
email: email,
currency: 'usd',
name: 'Update credit card',
description: 'Enter your credit card number',
panelLabel: 'Update',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
billingAddress: true,
zipCode: true,
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
};
planService.getEmail = function(orgname) {
var email = null;
if (UserService.currentUser()) {
email = UserService.currentUser().email;
if (orgname) {
org = UserService.getOrganization(orgname);
if (org) {
emaiil = org.email;
}
}
}
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
if (!Features.BILLING) { return; }
// If the async parameter is true and this is a browser that does not allow async popup of the
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
if (opt_async && (isIE || isMobileSafari)) {
bootbox.dialog({
"message": "Please click 'Subscribe' to continue",
"buttons": {
"subscribe": {
"label": "Subscribe",
"className": "btn-primary",
"callback": function() {
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
}
},
"close": {
"label": "Cancel",
"className": "btn-default"
}
}
});
return;
}
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
if (Config.MIXPANEL_KEY) {
mixpanel.track('plan_subscribe');
}
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
});
};
planService.getPlan(planId, function(planDetails) {
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: opt_title || 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
billingAddress: true,
zipCode: true,
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
});
};
return planService;
}]);

View file

@ -0,0 +1,100 @@
/**
* Service which defines the various role groups.
*/
angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', 'UserService',
function(UtilService, Restangular, ApiService, UserService) {
var roleService = {};
roleService.repoRolesOrNone = [
{ 'id': 'none', 'title': 'None', 'kind': 'default', 'description': 'No permissions on the repository' },
{ 'id': 'read', 'title': 'Read', 'kind': 'success', 'description': 'Can view and pull from the repository' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success', 'description': 'Can view, pull and push to the repository' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access, pull and push on the repository' }
];
roleService.repoRoles = roleService.repoRolesOrNone.slice(1);
roleService.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default', 'description': 'Inherits all permissions of the team' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success', 'description': 'Member and can create new repositories' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' }
];
var getPermissionEndpoint = function(repository, entityName, entityKind) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var namespace = repository.namespace;
var name = repository.name;
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', entityKind, entityName);
return Restangular.one(url.toString());
};
roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
var endpoint = getPermissionEndpoint(repository, entityName, entityKind);
endpoint.customDELETE().then(function() {
callback(true);
}, errorDisplay);
};
roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) {
if (role == 'none') {
roleService.deleteRepositoryRole(repository, entityKind, entityName, callback);
return;
}
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false);
});
var permission = {
'role': role
};
var endpoint = getPermissionEndpoint(repository, entityName, entityKind);
endpoint.customPUT(permission).then(function(resp) {
callback(true, resp);
}, errorDisplay);
};
roleService.getRepoPermissions = function(namespace, entityKind, entityName, callback) {
var errorHandler = ApiService.errorDisplay('Could not load permissions', callback);
if (entityKind == 'team') {
var params = {
'orgname': namespace,
'teamname': entityName
};
ApiService.getOrganizationTeamPermissions(null, params).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else if (entityKind == 'robot') {
var parts = entityName.split('+');
var shortName = parts[1];
var orgname = UserService.isOrganization(namespace) ? namespace : null;
ApiService.getRobotPermissions(orgname, null, {'robot_shortname': shortName}).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else {
throw Error('Unknown entity kind ' + entityKind);
}
};
return roleService;
}]);

View file

@ -0,0 +1,53 @@
import { RouteBuilder } from './route-builder.service';
import { Injectable, Inject } from 'ng-metadata/core';
import { PageService, QuayPage, QuayPageProfile } from '../page/page.service';
@Injectable(RouteBuilder.name)
export class RouteBuilderImpl implements RouteBuilder {
public currentProfile: string = 'layout';
public profiles: QuayPageProfile[] = [
// Start with the old pages (if we asked for it).
{id: 'old-layout', templatePath: '/static/partials/'},
// Fallback back combined new/existing pages.
{id: 'layout', templatePath: '/static/partials/'}
];
constructor(@Inject('routeProvider') private routeProvider: ng.route.IRouteProvider,
@Inject('pages') private pages: PageService) {
for (let i = 0; i < this.profiles.length; ++i) {
if (this.profiles[i].id == this.currentProfile) {
this.profiles = this.profiles.slice(i);
break;
}
}
}
public otherwise(options: any): void {
this.routeProvider.otherwise(options);
}
public route(path: string, pagename: string): RouteBuilder {
// Lookup the page, matching our lists of profiles.
var pair = this.pages.get(pagename, this.profiles);
if (!pair) {
throw Error('Unknown page: ' + pagename);
}
// Create the route.
var foundProfile = pair[0];
var page = pair[1];
var templateUrl = foundProfile.templatePath + page.templateName;
var options = page.flags || {};
options['templateUrl'] = templateUrl;
options['reloadOnSearch'] = false;
options['controller'] = page.controller;
this.routeProvider.when(path, options);
return this;
}
}

View file

@ -0,0 +1,118 @@
import { RouteBuilderImpl } from './route-builder.service.impl';
import { PageService } from '../page/page.service';
describe("Service: RouteBuilderImpl", () => {
var routeProviderMock: any;
var pagesMock: any;
var profiles: any[];
beforeEach((() => {
profiles = [
{id: 'old-layout', templatePath: '/static/partials/'},
{id: 'layout', templatePath: '/static/partials/'}
];
routeProviderMock = jasmine.createSpyObj('routeProvider', ['otherwise', 'when']);
pagesMock = jasmine.createSpyObj('pagesMock', ['get', 'create']);
}));
describe("constructor", () => {
it("returns a RouteBuilder object", () => {
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
expect(routeBuilder).toBeDefined();
});
it("initializes current profile to 'layout'", () => {
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
expect(routeBuilder.currentProfile).toEqual('layout');
});
it("initializes available profiles", () => {
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
var matchingRoutes: any[] = routeBuilder.profiles.filter((profile) => {
return profiles.indexOf(profile) == -1;
});
expect(matchingRoutes).toEqual(routeBuilder.profiles);
});
it("sets 'profiles' to the first given profile with id matching given current profile", () => {
var routeBuilder: RouteBuilderImpl = new RouteBuilderImpl(routeProviderMock, pagesMock);
expect(routeBuilder.profiles).toEqual([profiles[1]]);
});
});
describe("otherwise", () => {
var routeBuilder: RouteBuilderImpl;
beforeEach(() => {
routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock);
});
it("calls routeProvider to set fallback route with given options", () => {
var options = {1: "option"};
routeBuilder.otherwise(options);
expect(routeProviderMock.otherwise.calls.argsFor(0)[0]).toEqual(options);
});
});
describe("route", () => {
var routeBuilder: RouteBuilderImpl;
var path: string;
var pagename: string;
var page: any;
beforeEach(() => {
path = '/repository/:namespace/:name';
pagename = 'repo-view';
page = {
templateName: 'repository.html',
reloadOnSearch: false,
controller: jasmine.createSpy('pageController'),
flags: {},
};
routeBuilder = new RouteBuilderImpl(routeProviderMock, pagesMock);
});
it("calls pages with given pagename and 'profiles' to get matching page and profile pair", () => {
pagesMock.get.and.returnValue([profiles[1], page]);
routeBuilder.route(path, pagename);
expect(pagesMock.get.calls.argsFor(0)[0]).toEqual(pagename);
expect(pagesMock.get.calls.argsFor(0)[1]).toEqual(routeBuilder.profiles);
});
it("throws error if no matching page/profile pair found", () => {
pagesMock.get.and.returnValue();
try {
routeBuilder.route(path, pagename);
fail();
} catch (error) {
expect(error.message).toEqual('Unknown page: ' + pagename);
}
});
it("calls routeProvider to set route for given path and options", () => {
pagesMock.get.and.returnValue([profiles[1], page]);
var expectedOptions: any = {
templateUrl: profiles[1].templatePath + page.templateName,
reloadOnSearch: false,
controller: page.controller,
};
routeBuilder.route(path, pagename);
expect(routeProviderMock.when.calls.argsFor(0)[0]).toEqual(path);
expect(routeProviderMock.when.calls.argsFor(0)[1]).toEqual(expectedOptions);
});
it("returns itself (the RouteBuilder instance)", () => {
pagesMock.get.and.returnValue([profiles[1], page]);
expect(routeBuilder.route(path, pagename)).toEqual(routeBuilder);
});
});
});

View file

@ -0,0 +1,18 @@
/**
* Constructs client-side routes.
*/
export abstract class RouteBuilder {
/**
* Configure the redirect route.
* @param options Configuration options.
*/
public abstract otherwise(options: any): void;
/**
* Register a route.
* @param path The URL of the route.
* @param pagename The name of the page to associate with this route.
*/
public abstract route(path: string, pagename: string): RouteBuilder;
}

View file

@ -0,0 +1,39 @@
/**
* Service which monitors the current state of the registry.
*/
angular.module('quay')
.factory('StateService', ['$rootScope', '$timeout', function($rootScope, $timeout) {
var stateService = {};
var currentState = {
'inReadOnlyMode': false
};
stateService.inReadOnlyMode = function() {
return currentState.inReadOnlyMode;
};
stateService.setInReadOnlyMode = function() {
currentState.inReadOnlyMode = true;
};
stateService.updateStateIn = function(scope, opt_callback) {
scope.$watch(function () { return stateService.currentState(); }, function (currentState) {
$timeout(function(){
scope.currentRegistryState = currentState;
if (opt_callback) {
opt_callback(currentState);
}
}, 0, false);
}, true);
};
stateService.currentState = function() {
return currentState;
};
// Update the state in the root scope.
stateService.updateStateIn($rootScope);
return stateService;
}]);

View file

@ -0,0 +1,42 @@
/**
* Helper service for retrieving the statuspage status of the quay service.
*/
angular.module('quay').factory('StatusService', ['Features', function(Features) {
if (!Features.BILLING) {
return {
getStatus: function(callback) {}
};
}
var STATUSPAGE_PAGE_ID = '8szqd6w4s277';
var STATUSPAGE_SRC = 'https://statuspage-production.s3.amazonaws.com/se-v2.js';
var statusPageHandler = null;
var statusPageData = null;
var callbacks = [];
var handleGotData = function(data) {
if (!data) { return; }
statusPageData = data;
for (var i = 0; i < callbacks.length; ++i) {
callbacks[i](data);
}
callbacks = [];
};
$.getScript(STATUSPAGE_SRC, function(){
statusPageHandler = new StatusPage.page({ page: STATUSPAGE_PAGE_ID });
statusPageHandler.summary({
success : handleGotData
});
});
var statusService = {};
statusService.getStatus = function(callback) {
callbacks.push(callback);
handleGotData(statusPageData);
};
return statusService;
}]);

View file

@ -0,0 +1,172 @@
/**
* Service for building strings, with wildcards replaced with metadata.
*/
angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
var stringBuilderService = {};
var fieldIcons = {
'inviter': 'user',
'username': 'user',
'user': 'user',
'email': 'envelope',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'ci-robot',
'tag': 'tag',
'tags': 'tag',
'role': 'th-large',
'original_role': 'th-large',
'application_name': 'cloud',
'image': 'archive',
'original_image': 'archive',
'client_id': 'chain',
'manifest_digest': 'link'
};
var allowMarkdown = {
'description': true,
};
var filters = {
'obj': function(value) {
if (!value) { return []; }
return Object.getOwnPropertyNames(value);
},
'updated_tags': function(value) {
if (!value) { return []; }
return value.join(', ');
},
'kid': function(kid, metadata) {
if (metadata.name) {
return metadata.name;
}
return metadata.kid.substr(0, 12);
},
'created_date': function(value) {
return moment.unix(value).format('LLL');
},
'expiration_date': function(value) {
return moment.unix(value).format('LLL');
},
'old_expiration_date': function(value) {
return moment.unix(value).format('LLL');
}
};
stringBuilderService.buildUrl = function(value_or_func, metadata) {
var url = value_or_func;
if (typeof url != 'string') {
url = url(metadata);
}
// Find the variables to be replaced.
var varNames = [];
for (var i = 0; i < url.length; ++i) {
var c = url[i];
if (c == '{') {
for (var j = i + 1; j < url.length; ++j) {
var d = url[j];
if (d == '}') {
varNames.push(url.substring(i + 1, j));
i = j;
break;
}
}
}
}
// Replace all variables found.
for (var i = 0; i < varNames.length; ++i) {
var varName = varNames[i];
if (!metadata[varName]) {
return null;
}
url = url.replace('{' + varName + '}', metadata[varName]);
}
return url;
};
stringBuilderService.buildTrustedString = function(value_or_func, metadata, opt_codetag) {
return $sce.trustAsHtml(stringBuilderService.buildString(value_or_func, metadata, opt_codetag));
};
stringBuilderService.replaceField = function(description, prefix, key, value, opt_codetag) {
if (Array.isArray(value)) {
value = value.join(', ');
} else if (typeof value == 'object') {
for (var subkey in value) {
if (value.hasOwnProperty(subkey)) {
description = stringBuilderService.replaceField(description, prefix + key + '.',
subkey, value[subkey], opt_codetag)
}
}
return description
}
var safe = UtilService.textToSafeHtml(value.toString());
var safeHtml = safe;
if (allowMarkdown[key]) {
safeHtml = UtilService.getMarkedDown(safeHtml);
safeHtml = safeHtml.substr('<p>'.length, safeHtml.length - '<p></p>'.length);
}
var icon = fieldIcons[key];
if (icon) {
if (icon.indexOf('ci-') < 0) {
icon = 'fa-' + icon;
}
safeHtml = `<i class="fa ${icon}"></i>${safeHtml}`;
}
var codeTag = opt_codetag || 'code';
var tagKey = prefix + key;
description = description.replace(`{${tagKey}}`,
`<${codeTag} class="tag-${tagKey}" title="${safe}">${safeHtml}</${codeTag}>`);
return description
}
stringBuilderService.buildString = function(value_or_func, metadata, opt_codetag, opt_summarize) {
var description = value_or_func;
if (typeof description != 'string') {
description = description(metadata);
}
if (opt_summarize) {
// Remove any summary text.
description = description.replace(/\[\[([^\]])+\]\]/g, '');
} else {
// Remove summary text placeholders.
description = description.replace(/\[\[/g, '');
description = description.replace(/\]\]/g, '');
}
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
var value = metadata[key] != null ? metadata[key] : '(Unknown)';
if (filters[key]) {
value = filters[key](value, metadata);
}
description = stringBuilderService.replaceField(description, '', key, value, opt_codetag);
}
}
return description.replace(/(\r\n|\n|\r)/gm, '<br>');
};
return stringBuilderService;
}]);

View file

@ -0,0 +1,97 @@
/**
* Service which provides helper methods for constructing and managing tabular data.
*/
angular.module('quay').factory('TableService', ['ViewArray', function(ViewArray) {
var tableService = {};
tableService.tablePredicateClass = function(name, predicate, reverse) {
if (name != predicate) {
return '';
}
return 'current ' + (reverse ? 'reversed' : '');
};
tableService.orderBy = function(predicate, options) {
if (predicate == options.predicate) {
options.reverse = !options.reverse;
return;
}
options.reverse = false;
options.predicate = predicate;
};
tableService.getReversedTimestamp = function(datetime) {
if (!datetime) {
return -Number.MAX_VALUE;
}
return (new Date(datetime)).valueOf();
};
tableService.buildOrderedItems = function(items, options, filterFields, numericFields, opt_extrafilter) {
var orderedItems = ViewArray.create();
items.forEach(function(item) {
var filter = options.filter;
if (filter) {
var found = false;
for (var i = 0; i < filterFields.length; ++i) {
var filterField = filterFields[i];
if (item[filterField].indexOf(filter) >= 0) {
found = true;
break;
}
}
if (!found) {
return;
}
}
if (opt_extrafilter && !opt_extrafilter(item)) {
return;
}
orderedItems.push(item);
});
orderedItems.entries.sort(function(a, b) {
var left = a[options['predicate']];
var right = b[options['predicate']];
for (var i = 0; i < numericFields.length; ++i) {
var numericField = numericFields[i];
if (options['predicate'] == numericField) {
left = left * 1;
right = right * 1;
break;
}
}
if (left == null) {
left = '0.00';
}
if (right == null) {
right = '0.00';
}
if (left == right) {
return 0;
}
return left > right ? -1 : 1;
});
if (options['reverse']) {
orderedItems.entries.reverse();
}
orderedItems.setVisible(true);
return orderedItems;
};
return tableService;
}]);

View file

@ -0,0 +1,276 @@
/**
* Helper service for defining the various kinds of build triggers and retrieving information
* about them.
*/
angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'KeyService', 'Features', 'CookieService', 'Config',
function(UtilService, $sanitize, KeyService, Features, CookieService, Config) {
var triggerService = {};
var branch_tag = {
'title': 'Branch/Tag',
'type': 'autocomplete',
'name': 'refs',
'iconMap': {
'branch': 'fa-code-fork',
'tag': 'fa-tag'
}
};
var triggerTypes = {
'github': {
'run_parameters': [branch_tag],
'get_redirect_url': function(namespace, repository) {
var redirect_uri = KeyService['githubRedirectUri'] + '/trigger/' +
namespace + '/' + repository;
var client_id = KeyService['githubTriggerClientId'];
var authorize_url = new UtilService.UrlBuilder(KeyService['githubTriggerAuthorizeUrl']);
authorize_url.setQueryParameter('client_id', client_id);
authorize_url.setQueryParameter('scope', 'repo,user:email');
authorize_url.setQueryParameter('redirect_uri', redirect_uri);
return authorize_url.toString();
},
'is_external': true,
'is_enabled': function() {
return Features.GITHUB_BUILD;
},
'icon': 'fa-github',
'title': function() {
var isEnterprise = KeyService.isEnterprise('github-trigger');
if (isEnterprise) {
return 'GitHub Enterprise Repository Push';
}
return 'GitHub Repository Push';
},
'supports_full_directory_listing': true,
'templates': {
'credentials': '/static/directives/trigger/githost/credentials.html',
'trigger-description': '/static/directives/trigger/github/trigger-description.html'
},
'link_templates': {
'commit': '/commit/{sha}',
'branch': '/tree/{branch}',
'tag': '/releases/tag/{tag}',
}
},
'bitbucket': {
'run_parameters': [branch_tag],
'get_redirect_url': function(namespace, repository) {
return Config.getUrl('/bitbucket/setup/' + namespace + '/' + repository);
},
'is_external': false,
'is_enabled': function() {
return Features.BITBUCKET_BUILD;
},
'icon': 'fa-bitbucket',
'title': function() { return 'Bitbucket Repository Push'; },
'supports_full_directory_listing': false,
'templates': {
'credentials': '/static/directives/trigger/githost/credentials.html',
'trigger-description': '/static/directives/trigger/bitbucket/trigger-description.html'
},
'link_templates': {
'commit': '/commits/{sha}',
'branch': '/branch/{branch}',
'tag': '/commits/tag/{tag}',
}
},
'gitlab': {
'run_parameters': [branch_tag],
'get_redirect_url': function(namespace, repository) {
var redirect_uri = KeyService['gitlabRedirectUri'] + '/trigger';
var client_id = KeyService['gitlabTriggerClientId'];
var authorize_url = new UtilService.UrlBuilder(KeyService['gitlabTriggerAuthorizeUrl']);
authorize_url.setQueryParameter('client_id', client_id);
authorize_url.setQueryParameter('state', 'repo:' + namespace + '/' + repository);
authorize_url.setQueryParameter('redirect_uri', redirect_uri);
authorize_url.setQueryParameter('response_type', 'code');
return authorize_url.toString();
},
'is_external': false,
'is_enabled': function() {
return Features.GITLAB_BUILD;
},
'icon': 'fa-gitlab',
'title': function() { return 'GitLab Repository Push'; },
'supports_full_directory_listing': false,
'templates': {
'credentials': '/static/directives/trigger/githost/credentials.html',
'trigger-description': '/static/directives/trigger/gitlab/trigger-description.html'
},
'link_templates': {
'commit': '/commit/{sha}',
'branch': '/tree/{branch}',
'tag': '/commits/{tag}',
}
},
'custom-git': {
'run_parameters': [
{
'title': 'Commit',
'type': 'regex',
'name': 'commit_sha',
'regex': '^([A-Fa-f0-9]{7,})$',
'placeholder': '1c002dd'
}
],
'get_redirect_url': function(namespace, repository) {
return Config.getUrl('/customtrigger/setup/' + namespace + '/' + repository);
},
'is_external': false,
'is_enabled': function() { return true; },
'icon': 'fa-git-square',
'title': function() { return 'Custom Git Repository Push'; },
'templates': {
'credentials': '/static/directives/trigger/custom-git/credentials.html',
'trigger-description': '/static/directives/trigger/custom-git/trigger-description.html'
}
}
};
triggerService.populateTemplate = function(scope, name) {
scope.$watch('trigger', function(trigger) {
if (!trigger) { return; }
scope.triggerTemplate = triggerService.getTemplate(trigger.service, name);
});
};
triggerService.getCommitUrl = function(build) {
// Check for a predefined URL first.
if (build.trigger_metadata && build.trigger_metadata.commit_info &&
build.trigger_metadata.commit_info.url) {
return build.trigger_metadata.commit_info.url;
}
return triggerService.getFullLinkTemplate(build, 'commit')
.replace('{sha}', triggerService.getCommitSHA(build.trigger_metadata))
};
triggerService.getFullLinkTemplate = function(build, templateName) {
if (!build.trigger) {
return null;
}
var type = triggerTypes[build.trigger.service];
if (!type) {
return null;
}
var repositoryUrl = build.trigger.repository_url;
if (!repositoryUrl) {
return null;
}
var linkTemplate = type.link_templates;
if (!linkTemplate || !linkTemplate[templateName]) {
return null;
}
return repositoryUrl + linkTemplate[templateName];
};
triggerService.supportsFullListing = function(name) {
var type = triggerTypes[name];
if (!type) {
return false;
}
return !!type['supports_full_directory_listing'];
};
triggerService.getTypes = function() {
var types = [];
for (var key in triggerTypes) {
if (!triggerTypes.hasOwnProperty(key)) {
continue;
}
types.push(key);
}
return types;
};
triggerService.getTemplate = function(name, template) {
var type = triggerTypes[name];
if (!type) {
return '';
}
return type['templates'][template];
};
triggerService.getRedirectUrl = function(name, namespace, repository) {
var type = triggerTypes[name];
if (!type) {
return '';
}
return type['get_redirect_url'](namespace, repository);
};
triggerService.getDockerfileLocation = function(trigger) {
var subdirectory = trigger.config.subdir;
if (!subdirectory) {
return '//Dockerfile';
}
return '//' + subdirectory.replace(new RegExp('(^\/+|\/+$)'), '') + 'Dockerfile';
};
triggerService.isEnabled = function(name) {
var type = triggerTypes[name];
if (!type) {
return false;
}
return type['is_enabled']();
};
triggerService.getIcon = function(name) {
var type = triggerTypes[name];
if (!type) {
return 'Unknown';
}
return type['icon'];
};
triggerService.getTitle = function(name) {
var type = triggerTypes[name];
if (!type) {
return 'Unknown';
}
return type['title']();
};
triggerService.getDescription = function(name, config) {
var icon = triggerService.getIcon(name);
var title = triggerService.getTitle(name);
var source = '';
if (config && config['build_source']) {
source = UtilService.textToSafeHtml(config['build_source']);
}
var desc = '<i class"fa ' + icon + ' fa-lg" style="margin-left:2px; margin-right: 2px"></i> Push to ' + title + ' ' + source;
return desc;
};
triggerService.getCommitSHA = function(metadata) {
return metadata.commit || metadata.commit_sha;
};
triggerService.getMetadata = function(name) {
return triggerTypes[name];
};
triggerService.getRunParameters = function(name, config) {
var type = triggerTypes[name];
if (!type) {
return [];
}
return type['run_parameters'];
}
return triggerService;
}]);

View file

@ -0,0 +1,370 @@
/**
* Service which provides helper methods for performing some simple UI operations.
*/
angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', 'ApiService', function($timeout, $rootScope, $location, ApiService) {
var CheckStateController = function(items, itemKey) {
this.items = items;
this.checked = [];
this.allItems_ = items;
this.allCheckedMap_ = {};
this.itemKey_ = itemKey;
this.listeners_ = [];
this.page_ = null;
this.lastChanged_ = null;
this.ApiService = ApiService
};
CheckStateController.prototype.listen = function(callback) {
this.listeners_.push(callback);
};
CheckStateController.prototype.isChecked = function(item) {
return !!this.allCheckedMap_[item[this.itemKey_]];
};
CheckStateController.prototype.toggleItem = function(item, opt_shift) {
if (opt_shift && this.lastChanged_) {
var itemIndex = $.inArray(item, this.items);
var lastIndex = $.inArray(this.lastChanged_, this.items);
if (itemIndex >= 0 && lastIndex >= 0) {
var startIndex = Math.min(itemIndex, lastIndex);
var endIndex = Math.max(itemIndex, lastIndex);
var shouldCheck = this.isChecked(this.lastChanged_);
for (var i = startIndex; i <= endIndex; ++i) {
if (shouldCheck) {
this.checkItem(this.items[i]);
} else {
this.uncheckItem(this.items[i]);
}
}
return;
}
}
if (this.isChecked(item)) {
this.uncheckItem(item);
} else {
this.checkItem(item);
}
};
CheckStateController.prototype.toggleItems = function(opt_filter) {
this.lastChanged_= null;
this.updateMap_(this.checked, false);
if (this.checked.length) {
this.checked = [];
} else {
if (opt_filter) {
this.checked = this.items.filter((item) => (opt_filter.indexOf(item) >= 0));
} else {
this.checked = this.items.slice();
}
}
this.updateMap_(this.checked, true);
this.callListeners_();
};
CheckStateController.prototype.setPage = function(page, pageSize) {
this.items = this.allItems_.slice(page * pageSize, (page + 1) * pageSize);
this.rebuildCheckedList_();
};
CheckStateController.prototype.setChecked = function(items) {
this.allCheckedMap_ = {};
this.updateMap_(items, true);
this.rebuildCheckedList_();
};
CheckStateController.prototype.rebuildCheckedList_ = function() {
var that = this;
this.checked = [];
this.allItems_.forEach(function(item) {
if (that.allCheckedMap_[item[that.itemKey_]]) {
that.checked.push(item);
}
});
this.callListeners_();
};
CheckStateController.prototype.updateMap_ = function(items, is_checked) {
var that = this;
items.forEach(function(item) {
if (item == null) { return; }
that.allCheckedMap_[item[that.itemKey_]] = is_checked;
});
};
CheckStateController.prototype.checkByFilter = function(filter, opt_secondaryFilter) {
this.updateMap_(this.checked, false);
var filterFunc = filter;
if (opt_secondaryFilter) {
filterFunc = (item) => (opt_secondaryFilter.indexOf(item) >= 0 && filter(item));
}
this.checked = $.grep(this.items, filterFunc);
this.updateMap_(this.checked, true);
this.callListeners_();
};
CheckStateController.prototype.checkItem = function(item) {
if (this.isChecked(item)) {
return;
}
this.lastChanged_ = item;
this.checked.push(item);
this.allCheckedMap_[item[this.itemKey_]] = true;
this.callListeners_();
};
CheckStateController.prototype.uncheckItem = function(item) {
if (!this.isChecked(item)) {
return;
}
this.lastChanged_ = item;
this.checked = $.grep(this.checked, function(cItem) {
return cItem != item;
});
this.allCheckedMap_[item[this.itemKey_]] = false;
this.callListeners_();
};
CheckStateController.prototype.callListeners_ = function() {
var that = this;
var allCheckedMap = this.allCheckedMap_;
var allChecked = [];
this.allItems_.forEach(function(item) {
var key = item[that.itemKey_];
if (!!allCheckedMap[key]) {
allChecked.push(item);
}
});
this.listeners_.map(function(listener) {
listener(allChecked, that.checked);
});
};
//////////////////////////////////////////////////////////////////////////////////////
var uiService = {};
uiService.hidePopover = function(elem) {
var popover = $(elem).data('bs.popover');
if (popover) {
popover.hide();
}
};
uiService.showPopover = function(elem, content, opt_placement) {
var popover = $(elem).data('bs.popover');
if (!popover) {
$(elem).popover({'content': '-', 'placement': opt_placement || 'left'});
}
setTimeout(function() {
var popover = $(elem).data('bs.popover');
popover.options.content = content;
popover.show();
}, 500);
};
uiService.showFormError = function(elem, result, opt_placement) {
var message = ApiService.getErrorMessage(result, 'error');
if (message) {
uiService.showPopover(elem, message, opt_placement || 'bottom');
} else {
uiService.hidePopover(elem);
}
};
uiService.createCheckStateController = function(items, opt_checked) {
return new CheckStateController(items, opt_checked);
};
uiService.showPasswordDialog = function(message, callback, opt_canceledCallback) {
var success = function() {
var password = $('#passDialogBox').val();
$('#passDialogBox').val('');
callback(password);
};
var canceled = function() {
$('#passDialogBox').val('');
opt_canceledCallback && opt_canceledCallback();
};
var box = bootbox.dialog({
"message": message +
'<form style="margin-top: 10px" action="javascript:$(\'.btn-continue\').click();void(0)">' +
'<input id="passDialogBox" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"title": 'Please Verify',
"buttons": {
"verify": {
"label": "Verify",
"className": "btn-success btn-continue",
"callback": success
},
"close": {
"label": "Cancel",
"className": "btn-default",
"callback": canceled
}
}
});
box.bind('shown.bs.modal', function(){
box.find("input").focus();
box.find("form").submit(function() {
if (!$('#passDialogBox').val()) { return; }
box.modal('hide');
success();
});
});
};
uiService.clickElement = function(el) {
// From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements
var ev = document.createEvent("MouseEvent");
ev.initMouseEvent(
"click",
true /* bubble */, true /* cancelable */,
window, null,
0, 0, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null);
el.dispatchEvent(ev);
};
uiService.initializeTabs = function(scope, element, opt_clickCallback, opt_rememberCookie) {
var locationListener = null;
var disposed = false;
var changeTab = function(activeTab) {
if (disposed) { return; }
$('a[data-toggle="tab"]').each(function(index) {
var tabName = this.getAttribute('data-target').substr(1);
if (tabName != activeTab) {
return;
}
if ($(this).parent().hasClass('active')) {
return;
}
if (this.clientWidth == 0) {
setTimeout(function() {
changeTab(activeTab);
}, 100);
return;
}
var elem = this;
setTimeout(function() {
uiService.clickElement(elem);
}, 0);
});
};
var resetDefaultTab = function() {
if (disposed) { return; }
$timeout(function() {
element.find('a[data-toggle="tab"]').each(function(index) {
if (index == 0) {
var elem = this;
setTimeout(function() {
uiService.clickElement(elem);
}, 0);
}
});
});
};
var checkTabs = function() {
if (disposed) { return; }
// Poll until we find the tabs.
var tabs = element.find('a[data-toggle="tab"]');
if (tabs.length == 0) {
$timeout(checkTabs, 50);
return;
}
// Register listeners.
registerListeners(tabs);
// Set the active tab (if any).
var activeTab = $location.search()['tab'];
if (activeTab) {
changeTab(activeTab);
}
};
var registerListeners = function(tabs) {
// Listen for scope destruction.
scope.$on('$destroy', function() {
disposed = true;
locationListener && locationListener();
});
// Listen for route changes and update the tabs accordingly.
if (!opt_rememberCookie) {
locationListener = $rootScope.$on('$routeUpdate', function(){
if ($location.search()['tab']) {
changeTab($location.search()['tab']);
} else {
resetDefaultTab();
}
});
}
// Listen for tab changes.
tabs.on('shown.bs.tab', function (e) {
// Invoke the callback, if any.
opt_clickCallback && opt_clickCallback();
// Update the search location or cookie.
if (opt_rememberCookie) {
// TODO: this
} else {
var tabName = e.target.getAttribute('data-target').substr(1);
$rootScope.$apply(function() {
var isDefaultTab = tabs[0] == e.target;
var newSearch = $.extend($location.search(), {});
if (isDefaultTab) {
delete newSearch['tab'];
} else {
newSearch['tab'] = tabName;
}
$location.search(newSearch);
});
}
e.preventDefault();
});
};
// Start the checkTabs timer.
checkTabs();
};
return uiService;
}]);

View file

@ -0,0 +1,217 @@
import * as Raven from 'raven-js';
/**
* Service which monitors the current user session and provides methods for returning information
* about the user.
*/
angular.module('quay')
.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config', '$location', '$timeout',
function(ApiService, CookieService, $rootScope, Config, $location, $timeout) {
var userResponse = {
verified: false,
anonymous: true,
username: null,
email: null,
organizations: [],
logins: [],
beforeload: true
};
var userService = {};
var _INTERNAL_AUTH_SERVICES = ['ldap', 'jwtauthn', 'keystone'];
userService.hasEverLoggedIn = function() {
return CookieService.get('quay.loggedin') == 'true';
};
userService.updateUserIn = function(scope, opt_callback) {
scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
if (currentUser) {
$timeout(function(){
scope.user = currentUser;
if (opt_callback) {
opt_callback(currentUser);
}
}, 0, false);
};
}, true);
};
userService.load = function(opt_callback) {
var handleUserResponse = function(loadedUser) {
userResponse = loadedUser;
if (!userResponse.anonymous) {
if (Config.MIXPANEL_KEY) {
try {
mixpanel.identify(userResponse.username);
mixpanel.people.set({
'$email': userResponse.email,
'$username': userResponse.username,
'verified': userResponse.verified
});
mixpanel.people.set_once({
'$created': new Date()
})
} catch (e) {
window.console.log(e);
}
}
if (Config.MARKETO_MUNCHKIN_ID && userResponse['marketo_user_hash']) {
var associateLeadBody = {'Email': userResponse.email};
if (window.Munchkin !== undefined) {
try {
Munchkin.munchkinFunction(
'associateLead',
associateLeadBody,
userResponse['marketo_user_hash']
);
} catch (e) {
}
} else {
window.__quay_munchkin_queue.push([
'associateLead',
associateLeadBody,
userResponse['marketo_user_hash']
]);
}
}
if (window.Raven !== undefined) {
try {
Raven.setUser({
email: userResponse.email,
id: userResponse.username
});
} catch (e) {
window.console.log(e);
}
}
CookieService.putPermanent('quay.loggedin', 'true');
} else {
if (window.Raven !== undefined) {
Raven.setUser();
}
}
// If the loaded user has a prompt, redirect them to the update page.
if (loadedUser.prompts && loadedUser.prompts.length) {
$location.path('/updateuser');
return;
}
if (opt_callback) {
opt_callback(loadedUser);
}
};
ApiService.getLoggedInUser().then(function(loadedUser) {
handleUserResponse(loadedUser);
}, function() {
handleUserResponse({'anonymous': true});
});
};
userService.isOrganization = function(name) {
return !!userService.getOrganization(name);
};
userService.getOrganization = function(name) {
if (!userResponse || !userResponse.organizations) { return null; }
for (var i = 0; i < userResponse.organizations.length; ++i) {
var org = userResponse.organizations[i];
if (org.name == name) {
return org;
}
}
return null;
};
userService.isNamespaceAdmin = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
if (!org) {
return false;
}
return org.is_org_admin;
};
userService.isKnownNamespace = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
return !!org;
};
userService.getNamespace = function(namespace) {
var org = userService.getOrganization(namespace);
if (org) {
return org;
}
if (namespace == userResponse.username) {
return userResponse;
}
return null;
};
userService.getCLIUsername = function() {
if (!userResponse) {
return null;
}
var externalUsername = null;
userResponse.logins.forEach(function(login) {
if (_INTERNAL_AUTH_SERVICES.indexOf(login.service) >= 0) {
externalUsername = login.service_identifier;
}
});
return externalUsername || userResponse.username;
};
userService.deleteNamespace = function(info, callback) {
var namespace = info.user ? info.user.username : info.organization.name;
if (!namespace) {
return;
}
var errorDisplay = ApiService.errorDisplay('Could not delete namespace', callback);
var cb = function(resp) {
userService.load(function(currentUser) {
callback(true);
$location.path('/');
});
}
if (info.user) {
ApiService.deleteCurrentUser().then(cb, errorDisplay)
} else {
var delParams = {
'orgname': info.organization.name
};
ApiService.deleteAdminedOrganization(null, delParams).then(cb, errorDisplay);
}
};
userService.currentUser = function() {
return userResponse;
};
// Update the user in the root scope.
userService.updateUserIn($rootScope);
return userService;
}]);

View file

@ -0,0 +1,149 @@
var urlParseURL = require('url-parse');
var UrlBuilder = function(initial_url) {
this.url = urlParseURL(initial_url || '', '/');
};
UrlBuilder.prototype.setQueryParameter = function(paramName, paramValue) {
if (paramValue == null) {
return;
}
this.url.query = this.url.query || {};
this.url.query[paramName] = paramValue;
};
UrlBuilder.prototype.toString = function() {
return this.url.toString();
};
/**
* Service which exposes various utility methods.
*/
angular.module('quay').factory('UtilService', ['$sanitize', 'markdownConverter',
function($sanitize, markdownConverter) {
var utilService = {};
var adBlockEnabled = null;
utilService.isAdBlockEnabled = function(callback) {
if (adBlockEnabled !== null) {
callback(adBlockEnabled);
return;
}
if(typeof blockAdBlock === 'undefined') {
callback(true);
return;
}
var bab = new BlockAdBlock({
checkOnLoad: false,
resetOnEnd: true
});
bab.onDetected(function() { adBlockEnabled = true; callback(true); });
bab.onNotDetected(function() { adBlockEnabled = false; callback(false); });
bab.check();
};
utilService.isEmailAddress = function(val) {
var emailRegex = /^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
return emailRegex.test(val);
};
utilService.getMarkedDown = function(string) {
return markdownConverter.makeHtml(string || '');
};
utilService.getFirstMarkdownLineAsText = function(commentString, placeholderNeeded) {
if (!commentString) {
if (placeholderNeeded) {
return '<p style="visibility:hidden">placeholder</p>';
}
return '';
}
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return utilService.getMarkedDown(lines[i]);
}
return '';
};
utilService.getFirstMarkdownLineAsString = function(commentString) {
return utilService.getFirstMarkdownLineAsText(commentString, false).replace('</p>', '')
.replace('<p>', '');
};
utilService.escapeHtmlString = function(text) {
var textStr = (text || '').toString();
var adjusted = textStr.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
return adjusted;
};
utilService.stringToHTML = function(text) {
text = utilService.escapeHtmlString(text);
text = text.replace(/\n/g, '<br>');
return text;
};
utilService.getRestUrl = function(args) {
var path = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
path += '/';
}
path += encodeURI(arguments[i])
}
return new UrlBuilder(path);
};
utilService.textToSafeHtml = function(text) {
return $sanitize(utilService.escapeHtmlString(text));
};
utilService.UrlBuilder = UrlBuilder;
utilService.removeHtmlTags = function(text){
try {
return new DOMParser().parseFromString(text, 'text/html').body.textContent || text;
} catch(e) {
return text;
}
};
return utilService;
}]);

View file

@ -0,0 +1,147 @@
import { ViewArrayImpl } from './view-array.impl';
describe("ViewArrayImplImpl", () => {
var viewArrayImpl: ViewArrayImpl;
var $intervalMock: any;
beforeEach(() => {
$intervalMock = jasmine.createSpy('$intervalSpy');
$intervalMock.and.returnValue({});
$intervalMock.cancel = jasmine.createSpy('cancelSpy');
viewArrayImpl = new ViewArrayImpl($intervalMock);
});
describe("constructor", () => {
it("initializes values", () => {
expect(viewArrayImpl.isVisible).toBe(false);
expect(viewArrayImpl.visibleEntries).toBe(null);
expect(viewArrayImpl.entries.length).toEqual(0);
expect(viewArrayImpl.hasEntries).toBe(false);
expect(viewArrayImpl.hasHiddenEntries).toBe(false);
});
});
describe("length", () => {
it("returns the number of entries", () => {
viewArrayImpl.entries = [{}, {}, {}];
expect(viewArrayImpl.length()).toEqual(viewArrayImpl.entries.length);
});
});
describe("get", () => {
it("returns the entry at a given index", () => {
var index: number = 8;
viewArrayImpl.entries = new Array(10);
viewArrayImpl.entries[index] = 3;
expect(viewArrayImpl.get(index)).toEqual(viewArrayImpl.entries[index]);
});
});
describe("push", () => {
it("adds given element to the end of entries", () => {
var element: number = 3;
var originalLength: number = viewArrayImpl.length();
viewArrayImpl.push(element);
expect(viewArrayImpl.entries.length).toEqual(originalLength + 1);
expect(viewArrayImpl.get(originalLength)).toEqual(element);
});
it("sets 'hasEntries' to true", () => {
viewArrayImpl.push(2);
expect(viewArrayImpl.hasEntries).toBe(true);
});
it("starts timer if 'isVisible' is true", () => {
viewArrayImpl.isVisible = true;
viewArrayImpl.push(2);
expect($intervalMock).toHaveBeenCalled();
});
it("does not start timer if 'isVisible' is false", () => {
viewArrayImpl.isVisible = false;
viewArrayImpl.push(2);
expect($intervalMock).not.toHaveBeenCalled();
});
});
describe("toggle", () => {
it("sets 'isVisible' to false if currently true", () => {
viewArrayImpl.isVisible = true;
viewArrayImpl.toggle();
expect(viewArrayImpl.isVisible).toBe(false);
});
it("sets 'isVisible' to true if currently false", () => {
viewArrayImpl.isVisible = false;
viewArrayImpl.toggle();
expect(viewArrayImpl.isVisible).toBe(true);
});
});
describe("setVisible", () => {
it("sets 'isVisible' to false if given false", () => {
viewArrayImpl.setVisible(false);
expect(viewArrayImpl.isVisible).toBe(false);
});
it("sets 'visibleEntries' to empty array if given false", () => {
viewArrayImpl.setVisible(false);
expect(viewArrayImpl.visibleEntries.length).toEqual(0);
});
it("shows additional entries if given true", () => {
viewArrayImpl.setVisible(true);
});
it("does not show additional entries if given false", () => {
viewArrayImpl.setVisible(false);
});
it("starts timer if given true", () => {
viewArrayImpl.setVisible(true);
expect($intervalMock).toHaveBeenCalled();
});
it("does not stop timer if given false and timer is not active", () => {
viewArrayImpl.setVisible(false);
expect($intervalMock.cancel).not.toHaveBeenCalled();
});
it("stops timer if given false and timer is active", () => {
viewArrayImpl.isVisible = true;
viewArrayImpl.push(2);
viewArrayImpl.setVisible(false);
expect($intervalMock.cancel).toHaveBeenCalled();
});
});
describe("create", () => {
it("returns a new ViewArrayImpl instance", () => {
var newViewArrayImpl: ViewArrayImpl = viewArrayImpl.create();
expect(newViewArrayImpl).toBeDefined();
});
});
});

View file

@ -0,0 +1,96 @@
import { ViewArray } from './view-array';
import { Injectable, Inject } from 'ng-metadata/core';
@Injectable(ViewArray.name)
export class ViewArrayImpl implements ViewArray {
public entries: any[];
public isVisible: boolean;
public visibleEntries: any[];
public hasEntries: boolean;
public hasHiddenEntries: boolean;
private timerRef: any;
private currentIndex: number;
private additionalCount: number = 20;
constructor(@Inject('$interval') private $interval: ng.IIntervalService) {
this.isVisible = false;
this.visibleEntries = null;
this.hasEntries = false;
this.entries = [];
this.hasHiddenEntries = false;
this.timerRef = null;
this.currentIndex = 0;
}
public length(): number {
return this.entries.length;
}
public get(index: number): any {
return this.entries[index];
}
public push(elem: any): void {
this.entries.push(elem);
this.hasEntries = true;
if (this.isVisible) {
this.startTimer();
}
}
public toggle(): void {
this.setVisible(!this.isVisible);
}
public setVisible(newState: boolean): void {
this.isVisible = newState;
this.visibleEntries = [];
this.currentIndex = 0;
if (newState) {
this.showAdditionalEntries();
this.startTimer();
}
else {
this.stopTimer();
}
}
public create(): ViewArrayImpl {
return new ViewArrayImpl(this.$interval);
}
private showAdditionalEntries(): void {
var i: number = 0;
for (i = this.currentIndex; i < (this.currentIndex + this.additionalCount) && i < this.entries.length; ++i) {
this.visibleEntries.push(this.entries[i]);
}
this.currentIndex = i;
this.hasHiddenEntries = this.currentIndex < this.entries.length;
if (this.currentIndex >= this.entries.length) {
this.stopTimer();
}
}
private startTimer(): void {
if (this.timerRef) {
return;
}
this.timerRef = this.$interval(() => {
this.showAdditionalEntries();
}, 10);
}
private stopTimer(): void {
if (this.timerRef) {
this.$interval.cancel(this.timerRef);
this.timerRef = null;
}
}
}

View file

@ -0,0 +1,71 @@
import { ViewArrayImpl } from "static/js/services/view-array/view-array.impl";
/**
* Specialized wrapper around array which provides a toggle() method for viewing the contents of the
* array in a manner that is asynchronously filled in over a short time period. This prevents long
* pauses in the UI for ngRepeat's when the array is significant in size.
*/
export abstract class ViewArray {
/**
* The stored entries.
*/
public abstract entries: any;
/**
* If the entries are displayed.
*/
public abstract isVisible: boolean;
/**
* The displayed entries.
*/
public abstract visibleEntries: any[];
/**
* If there are stored entries.
*/
public abstract hasEntries: boolean;
/**
* If there are entries not visible.
*/
public abstract hasHiddenEntries: boolean;
/**
* Get the number of entries stored.
* @return number The number of entries.
*/
public abstract length(): number;
/**
* Get a specific entry.
* @param index The index of the entry.
* @return element The element at the given index.
*/
public abstract get(index: number): any;
/**
* Add a new element.
* @param elem The new element.
*/
public abstract push(elem: any): void;
/**
* Toggle whether the elements are visible.
*/
public abstract toggle(): void;
/**
* Set whether the elements are visible.
* @param newState True/False if the contents are visible.
*/
public abstract setVisible(newState: boolean): void;
/**
* Factory function to create a new ViewArray.
* @return viewArray New ViewArray instance.
*/
public abstract create(): ViewArrayImpl;
}

View file

@ -0,0 +1,569 @@
/**
* Service which provides helper methods for working with the vulnerability system.
*/
angular.module('quay').factory('VulnerabilityService', ['Config', 'ApiService', 'ImageMetadataService',
function(Config, ApiService, ImageMetadataService) {
var vulnService = {};
vulnService.LEVELS = window.__vuln_priority;
vulnService.getUnadjustedScoreOf = function(vuln) {
var severity = vulnService.LEVELS[vuln['Severity']];
return severity.score;
};
vulnService.getCVSSScoreOf = function(vuln) {
if (vuln.Metadata && vuln.Metadata.NVD && vuln.Metadata.NVD.CVSSv2 && vuln.Metadata.NVD.CVSSv2.Score) {
return vuln.Metadata.NVD.CVSSv2.Score;
}
return null;
}
vulnService.buildVulnerabilitiesInfo = function(image, resp) {
var levels = vulnService.getLevels();
var severityCountMap = {};
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
});
var fixable = [];
var vulnerabilities = [];
var featuresInfo = vulnService.buildFeaturesInfo(image, resp);
featuresInfo.features.forEach(function(feature) {
if (feature.vulnerabilities) {
vulnerabilities = vulnerabilities.concat(feature.vulnerabilities);
fixable = fixable.concat(feature.fixable);
feature.severityBreakdown.forEach(function(level) {
severityCountMap[level['index']] += level['value'];
});
}
});
var severityBreakdown = [];
levels.forEach(function(level) {
if (severityCountMap[level['index']]) {
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
}
});
return {
'vulnerabilities': vulnerabilities,
'fixable': fixable,
'severityBreakdown': severityBreakdown,
'features': featuresInfo.features,
}
};
vulnService.buildVulnerabilitiesInfoForFeature = function(image, feature) {
var levels = vulnService.getLevels();
var vulnerabilities = [];
var fixable = [];
var severityCountMap = {};
var fixableCountMap = {};
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
fixableCountMap[level['index']] = 0;
});
var score = 0;
var fixableScore = 0;
var highestSeverityIndex = levels.length;
if (feature.Vulnerabilities) {
var addedByImageId = feature.AddedBy ? feature.AddedBy.split('.')[0] : null;
feature.Vulnerabilities.forEach(function(vuln) {
var severity = vulnService.LEVELS[vuln['Severity']];
var cvssScore = vulnService.getCVSSScoreOf(vuln);
var unadjustedScore = vulnService.getUnadjustedScoreOf(vuln);
var currentScore = unadjustedScore;
var scoreDivergence = null;
// If the vulnerability has a CVSS score, ensure it is within 2 levels of the severity
// score from the distro. If it is out of that band, then we have a score divergence
// and use the distro's score directly.
if (cvssScore != null) {
if (cvssScore - unadjustedScore > 2) {
scoreDivergence = 'adjusted-lower';
} else if (unadjustedScore > cvssScore) {
scoreDivergence = 'adjusted-higher';
} else {
currentScore = cvssScore;
}
}
var exponentialScore = Math.pow(2, currentScore) + 0.1;
var vuln_object = {
'score': exponentialScore,
'scoreDivergence': scoreDivergence,
'severityInfo': severity,
'cvssScore': cvssScore,
'cvssColor': vulnService.getCVSSColor(cvssScore),
'name': vuln.Name,
'namespace': vuln.NamespaceName || vuln.Namespace,
'description': vuln.Description,
'link': vuln.Link,
'severity': vuln.Severity,
'metadata': vuln.Metadata,
'featureName': feature.Name,
'fixedInVersion': vuln.FixedBy,
'introducedInVersion': feature.Version,
'imageId': addedByImageId,
'imageCommand': ImageMetadataService.getImageCommand(image, addedByImageId),
'expanded': false
};
// Save the highest vulnerability severity for this feature.
highestSeverityIndex = Math.min(severity['index'], highestSeverityIndex);
// Add the score and (if necessary) the fixable scores.
score += exponentialScore;
severityCountMap[severity['index']]++;
vulnerabilities.push(vuln_object);
if (vuln.FixedBy) {
fixableCountMap[severity['index']]++;
fixableScore += exponentialScore;
fixable.push(vuln_object)
}
});
}
// Calculate the breakdown of the vulnerabilities by severity.
var severityBreakdown = [];
var fixableBreakdown = [];
var leftoverBreakdown = [];
levels.forEach(function(level) {
if (severityCountMap[level['index']]) {
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
if (fixableCountMap[level['index']]) {
fixableBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': fixableCountMap[level['index']],
'color': level['color']
});
}
var leftoverCount = severityCountMap[level['index']] - fixableCountMap[level['index']];
if (leftoverCount) {
leftoverBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': leftoverCount,
'color': level['color']
});
}
}
});
return {
'vulnerabilities': vulnerabilities,
'fixable': fixable,
'severityBreakdown': severityBreakdown,
'fixableBreakdown': fixableBreakdown,
'leftoverBreakdown': leftoverBreakdown,
'score': score,
'fixableScore': fixableScore,
'highestSeverity': levels[highestSeverityIndex],
};
};
vulnService.buildFeaturesInfo = function(image, resp) {
var features = [];
var severityCountMap = {};
var highestFixableScore = 0;
var levels = vulnService.getLevels();
levels.forEach(function(level) {
severityCountMap[level['index']] = 0;
});
vulnService.forEachFeature(resp, function(feature) {
// Calculate the scores and breakdowns for all the vulnerabilities under feature.
var vulnerabilityInfo = vulnService.buildVulnerabilitiesInfoForFeature(image, feature);
var addedByImageId = feature.AddedBy ? feature.AddedBy.split('.')[0] : null;
var feature_obj = {
'name': feature.Name,
'namespace': feature.NamespaceName || feature.Namespace,
'version': feature.Version,
'addedBy': feature.AddedBy,
'imageId': addedByImageId,
'imageCommand': ImageMetadataService.getImageCommand(image, addedByImageId),
'vulnCount': vulnerabilityInfo.vulnerabilities.length,
'severityBreakdown': vulnerabilityInfo.severityBreakdown,
'fixableBreakdown': vulnerabilityInfo.fixableBreakdown,
'leftoverBreakdown': vulnerabilityInfo.leftoverBreakdown,
'score': vulnerabilityInfo.score,
'fixableCount': vulnerabilityInfo.fixable.length,
'leftoverCount': vulnerabilityInfo.vulnerabilities.length - vulnerabilityInfo.fixable.length,
'fixableScore': vulnerabilityInfo.fixableScore,
'leftoverScore': vulnerabilityInfo.score - vulnerabilityInfo.fixableScore,
'primarySeverity': vulnerabilityInfo.severityBreakdown[0],
'primaryLeftover': vulnerabilityInfo.leftoverBreakdown[0],
'vulnerabilities': vulnerabilityInfo.vulnerabilities,
'fixable': vulnerabilityInfo.fixable
};
if (vulnerabilityInfo.highestSeverity) {
severityCountMap[vulnerabilityInfo.highestSeverity['index']]++;
} else {
// Ensures that features with no vulns are always at the bottom of the table in the
// default sort by fixableScore.
feature_obj['fixableScore'] = -1;
feature_obj['leftoverScore'] = -1;
}
highestFixableScore = Math.max(highestFixableScore, vulnerabilityInfo.fixableScore);
features.push(feature_obj);
});
// Calculate the breakdown of each severity level for the features.
var totalCount = features.length;
var severityBreakdown = [];
levels.forEach(function(level) {
if (!severityCountMap[level['index']]) {
return;
}
totalCount -= severityCountMap[level['index']];
severityBreakdown.push({
'index': level['index'],
'label': level['title'],
'value': severityCountMap[level['index']],
'color': level['color']
});
});
if (totalCount > 0) {
severityBreakdown.push({
'index': levels.length,
'label': 'None',
'value': totalCount,
'color': '#2FC98E'
});
}
return {
'features': features,
'brokenFeaturesCount': features.length - totalCount,
'fixableFeatureCount': features.filter(function(f) { return f.fixableScore > 0 }).length,
'severityBreakdown': severityBreakdown,
'highestFixableScore': highestFixableScore
}
};
vulnService.loadImageVulnerabilities = function(repo, image_id, result, reject) {
var params = {
'imageid': image_id,
'repository': repo.namespace + '/' + repo.name,
'vulnerabilities': true,
};
ApiService.getRepoImageSecurity(null, params).then(result, reject);
};
vulnService.loadManifestVulnerabilities = function(repo, digest, result, reject) {
var params = {
'manifestref': digest,
'repository': repo.namespace + '/' + repo.name,
'vulnerabilities': true,
};
ApiService.getRepoManifestSecurity(null, params).then(result, reject);
};
vulnService.hasFeatures = function(resp) {
return resp.data && resp.data.Layer && resp.data.Layer.Features && resp.data.Layer.Features.length;
};
vulnService.forEachFeature = function(resp, callback) {
if (!vulnService.hasFeatures(resp)) {
return;
}
resp.data.Layer.Features.forEach(callback);
};
vulnService.forEachVulnerability = function(resp, callback) {
if (!vulnService.hasFeatures(resp)) {
return;
}
vulnService.forEachFeature(resp, function(feature) {
if (feature.Vulnerabilities) {
feature.Vulnerabilities.forEach(callback);
}
});
};
var cvssSeverityMap = {};
vulnService.getSeverityForCVSS = function(score) {
if (cvssSeverityMap[score]) {
return cvssSeverityMap[score];
}
var levels = vulnService.getLevels();
for (var i = 0; i < levels.length; ++i) {
if (score >= levels[i].score) {
cvssSeverityMap[score] = levels[i];
return levels[i];
}
}
return vulnService.LEVELS['Unknown'];
};
vulnService.getCVSSColor = function(score) {
if (score == null) {
return null;
}
return vulnService.getSeverityForCVSS(score).color;
};
vulnService.getLevels = function() {
var levels = Object.keys(vulnService.LEVELS).map(function(key) {
return vulnService.LEVELS[key];
});
return levels.sort(function(a, b) {
return a.index - b.index;
});
};
vulnService.parseVectorsString = function(vectorsString) {
return vectorsString.split('/');
};
vulnService.getVectorTitle = function(vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
return vector.title;
};
vulnService.getVectorDescription = function(vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
return vector.description;
};
vulnService.getVectorClasses = function(option, vectorString) {
var parts = vectorString.split(':');
var vector = vulnService.NVD_VECTORS[parts[0]];
if (!vector) {
return '';
}
var classes = '';
if (option.id == parts[1]) {
classes += 'current-vector ';
} else {
classes += 'not-current-vector ';
}
classes += option.severity;
return classes;
};
vulnService.getVectorOptions = function(vectorString) {
var parts = vectorString.split(':');
return vulnService.NVD_VECTORS[parts[0]].values;
};
vulnService.NVD_VECTORS = {
'AV': {
'title': 'Access Vector',
'description': 'This metric reflects how the vulnerability is exploited. The more remote an attacker can be to attack a host, the greater the vulnerability score.',
'values': [
{
'id': 'N',
'title': 'Network',
'description': 'A vulnerability exploitable with network access means the vulnerable software is bound to the network stack and the attacker does not require local network access or local access. Such a vulnerability is often termed "remotely exploitable". An example of a network attack is an RPC buffer overflow.',
'severity': 'high'
},
{
'id': 'A',
'title': 'Adjacent Network',
'description': 'A vulnerability exploitable with adjacent network access requires the attacker to have access to either the broadcast or collision domain of the vulnerable software. Examples of local networks include local IP subnet, Bluetooth, IEEE 802.11, and local Ethernet segment.',
'severity': 'medium'
},
{
'id': 'L',
'title': 'Local',
'description': 'A vulnerability exploitable with only local access requires the attacker to have either physical access to the vulnerable system or a local (shell) account. Examples of locally exploitable vulnerabilities are peripheral attacks such as Firewire/USB DMA attacks, and local privilege escalations (e.g., sudo).',
'severity': 'low'
}
]
},
'AC': {
'title': 'Access Complexity',
'description': 'This metric measures the complexity of the attack required to exploit the vulnerability once an attacker has gained access to the target system. For example, consider a buffer overflow in an Internet service: once the target system is located, the attacker can launch an exploit at will.',
'values': [
{
'id': 'L',
'title': 'Low',
'description': 'Specialized access conditions or extenuating circumstances do not exist making this easy to exploit',
'severity': 'high'
},
{
'id': 'M',
'title': 'Medium',
'description': 'The access conditions are somewhat specialized making this somewhat difficult to exploit',
'severity': 'medium'
},
{
'id': 'H',
'title': 'High',
'description': 'Specialized access conditions exist making this harder to exploit',
'severity': 'low'
}
]
},
'Au': {
'title': 'Authentication',
'description': 'This metric measures the number of times an attacker must authenticate to a target in order to exploit a vulnerability. This metric does not gauge the strength or complexity of the authentication process, only that an attacker is required to provide credentials before an exploit may occur.  The fewer authentication instances that are required, the higher the vulnerability score.',
'values': [
{
'id': 'N',
'title': 'None',
'description': 'Authentication is not required to exploit the vulnerability.',
'severity': 'high'
},
{
'id': 'S',
'title': 'Single',
'description': 'The vulnerability requires an attacker to be logged into the system (such as at a command line or via a desktop session or web interface).',
'severity': 'medium'
},
{
'id': 'M',
'title': 'Multiple',
'description': 'Exploiting the vulnerability requires that the attacker authenticate two or more times, even if the same credentials are used each time. An example is an attacker authenticating to an operating system in addition to providing credentials to access an application hosted on that system.',
'severity': 'low'
}
]
},
'C': {
'title': 'Confidentiality Impact',
'description': 'This metric measures the impact on confidentiality of a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones. Increased confidentiality impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is total information disclosure, resulting in all system files being revealed. The attacker is able to read all of the system\'s data (memory, files, etc.)',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'There is considerable informational disclosure. Access to some system files is possible, but the attacker does not have control over what is obtained, or the scope of the loss is constrained. An example is a vulnerability that divulges only certain tables in a database.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the confidentiality of the system.',
'severity': 'low'
}
]
},
'I': {
'title': 'Integrity Impact',
'description': 'This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and guaranteed veracity of information. Increased integrity impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is a total compromise of system integrity. There is a complete loss of system protection, resulting in the entire system being compromised. The attacker is able to modify any files on the target system',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'Modification of some system files or information is possible, but the attacker does not have control over what can be modified, or the scope of what the attacker can affect is limited. For example, system or application files may be overwritten or modified, but either the attacker has no control over which files are affected or the attacker can modify files within only a limited context or scope.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the integrity of the system.',
'severity': 'low'
}
]
},
'A': {
'title': 'Availability Impact',
'description': 'This metric measures the impact to availability of a successfully exploited vulnerability. Availability refers to the accessibility of information resources. Attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system. Increased availability impact increases the vulnerability score.',
'values': [
{
'id': 'C',
'title': 'Complete',
'description': 'There is a total shutdown of the affected resource. The attacker can render the resource completely unavailable.',
'severity': 'high'
},
{
'id': 'P',
'title': 'Partial',
'description': 'There is reduced performance or interruptions in resource availability. An example is a network-based flood attack that permits a limited number of successful connections to an Internet service.',
'severity': 'medium'
},
{
'id': 'N',
'title': 'None',
'description': 'There is no impact to the availability of the system.',
'severity': 'low'
}
]
}
};
return vulnService;
}]);