<commit_before>// This file is leveraged when the start actually spins a new server up.
// Meaning, updated code here WILL NOT RUN unless a new server needs to spun
// up. To force a new server to spin up, alternative between different ports
// when running `gulp openReport`, like so:
//
// `gulp openReport --report-port=3034`
const express = require('express');
const path = require('path');
const _ = require('underscore');
const os = require('os');
// const exec = require('child_process').exec;
const spawn = require('child_process').spawn; // TODO: combine this with above
const argv = require('yargs').argv;
const paths = require('./gulp/util/paths');
const fs = require('fs-extra');
const archiver = require('archiver');

const app = express();

fs.writeFileSync('current-test.txt', 'done');

const rootDir = __dirname;

// FORK: Allow port to be configurable.
const port = argv['report-port'] || 3001;

app.use(express.json());
app.use(express.urlencoded());
app.use(express.static(rootDir));

app.use('/bitmaps_reference', express.static(paths.bitmaps_reference));
app.use('/bitmaps_test', express.static(paths.bitmaps_test));

// app.all('/app/',function(req,res){
//  console.log(new Date());
//  exec("gulp test",puts);
//  res.send('ok');
// })
//
app.get('/download', (req, res) => {
  const blessedDir = path.join(paths.bitmaps_reference, '../', 'blessed');

  try {
    fs.accessSync(blessedDir);
  } catch (e) {
    res.send('No images have been blessed yet to download.');
    return;
  }

  const output = fs.createWriteStream(`${__dirname}/bitmaps_reference.zip`);
  const archive = archiver('zip', {
    store: true,
  });

  // listen for all archive data to be written
  output.on('close', () => {
    console.log(`${archive.pointer()} total bytes`);
    console.log('archiver has been finalized and the output file descriptor has closed.');

    res.download(path.resolve(path.join(__dirname, '/bitmaps_reference.zip')));
  });

  // good practice to catch this error explicitly
  archive.on('error', (err) => {
    throw err;
  });

  // pipe archive data to the file
  archive.pipe(output);

  archive.directory(blessedDir, 'bitmaps_reference', false);

  // finalize the archive (ie we are done appending files but streams have to finish yet)
  archive.finalize();
});

app.post('/baseline', (req, res) => {
  const params = req.body;

  // TODO: Make the string splitting and manipulation more robust
  // TODO: Add error handling
  const toBless = params.toBless;
  const status = toBless ? 'blessed' : 'fail';

  // I think something about this process isn't right. We are really just trying
  // to grab the test image and replace the reference image with it.
  //
  // We also need to revert it if possible.
  // This gives us the right directory under bitmaps_test the
  // relevant screenshots are in.
  const blessed = params.blessed.split('bitmaps_test/')[1];
  // This gives us the basename.
  const basename = path.basename(blessed);
  console.info('blessed', blessed);
  console.info('basename', basename);

  // move the blessed image to the bitmaps reference.
  //
  // In all of this, we just need to know the locations of the references
  // and test directories.
  //
  // We are copying the screenshot from the bitmaps_test directory to
  // the bitmaps_reference directory. That is what 'blessing' does.
  //
  // something like ../../ will be extracted
  //
  // I do not really understand yet how to get the right cwd.
  // const cwd = `${paths.bitmaps_reference.split('../')[0]}../`;
  const testFile = path.join(paths.bitmaps_test, blessed);
  const refFile = path.join(paths.bitmaps_reference, basename);
  const revertFile = path.join(paths.bitmaps_reference, '../', 'tmp', basename);
  const downloadFile = path.join(paths.bitmaps_reference, '../', 'blessed', basename);

  // console.info('cwd', cwd);
  // console.info('testFile', testFile);
  // console.info('refFile', refFile);
  // console.info('revertFile', revertFile);
  //
  // fs.createReadStream(testFile)
  //   .pipe(fs.createWriteStream(refFile));

  // update the config.json to reflect status of blessed
  const configFileName = paths.compareConfigFileName;
  const configToUpdate = params.index;
  let configObj;

  fs.readFile(configFileName, 'utf8', (err, data) => {
    if (err) throw err;
    configObj = JSON.parse(data);

    for (testPair of configObj.testPairs) {
        if (testPair.fileName === req.body.fileName) {
          testPair.local_testStatus = status;
        }
      }

    fs.writeFile(configFileName, JSON.stringify(configObj, null, 2), function (err) {
      if (err) throw err;

      console.log('\n');
      if (toBless) {
        console.log('Blessed file received: ' + basename);
        fs.copySync(refFile, revertFile);
        fs.copySync(testFile, refFile);
        fs.copySync(testFile, downloadFile);
        console.log('Moving to baseline reference directory, ready for version control');
      } else {
        console.log('File to Unbless received: ' + basename);
        fs.copySync(revertFile, refFile);
        fs.removeSync(downloadFile);
        console.log('Reset to "fail" status');
      }
      console.log('Updating data (writing to ' + configFileName + ')');

      // respond with the index of the object to update on the client
      res.send({ testPairToUpdate: configToUpdate });
    });
  });
});

// This receives and stores the test name and baseline boolean in a text file
// The /backstop request will read the data from this file
// TODO: see if this is even necessary
app.post('/backstop-test-prep', (req, res) => {
  const currentTest = fs.readFileSync('current-test.txt', 'utf8');
  if (currentTest === 'done') {
    console.log('Preparing to test: ', req.body.test_name);
    fs.writeFileSync('current-test.txt', JSON.stringify(req.body));
    console.log('Writing to current-test.txt');
    res.send('ok');
  } else {
    res.send({ status: 'busy' });
  }
});

// This fires off a gulp backstop task
// responds with a Server Sent Event
// so the stdout can be printed on the client page
app.get('/backstop', function(req, res) {

  // since you can't pass data to SSE, grab the tests to run
  // from current-test.txt
  var current_test_data = fs.readFileSync('current-test.txt', 'utf8');

  current_test_data = JSON.parse(current_test_data);

  fs.writeFileSync('current-test.txt', 'running');

  // array of options for spawn
  var gulpBackstopOptions = ['backstop'];

  // set options for gulp backstop command
  if (current_test_data.test_name === 'all') {
    gulpBackstopOptions.push('--path=ALL');
  } else  {
    gulpBackstopOptions.push('--name=' + current_test_data.test_name);
  }

  if (current_test_data.baseline) {
    gulpBackstopOptions.push('--baseline');
  }

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-control": "no-cache"
  });

  var spw = spawn('gulp', gulpBackstopOptions, {cwd: '../../'});

  var str = '';

  spw.stdout.on('data', function (data) {
    str += data.toString();

    // Flush out line by line.
    var lines = str.split("\n");

    // this cleans up the string \n
    for (var i in lines) {
      if (i == lines.length - 1) {
        str = lines[i];
      } else {
        console.log(lines[i]);
        // Note: The double-newline is *required*
        res.write('data: ' + lines[i] + "\n\n");
      }
    }

  });

  spw.on('close', function (code) {
    if (code === 0) {
      res.write('event: done' + "\n");
      res.write('data: {"test_name": "TODO: Actually pass in uselful test_name"}' + '\n\n');
      fs.writeFileSync('current-test.txt', 'done');
      console.log('Done! Gulp backstop child process closed with code 0');
    } else {
      console.log('Gulp backstop child process FAILED to be killed.');
    }
    res.end(str);
  });

  // TODO: this will error if you uncomment, need to debug
  spw.stderr.on('data', function (data) {
    //res.end('stderr: ' + data);
  });

});

var listenerHook = app.listen(port);

//===================
console.log('Current Directory: '+__dirname);
console.log('Serving files from: '+rootDir);
console.log('Listening on: ' + getAddresses() + ':' + port + '');

//=====================
function getAddresses(){
  var interfaces = os.networkInterfaces(),
    addresses = [];

  _.each(interfaces,function(net) {
    _.each(net,function(address) {
      if (address.family == 'IPv4' && !address.internal) addresses.push(address.address);
    });
  });

  return addresses;
}
<commit_msg>Formatting fix.<commit_after>// This file is leveraged when the start actually spins a new server up.
// Meaning, updated code here WILL NOT RUN unless a new server needs to spun
// up. To force a new server to spin up, alternative between different ports
// when running `gulp openReport`, like so:
//
// `gulp openReport --report-port=3034`
const express = require('express');
const path = require('path');
const _ = require('underscore');
const os = require('os');
// const exec = require('child_process').exec;
const spawn = require('child_process').spawn; // TODO: combine this with above
const argv = require('yargs').argv;
const paths = require('./gulp/util/paths');
const fs = require('fs-extra');
const archiver = require('archiver');

const app = express();

fs.writeFileSync('current-test.txt', 'done');

const rootDir = __dirname;

// FORK: Allow port to be configurable.
const port = argv['report-port'] || 3001;

app.use(express.json());
app.use(express.urlencoded());
app.use(express.static(rootDir));

app.use('/bitmaps_reference', express.static(paths.bitmaps_reference));
app.use('/bitmaps_test', express.static(paths.bitmaps_test));

// app.all('/app/',function(req,res){
//  console.log(new Date());
//  exec("gulp test",puts);
//  res.send('ok');
// })
//
app.get('/download', (req, res) => {
  const blessedDir = path.join(paths.bitmaps_reference, '../', 'blessed');

  try {
    fs.accessSync(blessedDir);
  } catch (e) {
    res.send('No images have been blessed yet to download.');
    return;
  }

  const output = fs.createWriteStream(`${__dirname}/bitmaps_reference.zip`);
  const archive = archiver('zip', {
    store: true,
  });

  // listen for all archive data to be written
  output.on('close', () => {
    console.log(`${archive.pointer()} total bytes`);
    console.log('archiver has been finalized and the output file descriptor has closed.');

    res.download(path.resolve(path.join(__dirname, '/bitmaps_reference.zip')));
  });

  // good practice to catch this error explicitly
  archive.on('error', (err) => {
    throw err;
  });

  // pipe archive data to the file
  archive.pipe(output);

  archive.directory(blessedDir, 'bitmaps_reference', false);

  // finalize the archive (ie we are done appending files but streams have to finish yet)
  archive.finalize();
});

