001/*
002 * Syncany, www.syncany.org
003 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
004 *
005 * This program is free software: you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation, either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017 */
018package org.syncany.cli;
019
020import java.io.IOException;
021import java.lang.reflect.Constructor;
022import java.lang.reflect.InvocationTargetException;
023import java.net.URI;
024import java.net.UnknownHostException;
025import java.util.Arrays;
026import java.util.HashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.concurrent.ExecutionException;
030import java.util.concurrent.Future;
031import java.util.concurrent.TimeUnit;
032import java.util.concurrent.TimeoutException;
033import java.util.logging.Level;
034import java.util.logging.Logger;
035
036import joptsimple.OptionSet;
037import joptsimple.OptionSpec;
038
039import org.syncany.cli.util.InitConsole;
040import org.syncany.config.to.ConfigTO;
041import org.syncany.crypto.CipherUtil;
042import org.syncany.operations.daemon.messages.ShowMessageExternalEvent;
043import org.syncany.operations.init.GenlinkOperationResult;
044import org.syncany.plugins.Plugins;
045import org.syncany.plugins.UserInteractionListener;
046import org.syncany.plugins.transfer.NestedTransferPluginOption;
047import org.syncany.plugins.transfer.StorageException;
048import org.syncany.plugins.transfer.StorageTestResult;
049import org.syncany.plugins.transfer.TransferPlugin;
050import org.syncany.plugins.transfer.TransferPluginOption;
051import org.syncany.plugins.transfer.TransferPluginOption.ValidationResult;
052import org.syncany.plugins.transfer.TransferPluginOptionCallback;
053import org.syncany.plugins.transfer.TransferPluginOptionConverter;
054import org.syncany.plugins.transfer.TransferPluginOptions;
055import org.syncany.plugins.transfer.TransferPluginUtil;
056import org.syncany.plugins.transfer.TransferSettings;
057import org.syncany.plugins.transfer.oauth.OAuth;
058import org.syncany.plugins.transfer.oauth.OAuthGenerator;
059import org.syncany.plugins.transfer.oauth.OAuthTokenFinish;
060import org.syncany.plugins.transfer.oauth.OAuthTokenWebListener;
061import org.syncany.util.ReflectionUtil;
062import org.syncany.util.StringUtil;
063import org.syncany.util.StringUtil.StringJoinListener;
064
065import com.google.common.base.Predicate;
066import com.google.common.base.Strings;
067import com.google.common.collect.Iterables;
068import com.google.common.eventbus.Subscribe;
069
070/**
071 * The abstract init command provides multiple shared methods for the 'init'
072 * and 'connect' command. Both commands must provide the ability to
073 * query a user for transfer settings or parse settings from the command line
074 *
075 * @author Philipp C. Heckel (philipp.heckel@gmail.com)
076 * @author Christian Roth (christian.roth@port17.de)
077 */
078public abstract class AbstractInitCommand extends Command implements UserInteractionListener {
079        private static final Logger logger = Logger.getLogger(AbstractInitCommand.class.getName());
080
081        protected static final char NESTED_OPTIONS_SEPARATOR = '.';
082        protected static final String GENERIC_PLUGIN_TYPE_IDENTIFIER = ":type";
083        protected static final int PASSWORD_MIN_LENGTH = 10;
084        protected static final int PASSWORD_WARN_LENGTH = 12;
085        protected static final int OAUTH_TOKEN_WAIT_TIMEOUT = 60;
086
087        protected InitConsole console;
088        protected boolean isInteractive;
089        protected boolean isHeadless;
090
091        public AbstractInitCommand() {
092                console = InitConsole.getInstance();
093        }
094
095        protected ConfigTO createConfigTO(TransferSettings transferSettings) throws Exception {
096                ConfigTO configTO = new ConfigTO();
097
098                configTO.setDisplayName(getDefaultDisplayName());
099                configTO.setMachineName(getRandomMachineName());
100                configTO.setMasterKey(null);
101                configTO.setTransferSettings(transferSettings); // can be null
102
103                return configTO;
104        }
105
106        protected TransferSettings createTransferSettingsFromOptions(OptionSet options, OptionSpec<String> optionPlugin,
107                        OptionSpec<String> optionPluginOpts) throws Exception {
108
109                TransferPlugin plugin;
110                TransferSettings transferSettings;
111
112                // Parse --plugin and --plugin-option values
113                List<String> pluginOptionStrings = options.valuesOf(optionPluginOpts);
114                Map<String, String> knownPluginSettings = parsePluginSettingsFromOptions(pluginOptionStrings);
115
116                // Validation of some constraints
117                if (!options.has(optionPlugin) && knownPluginSettings.size() > 0) {
118                        throw new IllegalArgumentException("Provided plugin settings without a plugin name.");
119                }
120
121                plugin = options.has(optionPlugin) ? initPlugin(options.valueOf(optionPlugin)) : askPlugin();
122                transferSettings = askPluginSettings(plugin.createEmptySettings(), knownPluginSettings);
123
124                return transferSettings;
125        }
126
127        private Map<String, String> parsePluginSettingsFromOptions(List<String> pluginSettingsOptList) throws Exception {
128                Map<String, String> pluginOptionValues = new HashMap<>();
129
130                // Fill settings map
131                for (String pluginSettingKeyValue : pluginSettingsOptList) {
132                        String[] keyValue = pluginSettingKeyValue.split("=", 2);
133
134                        if (keyValue.length != 2) {
135                                throw new Exception("Invalid setting: " + pluginSettingKeyValue);
136                        }
137
138                        pluginOptionValues.put(keyValue[0], keyValue[1]);
139                }
140
141                return pluginOptionValues;
142        }
143
144        private TransferPlugin initPlugin(String pluginStr) throws Exception {
145                TransferPlugin plugin = Plugins.get(pluginStr, TransferPlugin.class);
146
147                if (plugin == null) {
148                        throw new Exception("ERROR: Plugin '" + pluginStr + "' does not exist.");
149                }
150
151                return plugin;
152        }
153
154        private TransferSettings askPluginSettings(TransferSettings settings, Map<String, String> knownPluginSettings) throws StorageException {
155                if (isInteractive) {
156                        out.println();
157                        out.println("Connection details for " + settings.getType() + " connection:");
158                }
159                else {
160                        logger.log(Level.INFO, "Non interactive mode");
161                }
162
163                try {
164                        // Show OAuth output
165                        printOAuthInformation(settings);
166
167                        // Ask for plugin settings
168                        List<TransferPluginOption> pluginOptions = TransferPluginOptions.getOrderedOptions(settings.getClass());
169
170                        for (TransferPluginOption option : pluginOptions) {
171                                askPluginSettings(settings, option, knownPluginSettings, "");
172                        }
173                }
174                catch (NoSuchFieldException e) {
175                        logger.log(Level.SEVERE, "No token could be found, maybe user denied access", e);
176                        throw new StorageException("No token found. Did you accept the authorization?", e);
177                }
178                catch (TimeoutException e) {
179                        logger.log(Level.SEVERE, "No token was received in the given time interval", e);
180                        throw new StorageException("No token was received in the given time interval", e);
181                }
182                catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException | IOException | InterruptedException | ExecutionException e) {
183                        logger.log(Level.SEVERE, "Unable to execute option generator", e);
184                        throw new RuntimeException("Unable to execute option generator: " + e.getMessage());
185                }
186
187                if (!settings.isValid()) {
188                        if (askRetryInvalidSettings(settings.getReasonForLastValidationFail())) {
189                                return askPluginSettings(settings, knownPluginSettings);
190                        }
191
192                        throw new StorageException("Validation failed: " + settings.getReasonForLastValidationFail());
193                }
194
195                logger.log(Level.INFO, "Settings are " + settings.toString());
196
197                return settings;
198        }
199
200        private void printOAuthInformation(TransferSettings settings) throws StorageException, NoSuchMethodException, SecurityException,
201                                        InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, IOException, ExecutionException, InterruptedException, TimeoutException, NoSuchFieldException {
202                OAuth oAuthSettings =   settings.getClass().getAnnotation(OAuth.class);
203
204                if (oAuthSettings != null) {
205                        Constructor<? extends OAuthGenerator> optionCallbackClassConstructor = oAuthSettings.value().getDeclaredConstructor(settings.getClass());
206                        OAuthGenerator oAuthGenerator = optionCallbackClassConstructor.newInstance(settings);
207
208                        if (isHeadless) {
209                                logger.log(Level.FINE, "User is in headless mode and the plugin is OAuth based");
210
211                                if (oAuthGenerator instanceof OAuthGenerator.WithNoRedirectMode) {
212                                        doOAuthInCopyTokenMode(oAuthGenerator);
213                                }
214                                else {
215                                        throw new RuntimeException("OAuth based plugin does not support headless mode");
216                                }
217                        }
218                        else {
219                                doOAuthInRedirectMode(oAuthGenerator, oAuthSettings);
220                        }
221
222                }
223        }
224
225        private void doOAuthInCopyTokenMode(OAuthGenerator generator) throws StorageException {
226                URI oAuthURL = ((OAuthGenerator.WithNoRedirectMode) generator).generateAuthUrl();
227
228                out.println();
229                out.println("This plugin needs you to authenticate your account so that Syncany can access it.");
230                out.printf("Please navigate to the URL below and enter the token:\n\n  %s\n\n", oAuthURL.toString());
231                out.print("- Token (paste from URL): ");
232
233                String token = console.readLine();
234                generator.checkToken(token, null);
235        }
236
237        private void doOAuthInRedirectMode(OAuthGenerator generator, OAuth settings) throws IOException, InterruptedException, ExecutionException, TimeoutException, StorageException {
238                OAuthTokenWebListener.Builder tokenListerBuilder = OAuthTokenWebListener.forMode(settings.mode());
239
240                if (settings.callbackPort() != OAuth.RANDOM_PORT) {
241                        tokenListerBuilder.setPort(settings.callbackPort());
242                }
243
244                if (!settings.callbackId().equals(OAuth.PLUGIN_ID)) {
245                        tokenListerBuilder.setId(settings.callbackId());
246                }
247
248                // non standard plugin?
249                if (generator instanceof OAuthGenerator.WithInterceptor) {
250                        tokenListerBuilder.setTokenInterceptor(((OAuthGenerator.WithInterceptor) generator).getInterceptor());
251                }
252
253                if (generator instanceof OAuthGenerator.WithExtractor) {
254                        tokenListerBuilder.setTokenExtractor(((OAuthGenerator.WithExtractor) generator).getExtractor());
255                }
256
257                OAuthTokenWebListener tokenListener = tokenListerBuilder.build();
258
259                URI oAuthURL = generator.generateAuthUrl(tokenListener.start());
260                Future<OAuthTokenFinish> futureTokenResponse = tokenListener.getToken();
261
262                out.println();
263                out.println("This plugin needs you to authenticate your account so that Syncany can access it.");
264                out.printf("Please navigate to the URL below and accept the given permissions:\n\n  %s\n\n", oAuthURL.toString());
265                out.print("Waiting for authorization...");
266
267                OAuthTokenFinish tokenResponse = futureTokenResponse.get(OAUTH_TOKEN_WAIT_TIMEOUT, TimeUnit.SECONDS);
268
269                if (tokenResponse != null) {
270                        out.printf(" received token '%s'\n\n", tokenResponse.getToken());
271                        generator.checkToken(tokenResponse.getToken(), tokenResponse.getCsrfState());
272                }
273                else {
274                        out.println(" canceled");
275                        throw new StorageException("Error while acquiring token, perhaps user denied authorization");
276                }
277        }
278
279        private void askPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings, String nestPrefix)
280                        throws IllegalAccessException, InstantiationException, StorageException, IllegalArgumentException, InvocationTargetException,
281                        NoSuchMethodException, SecurityException {
282
283                if (option instanceof NestedTransferPluginOption) {
284                        Class<?> childPluginTransferSettingsClass = ReflectionUtil.getClassFromType(option.getType());
285                        boolean isGenericChildPlugin = TransferSettings.class.equals(childPluginTransferSettingsClass);
286
287                        if (isGenericChildPlugin) {
288                                askGenericChildPluginSettings(settings, option, knownPluginSettings, nestPrefix);
289                        }
290                        else {
291                                askConreteChildPluginSettings(settings, (NestedTransferPluginOption) option, knownPluginSettings, nestPrefix);
292                        }
293                }
294                else {
295                        askNormalPluginSettings(settings, option, knownPluginSettings, nestPrefix);
296                }
297        }
298
299        private void askNormalPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings,
300                        String nestPrefix)
301                        throws StorageException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException,
302                        NoSuchMethodException, SecurityException {
303
304                TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback());
305                TransferPluginOptionConverter optionConverter = createOptionConverter(settings, option.getConverter());
306
307                if (!isInteractive && !knownPluginSettings.containsKey(nestPrefix + option.getName())) {
308                        throw new IllegalArgumentException("Missing plugin option (" + nestPrefix + option.getName() + ") in non-interactive mode.");
309                }
310                else if (knownPluginSettings.containsKey(nestPrefix + option.getName())) {
311                        settings.setField(option.getField().getName(), knownPluginSettings.get(nestPrefix + option.getName()));
312                }
313                else if (!option.isVisible()) {
314                        // Do nothing. Invisible option!
315                }
316                else {
317                        callAndPrintPreQueryCallback(optionCallback);
318
319                        String optionValue = askPluginOption(settings, option);
320
321                        if (optionConverter != null) {
322                                optionValue = optionConverter.convert(optionValue);
323                        }
324
325                        settings.setField(option.getField().getName(), optionValue);
326
327                        callAndPrintPostQueryCallback(optionCallback, optionValue);
328                }
329        }
330
331        /**
332         * Queries the user for a plugin (which plugin to use?) and then
333         * asks for all of the plugin's settings.
334         *
335         * <p>This case is triggered by a field looking like this:
336         * <code>private TransferSettings childPluginSettings;</code>
337         */
338        private void askGenericChildPluginSettings(TransferSettings settings, TransferPluginOption option, Map<String, String> knownPluginSettings,
339                        String nestPrefix)
340                        throws StorageException, IllegalAccessException, InstantiationException, IllegalArgumentException, InvocationTargetException,
341                        NoSuchMethodException, SecurityException {
342
343                TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback());
344
345                if (isInteractive) {
346                        callAndPrintPreQueryCallback(optionCallback);
347
348                        out.println();
349                        out.println(option.getDescription() + ":");
350                }
351
352                TransferPlugin childPlugin = null;
353                Class<? extends TransferPlugin> pluginClass = TransferPluginUtil.getTransferPluginClass(settings.getClass());
354
355                // Non-interactive: Plugin settings might be given via command line
356                try {
357                        childPlugin = initPlugin(knownPluginSettings.get(nestPrefix + option.getName() + GENERIC_PLUGIN_TYPE_IDENTIFIER));
358                }
359                catch (Exception e) {
360                        if (!isInteractive) {
361                                throw new IllegalArgumentException("Missing nested plugin type (" + nestPrefix + option.getName() + GENERIC_PLUGIN_TYPE_IDENTIFIER
362                                                + ") in non-interactive mode.");
363                        }
364                }
365
366                // Interactive mode: Ask for sub-plugin
367                while (childPlugin == null) {
368                        childPlugin = askPlugin(pluginClass);
369                }
370
371                if (isInteractive) {
372                        out.println();
373                }
374
375                // Create nested/child settings
376                TransferSettings childSettings = childPlugin.createEmptySettings();
377
378                settings.setField(option.getField().getName(), childSettings);
379                nestPrefix = nestPrefix + option.getName() + NESTED_OPTIONS_SEPARATOR;
380
381                for (TransferPluginOption nestedOption : TransferPluginOptions.getOrderedOptions(childSettings.getClass())) {
382                        askPluginSettings(childSettings, nestedOption, knownPluginSettings, nestPrefix);
383                }
384
385                if (isInteractive) {
386                        callAndPrintPostQueryCallback(optionCallback, null);
387                }
388        }
389
390        /**
391         * Asks the user for all of the child plugin's settings.
392         *
393         * <p>This case is triggered by a field looking like this:
394         * <code>private LocalTransferSettings localChildPluginSettings;</code>
395         */
396        private void askConreteChildPluginSettings(TransferSettings settings, NestedTransferPluginOption option, Map<String, String> knownPluginSettings,
397                        String nestPrefix) throws StorageException, IllegalAccessException, InstantiationException, IllegalArgumentException,
398                        InvocationTargetException, NoSuchMethodException, SecurityException {
399
400                TransferPluginOptionCallback optionCallback = createOptionCallback(settings, option.getCallback());
401
402                if (isInteractive) {
403                        callAndPrintPreQueryCallback(optionCallback);
404
405                        out.println();
406                        out.println(option.getDescription() + ":");
407                }
408
409                for (TransferPluginOption nestedPluginOption : option.getOptions()) {
410                        Class<?> nestedTransferSettingsClass = ReflectionUtil.getClassFromType(option.getType());
411
412                        if (nestedTransferSettingsClass == null) {
413                                throw new RuntimeException("No class found for type: " + option.getType());
414                        }
415
416                        TransferSettings nestedSettings = (TransferSettings) nestedTransferSettingsClass.newInstance();
417
418                        settings.setField(option.getField().getName(), nestedSettings);
419                        nestPrefix = nestPrefix + option.getName() + NESTED_OPTIONS_SEPARATOR;
420
421                        askPluginSettings(nestedSettings, nestedPluginOption, knownPluginSettings, nestPrefix);
422                }
423
424                if (isInteractive) {
425                        callAndPrintPostQueryCallback(optionCallback, null);
426                }
427        }
428
429        private void callAndPrintPreQueryCallback(TransferPluginOptionCallback optionCallback) {
430                if (optionCallback != null) {
431                        String preQueryMessage = optionCallback.preQueryCallback();
432
433                        if (preQueryMessage != null) {
434                                out.println(preQueryMessage);
435                        }
436                }
437        }
438
439        private void callAndPrintPostQueryCallback(TransferPluginOptionCallback optionCallback, String optionValue) {
440                if (optionCallback != null) {
441                        String postQueryMessage = optionCallback.postQueryCallback(optionValue);
442
443                        if (postQueryMessage != null) {
444                                out.println(postQueryMessage);
445                        }
446                }
447        }
448
449        private String askPluginOption(TransferSettings settings, TransferPluginOption option) throws StorageException {
450                while (true) {
451                        String value;
452
453                        // Retrieve value
454                        if (option.isSensitive()) {
455                                // The option is sensitive. Could be either mandatory or optional
456                                value = askPluginOptionSensitive(settings, option);
457                        }
458                        else if (!option.isRequired()) {
459                                // The option is optional
460                                value = askPluginOptionOptional(settings, option);
461                        }
462                        else {
463                                // The option is mandatory, but not sensitive
464                                value = askPluginOptionNormal(settings, option);
465                        }
466
467                        if ("".equals(value)) {
468                                value = null;
469                        }
470
471                        // Validate result
472                        ValidationResult validationResult = option.isValid(value);
473
474                        switch (validationResult) {
475                        case INVALID_NOT_SET:
476                                out.println("ERROR: This option is mandatory.");
477                                out.println();
478                                break;
479
480                        case INVALID_TYPE:
481                                out.println("ERROR: Not a valid input.");
482                                out.println();
483                                break;
484
485                        case VALID:
486                                return value;
487
488                        default:
489                                throw new RuntimeException("Invalid return type: " + validationResult);
490                        }
491                }
492        }
493
494        private String askPluginOptionNormal(TransferSettings settings, TransferPluginOption option) throws StorageException {
495                String knownOptionValue = settings.getField(option.getField().getName());
496                String value = knownOptionValue;
497
498                if (option.isSingular() || knownOptionValue == null || "".equals(knownOptionValue)) {
499                        out.printf("- %s: ", getDescription(settings, option));
500                        value = console.readLine();
501                }
502                else {
503                        out.printf("- %s (%s): ", getDescription(settings, option), knownOptionValue);
504                        value = console.readLine();
505
506                        if ("".equals(value)) {
507                                value = knownOptionValue;
508                        }
509                }
510
511                return value;
512        }
513
514        private String askPluginOptionOptional(TransferSettings settings, TransferPluginOption option) throws StorageException {
515                String knownOptionValue = settings.getField(option.getField().getName());
516                String value = knownOptionValue;
517
518                if (knownOptionValue == null || "".equals(knownOptionValue)) {
519                        String defaultValueDescription = settings.getField(option.getField().getName());
520
521                        if (defaultValueDescription == null) {
522                                defaultValueDescription = "none";
523                        }
524
525                        out.printf("- %s (optional, default is %s): ", getDescription(settings, option), defaultValueDescription);
526                        value = console.readLine();
527                }
528                else {
529                        out.printf("- %s (%s): ", getDescription(settings, option), knownOptionValue);
530                        value = console.readLine();
531
532                        if ("".equals(value)) {
533                                value = knownOptionValue;
534                        }
535                }
536
537                return value;
538        }
539
540        private String askPluginOptionSensitive(TransferSettings settings, TransferPluginOption option) throws StorageException {
541                String knownOptionValue = settings.getField(option.getField().getName());
542                String value = knownOptionValue;
543                String optionalIndicator = option.isRequired() ? "" : ", optional";
544
545                if (option.isSingular() || knownOptionValue == null || "".equals(knownOptionValue)) {
546                        out.printf("- %s (not displayed%s): ", getDescription(settings, option), optionalIndicator);
547                        value = String.copyValueOf(console.readPassword());
548                }
549                else {
550                        out.printf("- %s (***, not displayed%s): ", getDescription(settings, option), optionalIndicator);
551                        value = String.copyValueOf(console.readPassword());
552
553                        if ("".equals(value)) {
554                                value = knownOptionValue;
555                        }
556                }
557
558                return value;
559        }
560
561        private String getDescription(TransferSettings settings, TransferPluginOption option) {
562                Class<?> clazzForType = ReflectionUtil.getClassFromType(option.getType());
563
564                if (clazzForType != null && Enum.class.isAssignableFrom(clazzForType)) {
565                        Object[] enumValues = clazzForType.getEnumConstants();
566
567                        if (enumValues == null) {
568                                throw new RuntimeException("Invalid TransferSettings class found: Enum at " + settings + " has no values");
569                        }
570
571                        logger.log(Level.FINE, "Found enum option, values are: " + StringUtil.join(enumValues, ", "));
572
573                        return String.format("%s, choose from %s", option.getDescription(), StringUtil.join(enumValues, ", "));
574                }
575                else {
576                        return option.getDescription();
577                }
578        }
579
580        protected TransferPlugin askPlugin() {
581                return askPlugin(null);
582        }
583
584        protected TransferPlugin askPlugin(final Class<? extends TransferPlugin> ignoreTransferPluginClass) {
585                TransferPlugin plugin = null;
586                final List<TransferPlugin> plugins = Plugins.list(TransferPlugin.class);
587
588                Iterables.removeIf(plugins, new Predicate<TransferPlugin>() {
589                        @Override
590                        public boolean apply(TransferPlugin transferPlugin) {
591                                return ignoreTransferPluginClass == transferPlugin.getClass();
592                        }
593                });
594
595                String pluginsList = StringUtil.join(plugins, ", ", new StringJoinListener<TransferPlugin>() {
596                        @Override
597                        public String getString(TransferPlugin plugin) {
598                                return plugin.getId();
599                        }
600                });
601
602                while (plugin == null) {
603                        out.println("Choose a storage plugin. Available plugins are: " + pluginsList);
604                        out.print("Plugin: ");
605                        String pluginStr = console.readLine();
606
607                        plugin = Plugins.get(pluginStr, TransferPlugin.class);
608
609                        if (plugin == null || ignoreTransferPluginClass == plugin.getClass()) {
610                                out.println("ERROR: Plugin does not exist or cannot be used.");
611                                out.println();
612
613                                plugin = null;
614                        }
615                }
616
617                return plugin;
618        }
619
620        private TransferPluginOptionConverter createOptionConverter(TransferSettings settings,
621                        Class<? extends TransferPluginOptionConverter> optionConverterClass) throws InstantiationException, IllegalAccessException,
622                        IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
623
624                TransferPluginOptionConverter optionConverter = null;
625
626                if (optionConverterClass != null) {
627                        Constructor<? extends TransferPluginOptionConverter> optionConverterClassConstructor = optionConverterClass.getDeclaredConstructor(settings.getClass());
628                        optionConverter = optionConverterClassConstructor.newInstance(settings);
629                }
630
631                return optionConverter;
632        }
633
634        private TransferPluginOptionCallback createOptionCallback(TransferSettings settings,
635                        Class<? extends TransferPluginOptionCallback> optionCallbackClass) throws InstantiationException, IllegalAccessException,
636                        IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
637
638                TransferPluginOptionCallback optionCallback = null;
639
640                if (optionCallbackClass != null) {
641                        Constructor<? extends TransferPluginOptionCallback> optionCallbackClassConstructor = optionCallbackClass.getDeclaredConstructor(settings.getClass());
642                        optionCallback = optionCallbackClassConstructor.newInstance(settings);
643                }
644
645                return optionCallback;
646        }
647
648        protected String getRandomMachineName() {
649                return CipherUtil.createRandomAlphabeticString(20);
650        }
651
652        protected String getDefaultDisplayName() throws UnknownHostException {
653                return System.getProperty("user.name");
654        }
655
656        protected boolean askRetryInvalidSettings(String failReason) {
657                return onUserConfirm("Validation failure", failReason, "Would you change the settings");
658        }
659
660        protected boolean askRetryConnection() {
661                return onUserConfirm(null, "Connection failure", "Would you change the settings and retry the connection");
662        }
663
664        protected TransferSettings updateTransferSettings(TransferSettings transferSettings) throws StorageException {
665                try {
666                        return askPluginSettings(transferSettings, new HashMap<String, String>());
667                }
668                catch (Exception e) {
669                        logger.log(Level.SEVERE, "Unable to reload old plugin settings", e);
670                        throw new StorageException("Unable to reload old plugin settings: " + e.getMessage());
671                }
672        }
673
674        protected void printLink(GenlinkOperationResult operationResult, boolean shortOutput) {
675                if (shortOutput) {
676                        out.println(operationResult.getShareLink());
677                }
678                else {
679                        out.println();
680                        out.println("   " + operationResult.getShareLink());
681                        out.println();
682
683                        if (operationResult.isShareLinkEncrypted()) {
684                                out.println("This link is encrypted with the given password, so you can safely share it.");
685                                out.println("using unsecure communication (chat, e-mail, etc.)");
686                                out.println();
687                                out.println("Note: The link contains the details of your repo connection which typically");
688                                out.println("      consist of usernames/password of the connection (e.g. FTP user/pass).");
689                        }
690                        else {
691                                out.println("WARNING: This link is NOT ENCRYPTED and might contain connection credentials");
692                                out.println("         Do NOT share this link unless you know what you are doing!");
693                                out.println();
694                                out.println("         The link contains the details of your repo connection which typically");
695                                out.println("         consist of usernames/password of the connection (e.g. FTP user/pass).");
696                        }
697
698                        out.println();
699                }
700        }
701
702        protected void printTestResult(StorageTestResult testResult) {
703                out.println("Details:");
704                out.println("- Target connect success: " + testResult.isTargetCanConnect());
705                out.println("- Target exists:          " + testResult.isTargetExists());
706                out.println("- Target creatable:       " + testResult.isTargetCanCreate());
707                out.println("- Target writable:        " + testResult.isTargetCanWrite());
708                out.println("- Repo file exists:       " + testResult.isRepoFileExists());
709                out.println();
710
711                if (testResult.getErrorMessage() != null) {
712                        out.println("Error message (see log file for details):");
713                        out.println("  " + testResult.getErrorMessage());
714                }
715        }
716
717        @Override
718        public boolean onUserConfirm(String header, String message, String question) {
719                if (header != null) {
720                        out.println();
721                        out.println(header);
722                        out.println(Strings.repeat("-", header.length()));
723                }
724
725                out.println(message);
726                out.println();
727
728                String yesno = console.readLine(question + " (y/n)? ");
729
730                if (!yesno.toLowerCase().startsWith("y") && !"".equals(yesno)) {
731                        return false;
732                }
733                else {
734                        return true;
735                }
736        }
737
738        @Subscribe
739        public void onShowMessage(ShowMessageExternalEvent messageEvent) {
740                out.println();
741                out.println(messageEvent.getMessage());
742        }
743
744        @Override
745        public String onUserPassword(String header, String message) {
746                if (!isInteractive) {
747                        throw new RuntimeException("Repository is encrypted, but no password was given in non-interactive mode.");
748                }
749
750                out.println();
751
752                if (header != null) {
753                        out.println(header);
754                        out.println(Strings.repeat("-", header.length()));
755                }
756
757                if (!message.trim().endsWith(":")) {
758                        message += ": ";
759                }
760
761                char[] passwordChars = console.readPassword(message);
762                return String.copyValueOf(passwordChars);
763        }
764
765        @Override
766        public String onUserNewPassword() {
767                out.println();
768                out.println("The password is used to encrypt data on the remote storage.");
769                out.println("Choose wisely!");
770                out.println();
771
772                String password = null;
773
774                while (password == null) {
775                        char[] passwordChars = console.readPassword("Password (min. " + PASSWORD_MIN_LENGTH + " chars): ");
776
777                        if (passwordChars.length < PASSWORD_MIN_LENGTH) {
778                                out.println("ERROR: This password is not allowed (too short, min. " + PASSWORD_MIN_LENGTH + " chars)");
779                                out.println();
780
781                                continue;
782                        }
783
784                        char[] confirmPasswordChars = console.readPassword("Confirm: ");
785
786                        if (!Arrays.equals(passwordChars, confirmPasswordChars)) {
787                                out.println("ERROR: Passwords do not match.");
788                                out.println();
789
790                                continue;
791                        }
792
793                        if (passwordChars.length < PASSWORD_WARN_LENGTH) {
794                                out.println();
795                                out.println("WARNING: The password is a bit short. Less than " + PASSWORD_WARN_LENGTH + " chars are not future-proof!");
796                                String yesno = console.readLine("Are you sure you want to use it (y/n)? ");
797
798                                if (!yesno.toLowerCase().startsWith("y") && !"".equals(yesno)) {
799                                        out.println();
800                                        continue;
801                                }
802                        }
803
804                        password = new String(passwordChars);
805                }
806
807                return password;
808        }
809}