1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5// Utils provide logging functions and other JS functions commonly used by the
6// app and media players.
7var Utils = new function() {
8  this.titleChanged = false;
9};
10
11// Adds options to document element.
12Utils.addOptions = function(elementID, keyValueOptions, disabledOptions) {
13  disabledOptions = disabledOptions || [];
14  var selectElement = document.getElementById(elementID);
15  var keys = Object.keys(keyValueOptions);
16  for (var i = 0; i < keys.length; i++) {
17    var key = keys[i];
18    var option = new Option(key, keyValueOptions[key]);
19    option.title = keyValueOptions[key];
20    if (disabledOptions.indexOf(key) >= 0)
21      option.disabled = true;
22    selectElement.options.add(option);
23  }
24};
25
26Utils.convertToArray = function(input) {
27  if (Array.isArray(input))
28    return input;
29  return [input];
30};
31
32Utils.convertToUint8Array = function(msg) {
33  if (typeof msg == 'string') {
34    var ans = new Uint8Array(msg.length);
35    for (var i = 0; i < msg.length; i++) {
36      ans[i] = msg.charCodeAt(i);
37    }
38    return ans;
39  }
40  // Assume it is an ArrayBuffer or ArrayBufferView. If it already is a
41  // Uint8Array, this will just make a copy of the view.
42  return new Uint8Array(msg);
43};
44
45Utils.createJWKData = function(keyId, key) {
46  // JWK routines copied from third_party/WebKit/LayoutTests/media/
47  //   encrypted-media/encrypted-media-utils.js
48  //
49  // Encodes data (Uint8Array) into base64 string without trailing '='.
50  // TODO(jrummell): Update once the EME spec is updated to say base64url
51  // encoding.
52  function base64Encode(data) {
53    var result = btoa(String.fromCharCode.apply(null, data));
54    return result.replace(/=+$/g, '');
55  }
56
57  // Creates a JWK from raw key ID and key.
58  function createJWK(keyId, key) {
59    var jwk = '{"kty":"oct","kid":"';
60    jwk += base64Encode(keyId);
61    jwk += '","k":"';
62    jwk += base64Encode(key);
63    jwk += '"}';
64    return jwk;
65  }
66
67  // Creates a JWK Set from an array of JWK(s).
68  function createJWKSet() {
69    var jwkSet = '{"keys":[';
70    for (var i = 0; i < arguments.length; i++) {
71      if (i != 0)
72        jwkSet += ',';
73      jwkSet += arguments[i];
74    }
75    jwkSet += ']}';
76    return jwkSet;
77  }
78
79  return Utils.convertToUint8Array(createJWKSet(createJWK(keyId, key)));
80};
81
82Utils.extractFirstLicenseKey = function(message) {
83  // Decodes data (Uint8Array) from base64 string.
84  // TODO(jrummell): Update once the EME spec is updated to say base64url
85  // encoding.
86  function base64Decode(data) {
87    return atob(data);
88  }
89
90  function convertToString(data) {
91    return String.fromCharCode.apply(null, Utils.convertToUint8Array(data));
92  }
93
94  try {
95    var json = JSON.parse(convertToString(message));
96    // Decode the first element of 'kids', return it as an Uint8Array.
97    return Utils.convertToUint8Array(base64Decode(json.kids[0]));
98  } catch (error) {
99    // Not valid JSON, so return message untouched as Uint8Array.
100    return Utils.convertToUint8Array(message);
101  }
102}
103
104Utils.documentLog = function(log, success, time) {
105  if (!docLogs)
106    return;
107  time = time || Utils.getCurrentTimeString();
108  var timeLog = '<span style="color: green">' + time + '</span>';
109  var logColor = !success ? 'red' : 'black'; // default is true.
110  log = '<span style="color: "' + logColor + '>' + log + '</span>';
111  docLogs.innerHTML = timeLog + ' - ' + log + '<br>' + docLogs.innerHTML;
112};
113
114Utils.ensureOptionInList = function(listID, option) {
115  var selectElement = document.getElementById(listID);
116  for (var i = 0; i < selectElement.length; i++) {
117    if (selectElement.options[i].value == option) {
118      selectElement.value = option;
119      return;
120    }
121  }
122  // The list does not have the option, let's add it and select it.
123  var optionElement = new Option(option, option);
124  optionElement.title = option;
125  selectElement.options.add(optionElement);
126  selectElement.value = option;
127};
128
129Utils.failTest = function(msg, newTitle) {
130  var failMessage = 'FAIL: ';
131  var title = 'FAILED';
132  // Handle exception messages;
133  if (msg.message) {
134    title = msg.name || 'Error';
135    failMessage += title + ' ' + msg.message;
136  } else if (msg instanceof Event) {
137    // Handle failing events.
138    failMessage = msg.target + '.' + msg.type;
139    title = msg.type;
140  } else {
141    failMessage += msg;
142  }
143  // Force newTitle if passed.
144  title = newTitle || title;
145  // Log failure.
146  Utils.documentLog(failMessage, false);
147  console.log(failMessage, msg);
148  Utils.setResultInTitle(title);
149};
150
151Utils.getCurrentTimeString = function() {
152  var date = new Date();
153  var hours = ('0' + date.getHours()).slice(-2);
154  var minutes = ('0' + date.getMinutes()).slice(-2);
155  var secs = ('0' + date.getSeconds()).slice(-2);
156  var milliSecs = ('00' + date.getMilliseconds()).slice(-3);
157  return hours + ':' + minutes + ':' + secs + '.' + milliSecs;
158};
159
160Utils.getDefaultKey = function(forceInvalidResponse) {
161  if (forceInvalidResponse) {
162    Utils.timeLog('Forcing invalid key data.');
163    return new Uint8Array([0xAA]);
164  }
165  return KEY;
166};
167
168Utils.getHexString = function(uintArray) {
169  var hex_str = '';
170  for (var i = 0; i < uintArray.length; i++) {
171    var hex = uintArray[i].toString(16);
172    if (hex.length == 1)
173      hex = '0' + hex;
174    hex_str += hex;
175  }
176  return hex_str;
177};
178
179Utils.getInitDataFromMessage = function(message, mediaType, decodeJSONMessage) {
180  var initData;
181  if (mediaType.indexOf('mp4') != -1) {
182    // Temporary hack for Clear Key in v0.1.
183    // If content uses mp4, then message.message is PSSH data. Instead of
184    // parsing that data we hard code the initData.
185    initData = Utils.convertToUint8Array(KEY_ID);
186  } else if (decodeJSONMessage) {
187    initData = Utils.extractFirstLicenseKey(message.message);
188  } else {
189    initData = Utils.convertToUint8Array(message.message);
190  }
191  return initData;
192};
193
194Utils.hasPrefix = function(msg, prefix) {
195  var message = String.fromCharCode.apply(null, msg);
196  return message.substring(0, prefix.length) == prefix;
197};
198
199Utils.installTitleEventHandler = function(element, event) {
200  element.addEventListener(event, function(e) {
201    Utils.setResultInTitle(e.type);
202  }, false);
203};
204
205Utils.isHeartBeatMessage = function(msg) {
206  return Utils.hasPrefix(Utils.convertToUint8Array(msg), HEART_BEAT_HEADER);
207};
208
209Utils.resetTitleChange = function() {
210  this.titleChanged = false;
211  document.title = '';
212};
213
214Utils.sendRequest = function(requestType, responseType, message, serverURL,
215                             onSuccessCallbackFn, forceInvalidResponse) {
216  var requestAttemptCount = 0;
217  var MAXIMUM_REQUEST_ATTEMPTS = 3;
218  var REQUEST_RETRY_DELAY_MS = 3000;
219
220  function sendRequestAttempt() {
221    requestAttemptCount++;
222    if (requestAttemptCount == MAXIMUM_REQUEST_ATTEMPTS) {
223      Utils.failTest('FAILED: Exceeded maximum license request attempts.');
224      return;
225    }
226    var xmlhttp = new XMLHttpRequest();
227    xmlhttp.responseType = responseType;
228    xmlhttp.open(requestType, serverURL, true);
229
230    xmlhttp.onload = function(e) {
231      if (this.status == 200) {
232        if (onSuccessCallbackFn)
233          onSuccessCallbackFn(this.response);
234      } else {
235        Utils.timeLog('Bad response status: ' + this.status);
236        Utils.timeLog('Bad response: ' + this.response);
237        Utils.timeLog('Retrying request if possible in ' +
238                      REQUEST_RETRY_DELAY_MS + 'ms');
239        setTimeout(sendRequestAttempt, REQUEST_RETRY_DELAY_MS);
240      }
241    };
242    Utils.timeLog('Attempt (' + requestAttemptCount +
243                  '): sending request to server: ' + serverURL);
244    xmlhttp.send(message);
245  }
246
247  if (forceInvalidResponse) {
248    Utils.timeLog('Not sending request - forcing an invalid response.');
249    return onSuccessCallbackFn([0xAA]);
250  }
251  sendRequestAttempt();
252};
253
254Utils.setResultInTitle = function(title) {
255  // If document title is 'ENDED', then update it with new title to possibly
256  // mark a test as failure.  Otherwise, keep the first title change in place.
257  if (!this.titleChanged || document.title.toUpperCase() == 'ENDED')
258    document.title = title.toUpperCase();
259  Utils.timeLog('Set document title to: ' + title + ', updated title: ' +
260                document.title);
261  this.titleChanged = true;
262};
263
264Utils.timeLog = function(/**/) {
265  if (arguments.length == 0)
266    return;
267  var time = Utils.getCurrentTimeString();
268  // Log to document.
269  Utils.documentLog(arguments[0], time);
270  // Log to JS console.
271  var logString = time + ' - ';
272  for (var i = 0; i < arguments.length; i++) {
273    logString += ' ' + arguments[i];
274  }
275  console.log(logString);
276};
277