app.post('/baseline', (req, res) => {
  const params = req.body;

  // TODO: Make the string splitting and manipulation more robust
  // TODO: Add error handling
  const toBless = params.toBless;
  const status = toBless ? 'blessed' : 'fail';

  // I think something about this process isn't right. We are really just trying
  // to grab the test image and replace the reference image with it.
  //
  // We also need to revert it if possible.
  // This gives us the right directory under bitmaps_test the
  // relevant screenshots are in.
  const blessed = params.blessed.split('bitmaps_test/')[1];
  // This gives us the basename.
  const basename = path.basename(blessed);
  console.info('blessed', blessed);
  console.info('basename', basename);

  // move the blessed image to the bitmaps reference.
  //
  // In all of this, we just need to know the locations of the references
  // and test directories.
  //
  // We are copying the screenshot from the bitmaps_test directory to
  // the bitmaps_reference directory. That is what 'blessing' does.
  //
  // something like ../../ will be extracted
  //
  // I do not really understand yet how to get the right cwd.
  // const cwd = `${paths.bitmaps_reference.split('../')[0]}../`;
  const testFile = path.join(paths.bitmaps_test, blessed);
  const refFile = path.join(paths.bitmaps_reference, basename);
  const revertFile = path.join(paths.bitmaps_reference, '../', 'tmp', basename);
  const downloadFile = path.join(paths.bitmaps_reference, '../', 'blessed', basename);

  // console.info('cwd', cwd);
  // console.info('testFile', testFile);
  // console.info('refFile', refFile);
  // console.info('revertFile', revertFile);
  //
  // fs.createReadStream(testFile)
  //   .pipe(fs.createWriteStream(refFile));

  // update the config.json to reflect status of blessed
  const configFileName = paths.compareConfigFileName;
  const configToUpdate = params.index;
  let configObj;

  fs.readFile(configFileName, 'utf8', (err, data) => {
    if (err) throw err;
    configObj = JSON.parse(data);

    for (testPair of configObj.testPairs) {
      if (testPair.fileName === req.body.fileName) {
        testPair.local_testStatus = status;
      }
    }

    fs.writeFile(configFileName, JSON.stringify(configObj, null, 2), function (err) {
      if (err) throw err;

      console.log('\n');
      if (toBless) {
        console.log('Blessed file received: ' + basename);
        fs.copySync(refFile, revertFile);
        fs.copySync(testFile, refFile);
        fs.copySync(testFile, downloadFile);
        console.log('Moving to baseline reference directory, ready for version control');
      } else {
        console.log('File to Unbless received: ' + basename);
        fs.copySync(revertFile, refFile);
        fs.removeSync(downloadFile);
        console.log('Reset to "fail" status');
      }
      console.log('Updating data (writing to ' + configFileName + ')');

      // respond with the index of the object to update on the client
      res.send({ testPairToUpdate: configToUpdate });
    });
  });
});

// This receives and stores the test name and baseline boolean in a text file
// The /backstop request will read the data from this file
// TODO: see if this is even necessary
app.post('/backstop-test-prep', (req, res) => {
  const currentTest = fs.readFileSync('current-test.txt', 'utf8');
  if (currentTest === 'done') {
    console.log('Preparing to test: ', req.body.test_name);
    fs.writeFileSync('current-test.txt', JSON.stringify(req.body));
    console.log('Writing to current-test.txt');
    res.send('ok');
  } else {
    res.send({ status: 'busy' });
  }
});

// This fires off a gulp backstop task
// responds with a Server Sent Event
// so the stdout can be printed on the client page
app.get('/backstop', function(req, res) {

  // since you can't pass data to SSE, grab the tests to run
  // from current-test.txt
  var current_test_data = fs.readFileSync('current-test.txt', 'utf8');

  current_test_data = JSON.parse(current_test_data);

  fs.writeFileSync('current-test.txt', 'running');

  // array of options for spawn
  var gulpBackstopOptions = ['backstop'];

  // set options for gulp backstop command
  if (current_test_data.test_name === 'all') {
    gulpBackstopOptions.push('--path=ALL');
  } else  {
    gulpBackstopOptions.push('--name=' + current_test_data.test_name);
  }

  if (current_test_data.baseline) {
    gulpBackstopOptions.push('--baseline');
  }

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-control": "no-cache"
  });

  var spw = spawn('gulp', gulpBackstopOptions, {cwd: '../../'});

  var str = '';

  spw.stdout.on('data', function (data) {
    str += data.toString();

    // Flush out line by line.
    var lines = str.split("\n");

    // this cleans up the string \n
    for (var i in lines) {
      if (i == lines.length - 1) {
        str = lines[i];
      } else {
        console.log(lines[i]);
        // Note: The double-newline is *required*
        res.write('data: ' + lines[i] + "\n\n");
      }
    }

  });

  spw.on('close', function (code) {
    if (code === 0) {
      res.write('event: done' + "\n");
      res.write('data: {"test_name": "TODO: Actually pass in uselful test_name"}' + '\n\n');
      fs.writeFileSync('current-test.txt', 'done');
      console.log('Done! Gulp backstop child process closed with code 0');
    } else {
      console.log('Gulp backstop child process FAILED to be killed.');
    }
    res.end(str);
  });

  // TODO: this will error if you uncomment, need to debug
  spw.stderr.on('data', function (data) {
    //res.end('stderr: ' + data);
  });

});

var listenerHook = app.listen(port);

//===================
console.log('Current Directory: '+__dirname);
console.log('Serving files from: '+rootDir);
console.log('Listening on: ' + getAddresses() + ':' + port + '');

//=====================
function getAddresses(){
  var interfaces = os.networkInterfaces(),
    addresses = [];

  _.each(interfaces,function(net) {
    _.each(net,function(address) {
      if (address.family == 'IPv4' && !address.internal) addresses.push(address.address);
    });
  });

  return addresses;
}
<|endoftext|>


<commit_before>#!/usr/bin/env node
'use strict';

const express = require('express');
const http = require('http');

const config = require('./lib/config').load();
const logger = require('./lib/logger').attach(config);

const host = config.get('service:hostname');
const port = config.get('service:port');
const app = express();
const server = http.createServer(app);

require('./lib/control/v1/core').attach(app);
require('./lib/control/v1/conqueso').attach(app);

server.listen(port, host, () => {
  logger.info('Listening on ' + host + ':' + port);
});
<commit_msg>Provide a command line option for loading a configuraiton file.<commit_after>#!/usr/bin/env node
'use strict';

const args = require('yargs')
  .usage('Usage: $0 [args]')
  .option('cf', {
    alias: 'config-file',
    describe: 'path to a configuration file',
    type: 'string'
  })
  .help('help')
  .argv

const express = require('express');
const http = require('http');

const config = require('./lib/config').load(args.cf);
const logger = require('./lib/logger').attach(config);

const host = config.get('service:hostname');
const port = config.get('service:port');
const app = express();
const server = http.createServer(app);

require('./lib/control/v1/core').attach(app);
require('./lib/control/v1/conqueso').attach(app);

server.listen(port, host, () => {
  logger.info('Listening on ' + host + ':' + port);
});
<|endoftext|>


<commit_before>const fs = require('fs');
const url = require('url');
const path = require('path');
const http = require('http');
const https = require('https');
const os = require('os');
const net = require('net');
const cwd = process.cwd();

const fport = (p = 0) =>
  new Promise((resolve, reject) => {
    const s = net.createServer();
    s.on('error', reject);
    s.listen(p, () => {
      const { port } = s.address();
      s.close(() => resolve(port));
    });
  });

const ips = Object.values(os.networkInterfaces())
  .reduce((every, i) => [...every, ...i], [])
  .filter(i => i.family === 'IPv4' && i.internal === false)
  .map(i => i.address);

const mimes = Object.entries(require('./types.json')).reduce(
  (all, [type, exts]) =>
    Object.assign(all, ...exts.map(ext => ({ [ext]: type }))),
  {}
);

const livereload = `
  <script>
    const source = new EventSource('/livereload');
    source.onmessage = e => location.reload(true);
    console.log('[servor] listening for file changes');
  </script>
`;

module.exports = async ({
  root = '.',
  fallback = 'index.html',
  port,
  reload = true,
  inject,
  credentials
} = {}) => {
  try {
    port = await fport(port || process.env.PORT || 8080);
  } catch {
    if (port || process.env.PORT) {
      console.log('[ERR] The port you have specified is already in use!');
      process.exit();
    }
    port = await fport();
  }
  root = root.startsWith('/') ? root : path.join(cwd, root);
  const clients = [];
  const protocol = credentials ? 'https' : 'http';
  const server = credentials
    ? cb => https.createServer(credentials, cb)
    : cb => http.createServer(cb);

  // Server utility functions

  const sendError = (res, resource, status) => {
    res.writeHead(status);
    res.end();
  };

  const sendFile = (res, resource, status, file, ext) => {
    res.writeHead(status, {
      'Content-Type': mimes[ext] || 'application/octet-stream',
      'Access-Control-Allow-Origin': '*'
    });
    res.write(file, 'binary');
    res.end();
  };

  const sendMessage = (res, channel, data) => {
    res.write(`event: ${channel}\nid: 0\ndata: ${data}\n`);
    res.write('\n\n');
  };

  const isRouteRequest = pathname =>
    !~pathname
      .split('/')
      .pop()
      .indexOf('.');

  // Start the server on the desired port

  server((req, res) => {
    const pathname = url.parse(req.url).pathname;
    if (reload && pathname === '/livereload') {
      res.writeHead(200, {
        Connection: 'keep-alive',
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Access-Control-Allow-Origin': '*'
      });
      sendMessage(res, 'connected', 'ready');
      setInterval(sendMessage, 60000, res, 'ping', 'waiting');
      clients.push(res);
    } else {
      const isRoute = isRouteRequest(pathname);
      const status = isRoute && pathname !== '/' ? 301 : 200;
      const resource = isRoute ? `/${fallback}` : decodeURI(pathname);
      const uri = path.join(root, resource);
      const ext = uri.replace(/^.*[\.\/\\]/, '').toLowerCase();
      fs.stat(uri, (err, stat) => {
        if (err) return sendError(res, resource, 404);
        fs.readFile(uri, 'binary', (err, file) => {
          if (err) return sendError(res, resource, 500);
          if (isRoute && inject) file = inject + file;
          if (isRoute && reload) file = livereload + file;
          sendFile(res, resource, status, file, ext);
        });
      });
    }
  }).listen(parseInt(port, 10));

  // Notify livereload clients on file change

  reload &&
    fs.watch(root, { recursive: true }, () => {
      while (clients.length > 0)
        sendMessage(clients.pop(), 'message', 'reload');
    });

  return {
    url: `${protocol}://localhost:${port}`,
    root,
    protocol,
    port,
    ips
  };
};
<commit_msg>Add back node 8 support (#38)<commit_after>const fs = require('fs');
const url = require('url');
const path = require('path');
const http = require('http');
const https = require('https');
const os = require('os');
const net = require('net');
const cwd = process.cwd();

const fport = (p = 0) =>
  new Promise((resolve, reject) => {
    const s = net.createServer();
    s.on('error', reject);
    s.listen(p, () => {
      const { port } = s.address();
      s.close(() => resolve(port));
    });
  });

const ips = Object.values(os.networkInterfaces())
  .reduce((every, i) => [...every, ...i], [])
  .filter(i => i.family === 'IPv4' && i.internal === false)
  .map(i => i.address);

const mimes = Object.entries(require('./types.json')).reduce(
  (all, [type, exts]) =>
    Object.assign(all, ...exts.map(ext => ({ [ext]: type }))),
  {}
);

const livereload = `
  <script>
    const source = new EventSource('/livereload');
    source.onmessage = e => location.reload(true);
    console.log('[servor] listening for file changes');
  </script>
`;

module.exports = async ({
  root = '.',
  fallback = 'index.html',
  port,
  reload = true,
  inject,
  credentials
} = {}) => {
  try {
    port = await fport(port || process.env.PORT || 8080);
  } catch(e) {
    if (port || process.env.PORT) {
      console.log('[ERR] The port you have specified is already in use!');
      process.exit();
    }
    port = await fport();
  }
  root = root.startsWith('/') ? root : path.join(cwd, root);
  const clients = [];
  const protocol = credentials ? 'https' : 'http';
  const server = credentials
    ? cb => https.createServer(credentials, cb)
    : cb => http.createServer(cb);

  // Server utility functions

  const sendError = (res, resource, status) => {
    res.writeHead(status);
    res.end();
  };

  const sendFile = (res, resource, status, file, ext) => {
    res.writeHead(status, {
      'Content-Type': mimes[ext] || 'application/octet-stream',
      'Access-Control-Allow-Origin': '*'
    });
    res.write(file, 'binary');
    res.end();
  };

  const sendMessage = (res, channel, data) => {
    res.write(`event: ${channel}\nid: 0\ndata: ${data}\n`);
    res.write('\n\n');
  };

  const isRouteRequest = pathname =>
    !~pathname
      .split('/')
      .pop()
      .indexOf('.');

  // Start the server on the desired port

  server((req, res) => {
    const pathname = url.parse(req.url).pathname;
    if (reload && pathname === '/livereload') {
      res.writeHead(200, {
        Connection: 'keep-alive',
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Access-Control-Allow-Origin': '*'
      });
      sendMessage(res, 'connected', 'ready');
      setInterval(sendMessage, 60000, res, 'ping', 'waiting');
      clients.push(res);
    } else {
      const isRoute = isRouteRequest(pathname);
      const status = isRoute && pathname !== '/' ? 301 : 200;
      const resource = isRoute ? `/${fallback}` : decodeURI(pathname);
      const uri = path.join(root, resource);
      const ext = uri.replace(/^.*[\.\/\\]/, '').toLowerCase();
      fs.stat(uri, (err, stat) => {
        if (err) return sendError(res, resource, 404);
        fs.readFile(uri, 'binary', (err, file) => {
          if (err) return sendError(res, resource, 500);
          if (isRoute && inject) file = inject + file;
          if (isRoute && reload) file = livereload + file;
          sendFile(res, resource, status, file, ext);
        });
      });
    }
  }).listen(parseInt(port, 10));

  // Notify livereload clients on file change

  reload &&
    fs.watch(root, { recursive: true }, () => {
      while (clients.length > 0)
        sendMessage(clients.pop(), 'message', 'reload');
    });

  return {
    url: `${protocol}://localhost:${port}`,
    root,
    protocol,
    port,
    ips
  };
};
<|endoftext|>


<commit_before>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

(function() {
    "use strict";

    var $ = require('jquery');
    var cockpit = require('cockpit');

    var utils = require('./utils');

    /* STORAGED CLIENT
     */

    /* HACK: https://github.com/storaged-project/storaged/pull/68 */
    var config = { };
    if (cockpit.manifests["storage"] && cockpit.manifests["storage"]["config"])
        config = cockpit.manifests["storage"]["config"];

    var client = { };

    /* Metrics
     */

    function instance_sampler(metrics) {
        var instances;
        var self = {
            data: { },
            close: close
        };

        function handle_meta(msg) {
            self.data = { };
            instances = [ ];
            for (var m = 0; m < msg.metrics.length; m++) {
                instances[m] = msg.metrics[m].instances;
                for (var i = 0; i < instances[m].length; i++)
                    self.data[instances[m][i]] = [ ];
            }
        }

        function handle_data(msg) {
            var changed = false;
            for (var s = 0; s < msg.length; s++) {
                var metrics = msg[s];
                for (var m = 0; m < metrics.length; m++) {
                    var inst = metrics[m];
                    for (var i = 0; i < inst.length; i++) {
                        if (inst[i] !== null) {
                            changed = true;
                            self.data[instances[m][i]][m] = inst[i];
                        }
                    }
                }
            }
            if (changed)
                $(self).triggerHandler('changed');
        }

        var channel = cockpit.channel({ payload: "metrics1",
                                        source: "internal",
                                        metrics: metrics
                                      });
        $(channel).on("closed", function (event, error) {
            console.log("closed", error);
        });
        $(channel).on("message", function (event, message) {
            var msg = JSON.parse(message);
            if (msg.length)
                handle_data(msg);
            else
                handle_meta(msg);
        });

        function close() {
            channel.close();
        }

        return self;
    }

    /* D-Bus proxies
     */

    var STORAGED_SERVICE;
    var STORAGED_OPATH_PFX;
    var STORAGED_IFACE_PFX;

    client.time_offset = undefined;  /* Number of milliseconds that the server is ahead of us. */
    client.features = undefined;

    client.storaged_client = undefined;

    function proxy(iface, path) {
        return client.storaged_client.proxy(STORAGED_IFACE_PFX + "." + iface,
                                            STORAGED_OPATH_PFX + "/" + path,
                                            { watch: true });
    }

    function proxies(iface) {
        /* We create the proxies here with 'watch' set to false and
         * establish a general watch for all of them.  This is more
         * efficient since it reduces the number of D-Bus calls done
         * by the cache.
         */
        return client.storaged_client.proxies(STORAGED_IFACE_PFX + "." + iface,
                                              STORAGED_OPATH_PFX,
                                              { watch: false });
    }

    function init_proxies () {
        client.storaged_client.watch({ path_namespace: STORAGED_OPATH_PFX });

        client.mdraids = proxies("MDRaid");
        client.vgroups = proxies("VolumeGroup");
        client.lvols = proxies("LogicalVolume");
        client.drives = proxies("Drive");
        client.drives_ata = proxies("Drive.Ata");
        client.blocks = proxies("Block");
        client.blocks_ptable = proxies("PartitionTable");
        client.blocks_part = proxies("Partition");
        client.blocks_lvm2 = proxies("Block.LVM2");
        client.blocks_pvol = proxies("PhysicalVolume");
        client.blocks_fsys = proxies("Filesystem");
        client.blocks_crypto = proxies("Encrypted");
        client.iscsi_sessions = proxies("ISCSI.Session");
        client.storaged_jobs = proxies("Job");

        if (STORAGED_SERVICE != "org.freedesktop.UDisks2") {
            client.udisks_client = cockpit.dbus("org.freedesktop.UDisks2");
            client.udisks_jobs = client.udisks_client.proxies("org.freedesktop.UDisks2.Job",
                                                              "/org/freedesktop/UDisks2");
        } else {
            client.udisks_client = null;
            client.udisks_jobs = { };
        }
    }

    /* Monitors
     */

    client.fsys_sizes = instance_sampler([ { name: "mount.used" },
                                           { name: "mount.total" }
                                         ]);

    client.blockdev_io = instance_sampler([ { name: "block.device.read", derive: "rate" },
                                            { name: "block.device.written", derive: "rate" }
                                          ]);

    /* Derived indices.
     */

    function is_multipath_master(block) {
        // The master has "mpath" in its device mapper UUID.  In the
        // future, storaged will hopefully provide this information
        // directly.
        if (block.Symlinks && block.Symlinks.length) {
            for (var i = 0; i < block.Symlinks.length; i++)
                if (utils.decode_filename(block.Symlinks[i]).indexOf("/dev/disk/by-id/dm-uuid-mpath-") === 0)
                    return true;
        }
        return false;
    }

    function update_indices() {
        var path, block, dev, mdraid, vgroup, pvol, lvol, part, i;

        client.broken_multipath_present = false;
        client.drives_multipath_blocks = { };
        client.drives_block = { };
        for (path in client.drives) {
            client.drives_multipath_blocks[path] = [ ];
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            if (!client.blocks_part[path] && client.drives_multipath_blocks[block.Drive] !== undefined) {
                if (is_multipath_master(block))
                    client.drives_block[block.Drive] = block;
                else
                    client.drives_multipath_blocks[block.Drive].push(block);
            }
        }
        for (path in client.drives_multipath_blocks) {
            /* If there is no multipath master and only a single
             * member, then this is actually a normal singlepath
             * device.
             */

            if (!client.drives_block[path] && client.drives_multipath_blocks[path].length == 1) {
                client.drives_block[path] = client.drives_multipath_blocks[path][0];
                client.drives_multipath_blocks[path] = [ ];
            } else {
                client.drives_multipath_blocks[path].sort(utils.block_cmp);
                if (!client.drives_block[path])
                    client.broken_multipath_present = true;
            }
        }

        client.mdraids_block = { };
        for (path in client.blocks) {
            block = client.blocks[path];
            if (block.MDRaid != "/")
                client.mdraids_block[block.MDRaid] = block;
        }

        client.mdraids_members = { };
        for (path in client.mdraids) {
            client.mdraids_members[path] = [ ];
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            if (client.mdraids_members[block.MDRaidMember] !== undefined)
                client.mdraids_members[block.MDRaidMember].push(block);
        }
        for (path in client.mdraids_members) {
            client.mdraids_members[path].sort(utils.block_cmp);
        }

        client.slashdevs_block = { };
        function enter_slashdev(block, enc) {
            client.slashdevs_block[utils.decode_filename(enc).replace(/^\/dev\//, "")] = block;
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            enter_slashdev(block, block.Device);
            enter_slashdev(block, block.PreferredDevice);
            for (i = 0; i < block.Symlinks.length; i++)
                enter_slashdev(block, block.Symlinks[i]);
        }

        client.uuids_mdraid = { };
        for (path in client.mdraids) {
            mdraid = client.mdraids[path];
            client.uuids_mdraid[mdraid.UUID] = mdraid;
        }

        client.vgnames_vgroup = { };
        for (path in client.vgroups) {
            vgroup = client.vgroups[path];
            client.vgnames_vgroup[vgroup.Name] = vgroup;
        }

        client.vgroups_pvols = { };
        for (path in client.vgroups) {
            client.vgroups_pvols[path] = [ ];
        }
        for (path in client.blocks_pvol) {
            pvol = client.blocks_pvol[path];
            if (client.vgroups_pvols[pvol.VolumeGroup] !== undefined)
                client.vgroups_pvols[pvol.VolumeGroup].push(pvol);
        }
        function cmp_pvols(a, b) {
            return utils.block_cmp(client.blocks[a.path], client.blocks[b.path]);
        }
        for (path in client.vgroups_pvols) {
            client.vgroups_pvols[path].sort(cmp_pvols);
        }

        client.vgroups_lvols = { };
        for (path in client.vgroups) {
            client.vgroups_lvols[path] = [ ];
        }
        for (path in client.lvols) {
            lvol = client.lvols[path];
            if (client.vgroups_lvols[lvol.VolumeGroup] !== undefined)
                client.vgroups_lvols[lvol.VolumeGroup].push(lvol);
        }
        for (path in client.vgroups_lvols) {
            client.vgroups_lvols[path].sort(function (a, b) { return a.Name.localeCompare(b.Name); });
        }

        client.lvols_block = { };
        for (path in client.blocks_lvm2) {
            client.lvols_block[client.blocks_lvm2[path].LogicalVolume] = client.blocks[path];
        }

        client.lvols_pool_members = { };
        for (path in client.lvols) {
            if (client.lvols[path].Type == "pool")
                client.lvols_pool_members[path] = [ ];
        }
        for (path in client.lvols) {
            lvol = client.lvols[path];
            if (client.lvols_pool_members[lvol.ThinPool] !== undefined)
                client.lvols_pool_members[lvol.ThinPool].push(lvol);
        }
        for (path in client.lvols_pool_members) {
            client.lvols_pool_members[path].sort(function (a, b) { return a.Name.localeCompare(b.Name); });
        }

        client.blocks_cleartext = { };
        for (path in client.blocks) {
            block = client.blocks[path];
            if (block.CryptoBackingDevice != "/")
                client.blocks_cleartext[block.CryptoBackingDevice] = block;
        }

        client.blocks_partitions = { };
        for (path in client.blocks_ptable) {
            client.blocks_partitions[path] = [ ];
        }
        for (path in client.blocks_part) {
            part = client.blocks_part[path];
            if (client.blocks_partitions[part.Table] !== undefined)
                client.blocks_partitions[part.Table].push(part);
        }
        for (path in client.blocks_partitions) {
            client.blocks_partitions[path].sort(function (a, b) { return a.Offset - b.Offset; });
        }
    }

    function init_model(callback) {

        function wait_all(objects, callback) {
            var obj = objects.pop();
            if (obj) {
                obj.wait(function () {
                    wait_all(objects, callback);
                });
            } else
                callback();
        }

        wait_all([ client.manager,
                   client.mdraids, client.vgroups, client.drives,
                   client.blocks, client.blocks_ptable, client.blocks_lvm2, client.blocks_fsys
                 ],
                 function () {
                     if (!client.manager.valid) {
                         client.features = false;
                         callback();
                     } else {
                         cockpit.spawn(["date", "+%s"])
                             .done(function (now) {
                                 client.time_offset = parseInt(now)*1000 - new Date().getTime();
                             })
                             .always(function () {
                                 client.manager.EnableModules(true)
                                     .always(function () {
                                         client.manager_lvm2 = proxy("Manager.LVM2", "Manager");
                                         client.manager_iscsi = proxy("Manager.ISCSI.Initiator", "Manager");
                                         wait_all([ client.manager_lvm2, client.manager_iscsi],
                                                  function () {
                                                      var iscsi = (config.with_storaged_iscsi_sessions != "no" &&
                                                                   client.manager_iscsi.valid &&
                                                                   client.manager_iscsi.SessionsSupported !== false);
                                                      client.features = { lvm2: client.manager_lvm2.valid,
                                                                          iscsi: iscsi
                                                                        };
                                                      callback();
                                                  });
                                     })
                                     .fail(function (error) {
                                         console.warn("Can't enable storaged modules", error.toString());
                                     });
                                 $(client.storaged_client).on('notify', function () {
                                     update_indices();
                                     $(client).triggerHandler('changed');
                                 });
                                 $(client.udisks_jobs).on('added removed changed', function () {
                                     $(client).triggerHandler('changed');
                                 });
                                 update_indices();
                             });
                     }
                 });
    }

    function init_storaged(callback) {
        /* Storaged 2.6 and later uses the UDisks2 API names, so we
         * try them first.
         */

        STORAGED_SERVICE =   "org.freedesktop.UDisks2";
        STORAGED_OPATH_PFX = "/org/freedesktop/UDisks2";
        STORAGED_IFACE_PFX = "org.freedesktop.UDisks2";

        client.storaged_client = cockpit.dbus(STORAGED_SERVICE);
        client.manager = proxy("Manager", "Manager");

        client.manager.wait(function () {
            if (!client.manager.valid || client.manager.EnableModules === undefined) {
                STORAGED_SERVICE =   "org.storaged.Storaged";
                STORAGED_OPATH_PFX = "/org/storaged/Storaged";
                STORAGED_IFACE_PFX = "org.storaged.Storaged";

                client.storaged_client = cockpit.dbus(STORAGED_SERVICE);
                client.manager = proxy("Manager", "Manager");
            }

            init_proxies();
            init_model(callback);
        });
    }

    client.init = init_storaged;

    module.exports = client;
}());
<commit_msg>storaged: Fall back to UDisks2 if Storaged isn't available<commit_after>/*
 * This file is part of Cockpit.
 *
 * Copyright (C) 2015 Red Hat, Inc.
 *
 * Cockpit is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation; either version 2.1 of the License, or
 * (at your option) any later version.
 *
 * Cockpit is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
 */

(function() {
    "use strict";

    var $ = require('jquery');
    var cockpit = require('cockpit');

    var utils = require('./utils');

    /* STORAGED CLIENT
     */

    /* HACK: https://github.com/storaged-project/storaged/pull/68 */
    var config = { };
    if (cockpit.manifests["storage"] && cockpit.manifests["storage"]["config"])
        config = cockpit.manifests["storage"]["config"];

    var client = { };

    /* Metrics
     */

    function instance_sampler(metrics) {
        var instances;
        var self = {
            data: { },
            close: close
        };

        function handle_meta(msg) {
            self.data = { };
            instances = [ ];
            for (var m = 0; m < msg.metrics.length; m++) {
                instances[m] = msg.metrics[m].instances;
                for (var i = 0; i < instances[m].length; i++)
                    self.data[instances[m][i]] = [ ];
            }
        }

        function handle_data(msg) {
            var changed = false;
            for (var s = 0; s < msg.length; s++) {
                var metrics = msg[s];
                for (var m = 0; m < metrics.length; m++) {
                    var inst = metrics[m];
                    for (var i = 0; i < inst.length; i++) {
                        if (inst[i] !== null) {
                            changed = true;
                            self.data[instances[m][i]][m] = inst[i];
                        }
                    }
                }
            }
            if (changed)
                $(self).triggerHandler('changed');
        }

        var channel = cockpit.channel({ payload: "metrics1",
                                        source: "internal",
                                        metrics: metrics
                                      });
        $(channel).on("closed", function (event, error) {
            console.log("closed", error);
        });
        $(channel).on("message", function (event, message) {
            var msg = JSON.parse(message);
            if (msg.length)
                handle_data(msg);
            else
                handle_meta(msg);
        });

        function close() {
            channel.close();
        }

        return self;
    }

    /* D-Bus proxies
     */

    var STORAGED_SERVICE;
    var STORAGED_OPATH_PFX;
    var STORAGED_IFACE_PFX;

    client.time_offset = undefined;  /* Number of milliseconds that the server is ahead of us. */
    client.features = undefined;

    client.storaged_client = undefined;

    function proxy(iface, path) {
        return client.storaged_client.proxy(STORAGED_IFACE_PFX + "." + iface,
                                            STORAGED_OPATH_PFX + "/" + path,
                                            { watch: true });
    }

    function proxies(iface) {
        /* We create the proxies here with 'watch' set to false and
         * establish a general watch for all of them.  This is more
         * efficient since it reduces the number of D-Bus calls done
         * by the cache.
         */
        return client.storaged_client.proxies(STORAGED_IFACE_PFX + "." + iface,
                                              STORAGED_OPATH_PFX,
                                              { watch: false });
    }

    function init_proxies () {
        client.storaged_client.watch({ path_namespace: STORAGED_OPATH_PFX });

        client.mdraids = proxies("MDRaid");
        client.vgroups = proxies("VolumeGroup");
        client.lvols = proxies("LogicalVolume");
        client.drives = proxies("Drive");
        client.drives_ata = proxies("Drive.Ata");
        client.blocks = proxies("Block");
        client.blocks_ptable = proxies("PartitionTable");
        client.blocks_part = proxies("Partition");
        client.blocks_lvm2 = proxies("Block.LVM2");
        client.blocks_pvol = proxies("PhysicalVolume");
        client.blocks_fsys = proxies("Filesystem");
        client.blocks_crypto = proxies("Encrypted");
        client.iscsi_sessions = proxies("ISCSI.Session");
        client.storaged_jobs = proxies("Job");

        if (STORAGED_SERVICE != "org.freedesktop.UDisks2") {
            client.udisks_client = cockpit.dbus("org.freedesktop.UDisks2");
            client.udisks_jobs = client.udisks_client.proxies("org.freedesktop.UDisks2.Job",
                                                              "/org/freedesktop/UDisks2");
        } else {
            client.udisks_client = null;
            client.udisks_jobs = { };
        }
    }

    /* Monitors
     */

    client.fsys_sizes = instance_sampler([ { name: "mount.used" },
                                           { name: "mount.total" }
                                         ]);

    client.blockdev_io = instance_sampler([ { name: "block.device.read", derive: "rate" },
                                            { name: "block.device.written", derive: "rate" }
                                          ]);

    /* Derived indices.
     */

    function is_multipath_master(block) {
        // The master has "mpath" in its device mapper UUID.  In the
        // future, storaged will hopefully provide this information
        // directly.
        if (block.Symlinks && block.Symlinks.length) {
            for (var i = 0; i < block.Symlinks.length; i++)
                if (utils.decode_filename(block.Symlinks[i]).indexOf("/dev/disk/by-id/dm-uuid-mpath-") === 0)
                    return true;
        }
        return false;
    }

    function update_indices() {
        var path, block, dev, mdraid, vgroup, pvol, lvol, part, i;

        client.broken_multipath_present = false;
        client.drives_multipath_blocks = { };
        client.drives_block = { };
        for (path in client.drives) {
            client.drives_multipath_blocks[path] = [ ];
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            if (!client.blocks_part[path] && client.drives_multipath_blocks[block.Drive] !== undefined) {
                if (is_multipath_master(block))
                    client.drives_block[block.Drive] = block;
                else
                    client.drives_multipath_blocks[block.Drive].push(block);
            }
        }
        for (path in client.drives_multipath_blocks) {
            /* If there is no multipath master and only a single
             * member, then this is actually a normal singlepath
             * device.
             */

            if (!client.drives_block[path] && client.drives_multipath_blocks[path].length == 1) {
                client.drives_block[path] = client.drives_multipath_blocks[path][0];
                client.drives_multipath_blocks[path] = [ ];
            } else {
                client.drives_multipath_blocks[path].sort(utils.block_cmp);
                if (!client.drives_block[path])
                    client.broken_multipath_present = true;
            }
        }

        client.mdraids_block = { };
        for (path in client.blocks) {
            block = client.blocks[path];
            if (block.MDRaid != "/")
                client.mdraids_block[block.MDRaid] = block;
        }

        client.mdraids_members = { };
        for (path in client.mdraids) {
            client.mdraids_members[path] = [ ];
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            if (client.mdraids_members[block.MDRaidMember] !== undefined)
                client.mdraids_members[block.MDRaidMember].push(block);
        }
        for (path in client.mdraids_members) {
            client.mdraids_members[path].sort(utils.block_cmp);
        }

        client.slashdevs_block = { };
        function enter_slashdev(block, enc) {
            client.slashdevs_block[utils.decode_filename(enc).replace(/^\/dev\//, "")] = block;
        }
        for (path in client.blocks) {
            block = client.blocks[path];
            enter_slashdev(block, block.Device);
            enter_slashdev(block, block.PreferredDevice);
            for (i = 0; i < block.Symlinks.length; i++)
                enter_slashdev(block, block.Symlinks[i]);
        }

        client.uuids_mdraid = { };
        for (path in client.mdraids) {
            mdraid = client.mdraids[path];
            client.uuids_mdraid[mdraid.UUID] = mdraid;
        }

        client.vgnames_vgroup = { };
        for (path in client.vgroups) {
            vgroup = client.vgroups[path];
            client.vgnames_vgroup[vgroup.Name] = vgroup;
        }

        client.vgroups_pvols = { };
        for (path in client.vgroups) {
            client.vgroups_pvols[path] = [ ];
        }
        for (path in client.blocks_pvol) {
            pvol = client.blocks_pvol[path];
            if (client.vgroups_pvols[pvol.VolumeGroup] !== undefined)
                client.vgroups_pvols[pvol.VolumeGroup].push(pvol);
        }
        function cmp_pvols(a, b) {
            return utils.block_cmp(client.blocks[a.path], client.blocks[b.path]);
        }
        for (path in client.vgroups_pvols) {
            client.vgroups_pvols[path].sort(cmp_pvols);
        }

        client.vgroups_lvols = { };
        for (path in client.vgroups) {
            client.vgroups_lvols[path] = [ ];
        }
        for (path in client.lvols) {
            lvol = client.lvols[path];
            if (client.vgroups_lvols[lvol.VolumeGroup] !== undefined)
                client.vgroups_lvols[lvol.VolumeGroup].push(lvol);
        }
        for (path in client.vgroups_lvols) {
            client.vgroups_lvols[path].sort(function (a, b) { return a.Name.localeCompare(b.Name); });
        }

        client.lvols_block = { };
        for (path in client.blocks_lvm2) {
            client.lvols_block[client.blocks_lvm2[path].LogicalVolume] = client.blocks[path];
        }

        client.lvols_pool_members = { };
        for (path in client.lvols) {
            if (client.lvols[path].Type == "pool")
                client.lvols_pool_members[path] = [ ];
        }
        for (path in client.lvols) {
            lvol = client.lvols[path];
            if (client.lvols_pool_members[lvol.ThinPool] !== undefined)
                client.lvols_pool_members[lvol.ThinPool].push(lvol);
        }
        for (path in client.lvols_pool_members) {
            client.lvols_pool_members[path].sort(function (a, b) { return a.Name.localeCompare(b.Name); });
        }

        client.blocks_cleartext = { };
        for (path in client.blocks) {
            block = client.blocks[path];
            if (block.CryptoBackingDevice != "/")
                client.blocks_cleartext[block.CryptoBackingDevice] = block;
        }

        client.blocks_partitions = { };
        for (path in client.blocks_ptable) {
            client.blocks_partitions[path] = [ ];
        }
        for (path in client.blocks_part) {
            part = client.blocks_part[path];
            if (client.blocks_partitions[part.Table] !== undefined)
                client.blocks_partitions[part.Table].push(part);
        }
        for (path in client.blocks_partitions) {
            client.blocks_partitions[path].sort(function (a, b) { return a.Offset - b.Offset; });
        }
    }

    function init_model(callback) {

        function wait_all(objects, callback) {
            var obj = objects.pop();
            if (obj) {
                obj.wait(function () {
                    wait_all(objects, callback);
                });
            } else {
                callback();
            }
        }

        function pull_time() {
            return cockpit.spawn(["date", "+%s"])
                .then(function (now) {
                    client.time_offset = parseInt(now)*1000 - new Date().getTime();
                });
        }

        function enable_features() {
            if (!client.manager.valid)
                return cockpit.resolve(false);
            if (!client.manager.EnableModules)
                return cockpit.resolve({ });
            return client.manager.EnableModules(true).then(
                function() {
                    var defer = cockpit.defer();
                    client.manager_lvm2 = proxy("Manager.LVM2", "Manager");
                    client.manager_iscsi = proxy("Manager.ISCSI.Initiator", "Manager");
                    wait_all([ client.manager_lvm2, client.manager_iscsi],
                            function () {
                                var iscsi = (config.with_storaged_iscsi_sessions != "no" &&
                                        client.manager_iscsi.valid &&
                                        client.manager_iscsi.SessionsSupported !== false);
                                defer.resolve({ lvm2: client.manager_lvm2.valid, iscsi: iscsi });
                            });
                    return defer.promise;
                }, function(error) {
                    console.warn("Can't enable storaged modules", error.toString());
                    return cockpit.resolve({ });
                });
        }

        wait_all([ client.manager,
                   client.mdraids, client.vgroups, client.drives,
                   client.blocks, client.blocks_ptable, client.blocks_lvm2, client.blocks_fsys
                 ], function () {
            pull_time().then(function() {
                enable_features().then(function(features) {
                    client.features = features;
                    callback();
                });

                $(client.storaged_client).on('notify', function () {
                    update_indices();
                    $(client).triggerHandler('changed');
                });
                $(client.udisks_jobs).on('added removed changed', function () {
                    $(client).triggerHandler('changed');
                });
                update_indices();
            });
        });
    }

    client.older_than = function older_than(version) {
        return utils.compare_versions(this.manager.Version, version) < 0;
    };

    function init_manager() {
        /* Storaged 2.6 and later uses the UDisks2 API names, but try the
         * older storaged API first as a fallback.
         */

        var storaged_service = "org.storaged.Storaged";
        var storaged_opath_pfx = "/org/storaged/Storaged";
        var storaged_iface_pfx = "org.storaged.Storaged";

        var storaged = cockpit.dbus(storaged_service);
        var storaged_manager = storaged.proxy(storaged_iface_pfx + ".Manager",
                storaged_opath_pfx + "/Manager", { watch: true });

        function fallback_udisks() {
            STORAGED_SERVICE = "org.freedesktop.UDisks2";
            STORAGED_OPATH_PFX = "/org/freedesktop/UDisks2";
            STORAGED_IFACE_PFX = "org.freedesktop.UDisks2";

            var udisks = cockpit.dbus(STORAGED_SERVICE);
            var udisks_manager = udisks.proxy(STORAGED_IFACE_PFX + ".Manager",
                    STORAGED_OPATH_PFX + "/Manager", { watch: true });

            return udisks_manager.wait().then(function () {
                return udisks_manager;
            });
        }

        return storaged_manager.wait().then(function() {
            if (storaged_manager.valid) {
                console.log("Using older 'storaged' API: " + storaged_service);
                STORAGED_SERVICE = storaged_service;
                STORAGED_OPATH_PFX = storaged_opath_pfx;
                STORAGED_IFACE_PFX = storaged_iface_pfx;
                return storaged_manager;
            } else {
                return fallback_udisks();
            }
        }, fallback_udisks);
    }

    client.init = function init_storaged(callback) {
        init_manager().then(function(manager) {
            client.storaged_client = manager.client;
            client.manager = manager;

            // The first storaged version with the UDisks2 API names was 2.6
            client.is_old_udisks2 = (STORAGED_SERVICE == "org.freedesktop.UDisks2" && client.older_than("2.6"));
            if (client.is_old_udisks2)
                console.log("Using older 'udisks2' implementation: " + manager.Version);

            init_proxies();
            init_model(callback);
        }, function() {
            client.features = false;
            callback();
        });
    };

    module.exports = client;
}());
<|endoftext|>


<commit_before>$(document).ready(function () {
    var dropzoneMainText = $("#uploader .dz-message.needsclick");
    //Creating the list of images and users to be passed into the controller
    var listOfImages = [];
    var listOfUsers = [];
    var listOfDeleted = [];
    //Creates an Image object that is added to the list of Images
    $(document).on("click", "#add-img", function () {
        var title = $("#Image_Title").val();
        var description = $("#Image_Description_UserInput").val();
        var content = $("#b64uploader").val();
        if (title.length !== 0 && content.length !== 0) {
            if (title.length < 5) {
                alert("Screen title must have at least 5 characters!")
            }
            else{
                $("#uploader").css("width", "100%");
                $(this).removeClass("ready");
                var contentCol = listOfImages.map(v => v.Content);
                if ($.inArray(content, contentCol) > -1) {
                    alert("This image is already added");
                    myDropzone.removeAllFiles();
                 $("#uploader").html("").append(dropzoneMainText);
                 $(dropzoneMainText).show();
                }
                 else {
                     var image = {
                        "Title": title,
                        "Description": description,
                        "Content": content
                 };
                listOfImages.push(image);

                $("#Image_Title").val("").html("");
                $("#Image_Description_UserInput").val("");
                $("#b64uploader").val("");
                myDropzone.removeAllFiles();
                $("#uploader").html("").append(dropzoneMainText);
                $(dropzoneMainText).show();
                appendThumbnail(listOfImages.length - 1, content);
            }
        }

        }
        else {
            alert("You must add a screen and a title!")
        }
    })
    $("#create-screens").click(function () {
        if ($("#project-title").val() === "") {
            alert("Add a project title first!")
        }
        else{
            $(this).attr("id", "create-screens");
            $("#project-info").fadeOut(300);
            $("#review-title-header").fadeOut(300);
            setTimeout(function () {
                $("#reviewer-project-title").html($("#project-title").val());
                $("#reviewer-project-description").html($("#project-description").val());
                $("#review-title-header").html($("#project-title").val());
            }, 200)

            $("#review-title-header").fadeIn(300);
            $(".main-edit").delay(300).fadeIn(300);
            $(".image-upload-container").delay(300).fadeIn(300);

        }
    })
    function appendThumbnail(index, content) {

        var containerMain = $(document.createElement('div')).addClass("col-xs-4 col-md-2").attr('id', "image-" + index);
        var containerReview = $(document.createElement('div')).addClass("col-xs-3 col-md-3").attr('id', "image-rev-" + index);
        var deleteScreen = $(document.createElement('a')).addClass("delete-screen").html("");
        var image = $(document.createElement('img')).attr('src', content);
        var innerContainer = $(document.createElement('div')).addClass("col-xs-12 col-md-12 thumbnail-inner");
        $(image).appendTo(innerContainer);
        $(innerContainer).appendTo(containerReview);
        var mainInnerContainer = $(innerContainer).clone().append(deleteScreen).appendTo(containerMain);
        $(containerMain).appendTo("#thumbnail-container");
        $(containerReview).appendTo("#review-thumbnail-container");
        $(mainInnerContainer).click(function () {
            if (!$(event.target).closest('.delete-screen').length) {
                $("#uploader img").remove();
                $("#add-img").removeClass("ready");
                $("#edit-img").slideUp();
                $("#uploader").html("");
                $("#Image_Title").html("").val(listOfImages[index].Title);
                $("#Image_Description_UserInput").val(listOfImages[index].Description);
                $("#b64uploader").val(listOfImages[index].Content);
                var myImage = {
                    name: listOfImages[index].Title
                }
                $("#Image_Title").change(function () {
                    currentImage = index;
                    $("#edit-img").slideDown();
                })
                $("#Image_Description_UserInput").change(function () {
                    currentImage = index;
                    $("#edit-img").slideDown();
                })
                myDropzone.emit("addedfile", myImage);
                myDropzone.emit("thumbnail", myImage, listOfImages[index].Content);
                myDropzone.files.push(myImage);
                $("#add-img").removeClass("ready");
            }

        })

        $(deleteScreen).click(function (e) {
            if (listOfImages[index].Content === $("#b64uploader").val()) {
                $("#add-img").addClass("ready");
            }
            listOfImages[index].Content = "";
            $("#image-" + index).detach();
            $("#image-rev-" + index).detach();
        })
    }

    $("#edit-img").click(function () {
        listOfImages[currentImage].Title = $("#Image_Title").val();
        listOfImages[currentImage].Description = $("#Image_Description_UserInput").val();
        $("#Image_Title").off();
        $("#Image_Description_UserInput").off();
        $(this).slideUp();
    })
    $("#create-reviewer").click(function () {
        if ($("#b64uploader").val() !== "") {
            var contentCol = listOfImages.map(v => v.Content);
            if ($.inArray($("#b64uploader").val(), contentCol) > -1) {
                myDropzone.removeAllFiles();
                $("#uploader").html(dropzoneMainText).css("width", "100%");
                $("#Image_Title").val("");
                $("#Image_Description_UserInput").val("");
                $("#add-img").removeClass("ready");
            }
            else {
                if ($("#Image_Title").val()=="") {
                    $("#Image_Title").val("Screen " + listOfImages.length + 1);
                }
                $("#add-img").click();
            }

        }

        $(".add-reviewers-overlay").css("display", "flex").hide().fadeIn(400);
    })
    //Subtitution for .stopPropogation()
    $(".add-reviewers-overlay").click(function (event) {
        if (event.defaultPrevented) return;
        if (!$(event.target).closest('.panel.panel-default').length) {
            $(this).fadeOut(200);
        }
    })
    $("#submit-images").click(function () {
        $(this).prop("disabled", true);
        var finalScreenList = [];
        $(listOfImages).each(function (index) {
            if (this.Content !== "") {
                finalScreenList.push(this);
            }
        })

        //Get the details of the users who were checked as reviewers NEEDS TO BE UPDATED
        $(".reviewer-info input[type='checkbox']").each(function (index) {
            if ($(this).is(":checked")) {

                var userId = $("#Reviewers_" + index + "__Id").val();
                var profileName = $("#profilename-" + index + " label").html();
                var email = $("#email-" + index +" label").html();
                var user = {
                    "Id": userId,
                    "ProfileName": profileName,
                    "Email": email
                };
                listOfUsers.push(user);
            }

        })

        var CreatePageModel = {
            Screens: listOfImages,
            Reviewers: listOfUsers,
            ReviewTitle: $("#project-title").val(),
            ReviewDescription: $("#project-description").val(),
            ReviewThumbnail : $("#b64uploaderThumb").val()
        }
        var data={
            __RequestVerificationToken: $('[name= "__RequestVerificationToken"]').val(),
            CreatePageModel: CreatePageModel
        }
        $.ajax(
            {
                type: "POST", //HTTP POST Method
                url: location.pathname, // Controller
                data: data,
                success: function (url) {
                    window.location.href = url;
                }
            });
    })

    $(document).on("click", ".edit-project", function (e) {
        $(".main-edit").fadeOut(200);
        $(".add-reviewers-overlay").fadeOut(200);
        $(".image-upload-container").fadeOut(200);
        $("#project-info").delay(200).fadeIn(200);

    })
    $("#reset").click(function () {
        currentImage = -1;
        $("#edit-img").hide();
        myDropzone.removeAllFiles();
        $("#Image_Title").val('').off();
        $("#Image_Description_UserInput").val('').off();
        $("#add-img").removeClass("ready");
        $("#uploader").html(dropzoneMainText).animate({ width: "100%" }, 300)
    })
})
$("#check-all").change(function () {
    var checkBoxes = $(".add-reviewers-overlay .reviewer-info input[type='checkbox']");
    checkBoxes.prop("checked", !checkBoxes.prop("checked"));
})<commit_msg>No Review Title fix.<commit_after>$(document).ready(function () {
    var dropzoneMainText = $("#uploader .dz-message.needsclick");
    //Creating the list of images and users to be passed into the controller
    var listOfImages = [];
    var listOfUsers = [];
    var listOfDeleted = [];
    //Creates an Image object that is added to the list of Images
    $(document).on("click", "#add-img", function () {
        var title = $("#Image_Title").val();
        var description = $("#Image_Description_UserInput").val();
        var content = $("#b64uploader").val();
        if (title.length !== 0 && content.length !== 0) {
            if (title.length < 5) {
                alert("Screen title must have at least 5 characters!")
            }
            else{
                $("#uploader").css("width", "100%");
                $(this).removeClass("ready");
                var contentCol = listOfImages.map(v => v.Content);
                if ($.inArray(content, contentCol) > -1) {
                    alert("This image is already added");
                    myDropzone.removeAllFiles();
                 $("#uploader").html("").append(dropzoneMainText);
                 $(dropzoneMainText).show();
                }
                 else {
                     var image = {
                        "Title": title,
                        "Description": description,
                        "Content": content
                 };
                listOfImages.push(image);

                $("#Image_Title").val("").html("");
                $("#Image_Description_UserInput").val("");
                $("#b64uploader").val("");
                myDropzone.removeAllFiles();
                $("#uploader").html("").append(dropzoneMainText);
                $(dropzoneMainText).show();
                appendThumbnail(listOfImages.length - 1, content);
            }
        }

        }
        else {
            alert("You must add a screen and a title!")
        }
    })
    $("#create-screens").click(function () {

        if ($("#project-title").val() === "") {
            alert("Review title is empty! Defaulting to 'No Title'.");
            $("#project-title").val('No Title');
        }

        $(this).attr("id", "create-screens");
        $("#project-info").fadeOut(300);
        $("#review-title-header").fadeOut(300);
        setTimeout(function () {
            $("#reviewer-project-title").html($("#project-title").val());
            $("#reviewer-project-description").html($("#project-description").val());
            $("#review-title-header").html($("#project-title").val());
        }, 200)

        $("#review-title-header").fadeIn(300);
        $(".main-edit").delay(300).fadeIn(300);
        $(".image-upload-container").delay(300).fadeIn(300);

    })
    function appendThumbnail(index, content) {

        var containerMain = $(document.createElement('div')).addClass("col-xs-4 col-md-2").attr('id', "image-" + index);
        var containerReview = $(document.createElement('div')).addClass("col-xs-3 col-md-3").attr('id', "image-rev-" + index);
        var deleteScreen = $(document.createElement('a')).addClass("delete-screen").html("");
        var image = $(document.createElement('img')).attr('src', content);
        var innerContainer = $(document.createElement('div')).addClass("col-xs-12 col-md-12 thumbnail-inner");
        $(image).appendTo(innerContainer);
        $(innerContainer).appendTo(containerReview);
        var mainInnerContainer = $(innerContainer).clone().append(deleteScreen).appendTo(containerMain);
        $(containerMain).appendTo("#thumbnail-container");
        $(containerReview).appendTo("#review-thumbnail-container");
        $(mainInnerContainer).click(function () {
            if (!$(event.target).closest('.delete-screen').length) {
                $("#uploader img").remove();
                $("#add-img").removeClass("ready");
                $("#edit-img").slideUp();
                $("#uploader").html("");
                $("#Image_Title").html("").val(listOfImages[index].Title);
                $("#Image_Description_UserInput").val(listOfImages[index].Description);
                $("#b64uploader").val(listOfImages[index].Content);
                var myImage = {
                    name: listOfImages[index].Title
                }
                $("#Image_Title").change(function () {
                    currentImage = index;
                    $("#edit-img").slideDown();
                })
                $("#Image_Description_UserInput").change(function () {
                    currentImage = index;
                    $("#edit-img").slideDown();
                })
                myDropzone.emit("addedfile", myImage);
                myDropzone.emit("thumbnail", myImage, listOfImages[index].Content);
                myDropzone.files.push(myImage);
                $("#add-img").removeClass("ready");
            }

        })

        $(deleteScreen).click(function (e) {
            if (listOfImages[index].Content === $("#b64uploader").val()) {
                $("#add-img").addClass("ready");
            }
            listOfImages[index].Content = "";
            $("#image-" + index).detach();
            $("#image-rev-" + index).detach();
        })
    }

    $("#edit-img").click(function () {
        listOfImages[currentImage].Title = $("#Image_Title").val();
        listOfImages[currentImage].Description = $("#Image_Description_UserInput").val();
        $("#Image_Title").off();
        $("#Image_Description_UserInput").off();
        $(this).slideUp();
    })
    $("#create-reviewer").click(function () {
        if ($("#b64uploader").val() !== "") {
            var contentCol = listOfImages.map(v => v.Content);
            if ($.inArray($("#b64uploader").val(), contentCol) > -1) {
                myDropzone.removeAllFiles();
                $("#uploader").html(dropzoneMainText).css("width", "100%");
                $("#Image_Title").val("");
                $("#Image_Description_UserInput").val("");
                $("#add-img").removeClass("ready");
            }
            else {
                if ($("#Image_Title").val()=="") {
                    $("#Image_Title").val("Screen " + listOfImages.length + 1);
                }
                $("#add-img").click();
            }

        }

        $(".add-reviewers-overlay").css("display", "flex").hide().fadeIn(400);
    })
    //Subtitution for .stopPropogation()
    $(".add-reviewers-overlay").click(function (event) {
        if (event.defaultPrevented) return;
        if (!$(event.target).closest('.panel.panel-default').length) {
            $(this).fadeOut(200);
        }
    })
    $("#submit-images").click(function () {
        $(this).prop("disabled", true);
        var finalScreenList = [];
        $(listOfImages).each(function (index) {
            if (this.Content !== "") {
                finalScreenList.push(this);
            }
        })

        //Get the details of the users who were checked as reviewers NEEDS TO BE UPDATED
        $(".reviewer-info input[type='checkbox']").each(function (index) {
            if ($(this).is(":checked")) {

                var userId = $("#Reviewers_" + index + "__Id").val();
                var profileName = $("#profilename-" + index + " label").html();
                var email = $("#email-" + index +" label").html();
                var user = {
                    "Id": userId,
                    "ProfileName": profileName,
                    "Email": email
                };
                listOfUsers.push(user);
            }

        })

        var CreatePageModel = {
            Screens: listOfImages,
            Reviewers: listOfUsers,
            ReviewTitle: $("#project-title").val(),
            ReviewDescription: $("#project-description").val(),
            ReviewThumbnail : $("#b64uploaderThumb").val()
        }
        var data={
            __RequestVerificationToken: $('[name= "__RequestVerificationToken"]').val(),
            CreatePageModel: CreatePageModel
        }
        $.ajax(
            {
                type: "POST", //HTTP POST Method
                url: location.pathname, // Controller
                data: data,
                success: function (url) {
                    window.location.href = url;
                }
            });
    })

    $(document).on("click", ".edit-project", function (e) {
        $(".main-edit").fadeOut(200);
        $(".add-reviewers-overlay").fadeOut(200);
        $(".image-upload-container").fadeOut(200);
        $("#project-info").delay(200).fadeIn(200);

    })
    $("#reset").click(function () {
        currentImage = -1;
        $("#edit-img").hide();
        myDropzone.removeAllFiles();
        $("#Image_Title").val('').off();
        $("#Image_Description_UserInput").val('').off();
        $("#add-img").removeClass("ready");
        $("#uploader").html(dropzoneMainText).animate({ width: "100%" }, 300)
    })
})
$("#check-all").change(function () {
    var checkBoxes = $(".add-reviewers-overlay .reviewer-info input[type='checkbox']");
    checkBoxes.prop("checked", !checkBoxes.prop("checked"));
})<|endoftext|>


<commit_before>require('enyo');

/**
* Contains the declaration for the {@link module:enyo/RichText~RichText} kind.
* @module enyo/RichText
*/

var
	kind = require('../kind'),
	utils = require('../utils'),
	platform = require('../platform');

var
	Control = require('../Control'),
	Input = require('../Input');

/**
* The type of change to apply. Possible values are `'move'` and `'extend'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyType
*/

/**
* The direction in which to apply the change. Possible values include: `'forward'`,
* `'backward'`, `'left'`, and `'right'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyDirection
*/

/**
* The granularity of the change. Possible values include: `'character'`, `'word'`,
* `'sentence'`, `'line'`, `'paragraph'`, `'lineboundary'`, `'sentenceboundary'`,
* `'paragraphboundary'`, and `'documentboundary'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyAmount
*/

/**
* {@link module:enyo/RichText~RichText} is a multi-line text [input]{@link module:enyo/Input~Input} that supports rich
* formatting, such as bold, italics, and underlining.
*
* The content displayed in a RichText may be accessed at runtime via `get('value')`
* and `set('value')`.
*
* For more information, see the documentation on
* [Text Fields]{@linkplain $dev-guide/building-apps/controls/text-fields.html}
* in the Enyo Developer Guide.
*
* @class RichText
* @extends module:enyo/Input~Input
* @ui
* @public
*/
var RichText = module.exports = kind(
	/** @lends module:enyo/RichText~RichText.prototype */ {

	/**
	* @private
	*/
	name: 'enyo.RichText',

	/**
	* @private
	*/
	kind: Input,

	/**
	* @private
	*/
	classes: 'enyo-richtext enyo-selectable',

	/**
	* @private
	*/
	published:
		/** @lends module:enyo/RichText~RichText.prototype */ {

		/**
		* This flag is enabled by default in {@link module:enyo/RichText~RichText} to take advantage
		* of all the rich editing properties. However, this allows for **any** HTML to be
		* inserted into the RichText, including [<iframe>]{@glossary iframe} and
		* [<script>]{@glossary script} tags, which can be a security concern in
		* some situations. If set to `false`, any inserted HTML will be escaped.
		*
		* @type {Boolean}
		* @default true
		* @public
		*/
		allowHtml: true,

		/**
		* If `true`, the [RichText]{@link module:enyo/RichText~RichText} will not accept input or generate
		* [events]{@glossary event}.
		*
		* @type {Boolean}
		* @default false
		* @public
		*/
		disabled: false,

		/**
		* Value of the text field.
		*
		* @type {String}
		* @default ''
		* @public
		*/
		value: ''
	},

	/**
	* Set to `true` to focus this [control]{@link module:enyo/Control~Control} when it is rendered.
	*
	* @type {Boolean}
	* @default false
	* @public
	*/
	defaultFocus: false,

	/**
	* @private
	*/
	protectedStatics: {
		osInfo: [
			{os: 'android', version: 3},
			{os: 'ios', version: 5}
		],
		//* Returns true if the platform has contenteditable attribute.
		hasContentEditable: function() {
			for (var i=0, t; (t=RichText.osInfo[i]); i++) {
				if (platform[t.os] < t.version) {
					return false;
				}
			}
			return true;
		}
	},

	/**
	* @private
	*/
	attributes: {
		contenteditable: true
	},

	/**
	* @private
	*/
	handlers: {
		onfocus: 'focusHandler',
		onblur: 'blurHandler',
		onkeyup: 'updateValue',
		oncut: 'updateValueAsync',
		onpaste: 'updateValueAsync',
		// prevent oninput handler from being called lower in the inheritance chain
		oninput: null
	},

	/**
	* Creates [RichText]{@link module:enyo/RichText~RichText} as a `<div>` if the platform has the
	* `contenteditable` attribute; otherwise, creates it as a `<textarea>`.
	*
	* @method
	* @private
	*/
	create: kind.inherit(function (sup) {
		return function() {
			this.setTag(RichText.hasContentEditable()?'div':'textarea');
			sup.apply(this, arguments);
			this.disabledChanged();
		};
	}),

	/**
	* @private
	*/
	focusHandler: function () {
		this._value = this.get('value');
	},
	/**
	* Simulates [onchange]{@link module:enyo/Input~Input#onchange} {@glossary event}
	* exposed by [inputs]{@link module:enyo/Input~Input}.
	*
	* @fires module:enyo/Input~Input#onchange
	* @private
	*/
	blurHandler: function () {
		if (this._value !== this.get('value')) {
			this.bubble('onchange');
		}
	},
	/**
	* @private
	*/
	valueChanged: function () {
		var val = this.get('value');
		if (this.hasFocus() && val !== this.node.innerHTML) {
			this.selectAll();
			this.insertAtCursor(val);
		} else if(!this.hasFocus()) {
			this.set('content', val);
		}
		this.detectTextDirectionality((this.value || this.value === 0) ? this.value : '');
	},
	/**
	* @private
	*/
	disabledChanged: function () {
		if(this.tag === 'div') {
			this.setAttribute('contenteditable', this.disabled ? null : 'true');
		} else {
			this.setAttribute('disabled', this.disabled);
		}
		this.bubble('onDisabledChange');
	},
	/**
	* @private
	*/
	updateValue: function () {
		var val = this.node.innerHTML;
		this.set('value', val);
	},
	/**
	* @private
	*/
	updateValueAsync: function () {
		utils.asyncMethod(this.bindSafely('updateValue'));
	},

	/**
	* Determines whether this [control]{@link module:enyo/Control~Control} has focus.
	*
	* @returns {Boolean} `true` if the [RichText]{@link module:enyo/RichText~RichText} is focused;
	* otherwise, `false`.
	* @public
	*/
	hasFocus: function () {
		if (this.hasNode()) {
			return document.activeElement === this.node;
		}
	},
	/**
	* Retrieves the current [selection]{@glossary Selection} from the
	* [RichText]{@link module:enyo/RichText~RichText}.
	*
	* @returns {Selection} The [selection]{@glossary Selection} [object]{@glossary Object}.
	* @public
	*/
	getSelection: function () {
		if (this.hasFocus()) {
			return global.getSelection();
		}
	},

	/**
	* Removes the [selection]{@glossary Selection} [object]{@glossary Object}.
	*
	* @param {Boolean} start - If `true`, the [selection]{@glossary Selection} is
	*	[collapsed to the start]{@glossary Selection.collapseToStart} of the
	*	[range]{@glossary Range}; otherwise, it is
	*	[collapsed to the end]{@glossary Selection.collapseToEnd} of the range.
	* @public
	*/
	removeSelection: function (start) {
		var s = this.getSelection();
		if (s) {
			s[start ? 'collapseToStart' : 'collapseToEnd']();
		}
	},

	/**
	* Modifies the [selection]{@glossary Selection} [object]{@glossary Object}. Please
	* see the [Selection.modify]{@glossary Selection.modify} API for more information.
	*
	* @param {module:enyo/RichText~ModifyType} type - The type of change to apply.
	* @param {module:enyo/RichText~ModifyDirection} dir - The direction in which to apply the change.
	* @param {module:enyo/RichText~ModifyAmount} amount - The granularity of the change.
	* @public
	*/
	modifySelection: function (type, dir, amount) {
		var s = this.getSelection();
		if (s) {
			s.modify(type || 'move', dir, amount);
		}
	},

	/**
	* Moves the cursor according to the [Editing API]{@glossary Selection.modify}.
	*
	* @param {module:enyo/RichText~ModifyDirection} dir - The direction in which to apply the change.
	* @param {module:enyo/RichText~ModifyAmount} amount - The granularity of the change.
	* @public
	*/
	moveCursor: function (dir, amount) {
		this.modifySelection('move', dir, amount);
	},

	/**
	* Moves the cursor to end of text field.
	*
	* @public
	*/
	moveCursorToEnd: function () {
		this.moveCursor('forward', 'documentboundary');
	},

	/**
	* Moves the cursor to start of text field.
	*
	* @public
	*/
	moveCursorToStart: function () {
		this.moveCursor('backward', 'documentboundary');
	},

	/**
	* Selects all content in text field.
	*
	* @public
	*/
	selectAll: function () {
		if (this.hasFocus()) {
			document.execCommand('selectAll');
		}
	},

	/**
	* Inserts HTML at the cursor position. HTML will be escaped unless the
	* [allowHtml]{@link module:enyo/RichText~RichText#allowHtml} property is `true`.
	*
	* @param {String} val - The HTML to insert at the current cursor position.
	* @public
	*/
	insertAtCursor: function (val) {
		if (this.hasFocus()) {
			var v = this.allowHtml ? val : Control.escapeHtml(val).replace(/\n/g, '
');
			document.execCommand('insertHTML', false, v);
		}
	},

	// Accessibility

	/**
	* @private
	*/
	ariaObservers: [
		{to: 'aria-multiline', value: true}
	]
});
<commit_msg>ENYO-3174: Deprecate unstable APIs.<commit_after>require('enyo');

/**
* Contains the declaration for the {@link module:enyo/RichText~RichText} kind.
* @module enyo/RichText
*/

var
	kind = require('../kind'),
	utils = require('../utils'),
	platform = require('../platform');

var
	Control = require('../Control'),
	Input = require('../Input');

/**
* The type of change to apply. Possible values are `'move'` and `'extend'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyType
*/

/**
* The direction in which to apply the change. Possible values include: `'forward'`,
* `'backward'`, `'left'`, and `'right'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyDirection
*/

/**
* The granularity of the change. Possible values include: `'character'`, `'word'`,
* `'sentence'`, `'line'`, `'paragraph'`, `'lineboundary'`, `'sentenceboundary'`,
* `'paragraphboundary'`, and `'documentboundary'`.
*
* @typedef {String} module:enyo/RichText~RichText~ModifyAmount
*/

/**
* {@link module:enyo/RichText~RichText} is a multi-line text [input]{@link module:enyo/Input~Input} that supports rich
* formatting, such as bold, italics, and underlining.
*
* The content displayed in a RichText may be accessed at runtime via `get('value')`
* and `set('value')`.
*
* For more information, see the documentation on
* [Text Fields]{@linkplain $dev-guide/building-apps/controls/text-fields.html}
* in the Enyo Developer Guide.
*
* @class RichText
* @extends module:enyo/Input~Input
* @ui
* @public
*/
var RichText = module.exports = kind(
	/** @lends module:enyo/RichText~RichText.prototype */ {

	/**
	* @private
	*/
	name: 'enyo.RichText',

	/**
	* @private
	*/
	kind: Input,

	/**
	* @private
	*/
	classes: 'enyo-richtext enyo-selectable',

	/**
	* @private
	*/
	published:
		/** @lends module:enyo/RichText~RichText.prototype */ {

		/**
		* This flag is enabled by default in {@link module:enyo/RichText~RichText} to take advantage
		* of all the rich editing properties. However, this allows for **any** HTML to be
		* inserted into the RichText, including [<iframe>]{@glossary iframe} and
		* [<script>]{@glossary script} tags, which can be a security concern in
		* some situations. If set to `false`, any inserted HTML will be escaped.
		*
		* @type {Boolean}
		* @default true
		* @public
		*/
		allowHtml: true,

		/**
		* If `true`, the [RichText]{@link module:enyo/RichText~RichText} will not accept input or generate
		* [events]{@glossary event}.
		*
		* @type {Boolean}
		* @default false
		* @public
		*/
		disabled: false,

		/**
		* Value of the text field.
		*
		* @type {String}
		* @default ''
		* @public
		*/
		value: ''
	},

	/**
	* Set to `true` to focus this [control]{@link module:enyo/Control~Control} when it is rendered.
	*
	* @type {Boolean}
	* @default false
	* @public
	*/
	defaultFocus: false,

	/**
	* @private
	*/
	protectedStatics: {
		osInfo: [
			{os: 'android', version: 3},
			{os: 'ios', version: 5}
		],
		//* Returns true if the platform has contenteditable attribute.
		hasContentEditable: function() {
			for (var i=0, t; (t=RichText.osInfo[i]); i++) {
				if (platform[t.os] < t.version) {
					return false;
				}
			}
			return true;
		}
	},

	/**
	* @private
	*/
	attributes: {
		contenteditable: true
	},

	/**
	* @private
	*/
	handlers: {
		onfocus: 'focusHandler',
		onblur: 'blurHandler',
		onkeyup: 'updateValue',
		oncut: 'updateValueAsync',
		onpaste: 'updateValueAsync',
		// prevent oninput handler from being called lower in the inheritance chain
		oninput: null
	},

	/**
	* Creates [RichText]{@link module:enyo/RichText~RichText} as a `<div>` if the platform has the
	* `contenteditable` attribute; otherwise, creates it as a `<textarea>`.
	*
	* @method
	* @private
	*/
	create: kind.inherit(function (sup) {
		return function() {
			this.setTag(RichText.hasContentEditable()?'div':'textarea');
			sup.apply(this, arguments);
			this.disabledChanged();
		};
	}),

	/**
	* @private
	*/
	focusHandler: function () {
		this._value = this.get('value');
	},
	/**
	* Simulates [onchange]{@link module:enyo/Input~Input#onchange} {@glossary event}
	* exposed by [inputs]{@link module:enyo/Input~Input}.
	*
	* @fires module:enyo/Input~Input#onchange
	* @private
	*/
	blurHandler: function () {
		if (this._value !== this.get('value')) {
			this.bubble('onchange');
		}
	},
	/**
	* @private
	*/
	valueChanged: function () {
		var val = this.get('value');
		if (this.hasFocus() && val !== this.node.innerHTML) {
			this.selectAll();
			this.insertAtCursor(val);
		} else if(!this.hasFocus()) {
			this.set('content', val);
		}
		this.detectTextDirectionality((this.value || this.value === 0) ? this.value : '');
	},
	/**
	* @private
	*/
	disabledChanged: function () {
		if(this.tag === 'div') {
			this.setAttribute('contenteditable', this.disabled ? null : 'true');
		} else {
			this.setAttribute('disabled', this.disabled);
		}
		this.bubble('onDisabledChange');
	},
	/**
	* @private
	*/
	updateValue: function () {
		var val = this.node.innerHTML;
		this.set('value', val);
	},
	/**
	* @private
	*/
	updateValueAsync: function () {
		utils.asyncMethod(this.bindSafely('updateValue'));
	},

	/**
	* Determines whether this [control]{@link module:enyo/Control~Control} has focus.
	*
	* @returns {Boolean} `true` if the [RichText]{@link module:enyo/RichText~RichText} is focused;
	* otherwise, `false`.
	* @public
	*/
	hasFocus: function () {
		if (this.hasNode()) {
			return document.activeElement === this.node;
		}
	},
	/**
	* Retrieves the current [selection]{@glossary Selection} from the
	* [RichText]{@link module:enyo/RichText~RichText}.
	*
	* @returns {Selection} The [selection]{@glossary Selection} [object]{@glossary Object}.
	* @public
	*/
	getSelection: function () {
		if (this.hasFocus()) {
			return global.getSelection();
		}
	},

	/**
	* Removes the [selection]{@glossary Selection} [object]{@glossary Object}.
	*
	* @param {Boolean} start - If `true`, the [selection]{@glossary Selection} is
	*	[collapsed to the start]{@glossary Selection.collapseToStart} of the
	*	[range]{@glossary Range}; otherwise, it is
	*	[collapsed to the end]{@glossary Selection.collapseToEnd} of the range.
	* @public
	*/
	removeSelection: function (start) {
		var s = this.getSelection();
		if (s) {
			s[start ? 'collapseToStart' : 'collapseToEnd']();
		}
	},

	/**
	* Modifies the [selection]{@glossary Selection} [object]{@glossary Object}. Please
	* see the [Selection.modify]{@glossary Selection.modify} API for more information.
	*
	* @param {module:enyo/RichText~ModifyType} type - The type of change to apply.
	* @param {module:enyo/RichText~ModifyDirection} dir - The direction in which to apply the change.
	* @param {module:enyo/RichText~ModifyAmount} amount - The granularity of the change.
	* @deprecated since version 2.7
	* @public
	*/
	modifySelection: function (type, dir, amount) {
		var s = this.getSelection();
		if (s) {
			s.modify(type || 'move', dir, amount);
		}
	},

	/**
	* Moves the cursor according to the [Editing API]{@glossary Selection.modify}.
	*
	* @param {module:enyo/RichText~ModifyDirection} dir - The direction in which to apply the change.
	* @param {module:enyo/RichText~ModifyAmount} amount - The granularity of the change.
	* @deprecated since version 2.7
	* @public
	*/
	moveCursor: function (dir, amount) {
		this.modifySelection('move', dir, amount);
	},

	/**
	* Moves the cursor to end of text field.
	*
	* @deprecated since version 2.7
	* @public
	*/
	moveCursorToEnd: function () {
		this.moveCursor('forward', 'documentboundary');
	},

	/**
	* Moves the cursor to start of text field.
	*
	* @deprecated since version 2.7
	* @public
	*/
	moveCursorToStart: function () {
		this.moveCursor('backward', 'documentboundary');
	},

	/**
	* Selects all content in text field.
	*
	* @public
	*/
	selectAll: function () {
		if (this.hasFocus()) {
			document.execCommand('selectAll');
		}
	},

	/**
	* Inserts HTML at the cursor position. HTML will be escaped unless the
	* [allowHtml]{@link module:enyo/RichText~RichText#allowHtml} property is `true`.
	*
	* @param {String} val - The HTML to insert at the current cursor position.
	* @public
	*/
	insertAtCursor: function (val) {
		if (this.hasFocus()) {
			var v = this.allowHtml ? val : Control.escapeHtml(val).replace(/\n/g, '
');
			document.execCommand('insertHTML', false, v);
		}
	},

	// Accessibility

	/**
	* @private
	*/
	ariaObservers: [
		{to: 'aria-multiline', value: true}
	]
});
<|endoftext|>