diff --git a/HISTORY.rst b/HISTORY.rst index 9232337..8c6913d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -3,6 +3,13 @@ History ------- +10.2.2 (Unreleased) ++++++++++++++++++++ + +- New config hide_project_name and argument --hide-project-names for + obfuscating project names when sending coding activity to api. + + 10.2.1 (2018-04-26) +++++++++++++++++++ diff --git a/README.rst b/README.rst index a714aaf..a51109b 100644 --- a/README.rst +++ b/README.rst @@ -61,6 +61,7 @@ format. An example config file with all available options:: debug = false api_key = your-api-key hide_filenames = false + hide_project_names = false exclude = ^COMMIT_EDITMSG$ ^TAG_EDITMSG$ diff --git a/tests/samples/configs/paranoid_projects.cfg b/tests/samples/configs/paranoid_projects.cfg new file mode 100644 index 0000000..8ca5b44 --- /dev/null +++ b/tests/samples/configs/paranoid_projects.cfg @@ -0,0 +1,5 @@ +[settings] +debug = false +api_key = c21f8ebd-6a6a-48a0-900b-0870db3d7afe +api_url = https://api.wakatime.com/api/v1/heartbeats +hide_project_names = true diff --git a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args index f427f88..1ebe6ce 100644 --- a/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args +++ b/tests/samples/output/configs_test_config_file_not_passed_in_command_line_args @@ -4,8 +4,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames] - [--exclude EXCLUDE] [--exclude-unknown-project] - [--include INCLUDE] [--include-only-with-project-file] - [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] - [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] + [--hide-project-names] [--exclude EXCLUDE] + [--exclude-unknown-project] [--include INCLUDE] + [--include-only-with-project-file] [--extra-heartbeats] + [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] + [--config CONFIG] [--verbose] [--version] wakatime: error: Missing api key. Find your api key from wakatime.com/settings/api-key. diff --git a/tests/samples/output/configs_test_missing_config_file b/tests/samples/output/configs_test_missing_config_file index f427f88..1ebe6ce 100644 --- a/tests/samples/output/configs_test_missing_config_file +++ b/tests/samples/output/configs_test_missing_config_file @@ -4,8 +4,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames] - [--exclude EXCLUDE] [--exclude-unknown-project] - [--include INCLUDE] [--include-only-with-project-file] - [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] - [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] + [--hide-project-names] [--exclude EXCLUDE] + [--exclude-unknown-project] [--include INCLUDE] + [--include-only-with-project-file] [--extra-heartbeats] + [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] + [--config CONFIG] [--verbose] [--version] wakatime: error: Missing api key. Find your api key from wakatime.com/settings/api-key. diff --git a/tests/samples/output/main_test_timeout_passed_via_command_line b/tests/samples/output/main_test_timeout_passed_via_command_line index 5e27b26..bec9d4c 100644 --- a/tests/samples/output/main_test_timeout_passed_via_command_line +++ b/tests/samples/output/main_test_timeout_passed_via_command_line @@ -4,8 +4,9 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames] - [--exclude EXCLUDE] [--exclude-unknown-project] - [--include INCLUDE] [--include-only-with-project-file] - [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] - [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] + [--hide-project-names] [--exclude EXCLUDE] + [--exclude-unknown-project] [--include INCLUDE] + [--include-only-with-project-file] [--extra-heartbeats] + [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] + [--config CONFIG] [--verbose] [--version] wakatime: error: argument --timeout: invalid int value: 'abc' diff --git a/tests/samples/output/test_help_contents b/tests/samples/output/test_help_contents index 028f05c..cc2c93d 100644 --- a/tests/samples/output/test_help_contents +++ b/tests/samples/output/test_help_contents @@ -4,10 +4,11 @@ usage: wakatime [-h] [--entity FILE] [--key KEY] [--write] [--plugin PLUGIN] [--proxy PROXY] [--no-ssl-verify] [--project PROJECT] [--alternate-project ALTERNATE_PROJECT] [--language LANGUAGE] [--hostname HOSTNAME] [--disable-offline] [--hide-filenames] - [--exclude EXCLUDE] [--exclude-unknown-project] - [--include INCLUDE] [--include-only-with-project-file] - [--extra-heartbeats] [--log-file LOG_FILE] [--api-url API_URL] - [--timeout TIMEOUT] [--config CONFIG] [--verbose] [--version] + [--hide-project-names] [--exclude EXCLUDE] + [--exclude-unknown-project] [--include INCLUDE] + [--include-only-with-project-file] [--extra-heartbeats] + [--log-file LOG_FILE] [--api-url API_URL] [--timeout TIMEOUT] + [--config CONFIG] [--verbose] [--version] Common interface for the WakaTime api. @@ -49,6 +50,10 @@ optional arguments: --disable-offline Disables offline time logging instead of queuing logged time. --hide-filenames Obfuscate filenames. Will not send file names to api. + --hide-project-names Obfuscate project names. When a project folder is + detected instead of using the folder name as the + project, a .wakatime-project file is created with a + random project name. --exclude EXCLUDE Filename patterns to exclude from logging. POSIX regex syntax. Can be used more than once. --exclude-unknown-project diff --git a/tests/test_configs.py b/tests/test_configs.py index 52762d3..024babf 100644 --- a/tests/test_configs.py +++ b/tests/test_configs.py @@ -459,6 +459,46 @@ class ConfigsTestCase(TestCase): self.assertOfflineHeartbeatsSynced() self.assertSessionCacheSaved() + def test_obfuscte_project_names(self): + self.patched['wakatime.packages.requests.adapters.HTTPAdapter.send'].return_value = CustomResponse() + + with TemporaryDirectory() as tempdir: + shutil.copytree('tests/samples/projects/git', os.path.join(tempdir, 'git')) + shutil.move(os.path.join(tempdir, 'git', 'dot_git'), os.path.join(tempdir, 'git', '.git')) + entity = os.path.join(tempdir, 'git', 'emptyfile.txt') + now = u(int(time.time())) + config = 'tests/samples/configs/paranoid_projects.cfg' + key = u(uuid.uuid4()) + dependencies = [] + generated_proj = 'Icy Bridge 42' + + args = ['--file', entity, '--key', key, '--config', config, '--time', now, '--log-file', '~/.wakatime.log'] + + with mock.patch('wakatime.project.generate_project_name') as mock_proj: + mock_proj.return_value = generated_proj + + retval = execute(args) + + self.assertEquals(retval, SUCCESS) + self.assertNothingPrinted() + + heartbeat = { + 'language': 'Text only', + 'lines': 0, + 'entity': os.path.realpath(entity), + 'project': generated_proj, + 'time': float(now), + 'is_write': False, + 'type': 'file', + 'dependencies': dependencies, + 'user_agent': ANY, + } + self.assertHeartbeatSent(heartbeat) + + self.assertHeartbeatNotSavedOffline() + self.assertOfflineHeartbeatsSynced() + self.assertSessionCacheSaved() + @log_capture() def test_exclude_file(self, logs): logging.disable(logging.NOTSET) diff --git a/tests/test_project.py b/tests/test_project.py index 8ffcc1b..1b24800 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -15,6 +15,7 @@ from testfixtures import log_capture from wakatime.compat import u, open from wakatime.constants import API_ERROR, SUCCESS from wakatime.exceptions import NotYetImplemented +from wakatime.project import generate_project_name from wakatime.projects.base import BaseProject from wakatime.projects.git import Git from .utils import ANY, DynamicIterable, TestCase, TemporaryDirectory, CustomResponse, mock, json @@ -723,3 +724,7 @@ class ProjectTestCase(TestCase): self.assertNothingPrinted() self.assertNothingLogged(logs) self.assertEquals('proj-arg', self.patched['wakatime.offlinequeue.Queue.push'].call_args[0][0]['project']) + + def test_generate_project_name(self): + self.assertGreater(len(generate_project_name()), 1) + self.assertNotEqual(generate_project_name(), generate_project_name()) diff --git a/wakatime/arguments.py b/wakatime/arguments.py index 0df2d6a..ba1424f 100644 --- a/wakatime/arguments.py +++ b/wakatime/arguments.py @@ -133,6 +133,13 @@ def parse_arguments(): parser.add_argument('--hidefilenames', dest='hidefilenames', action='store_true', help=argparse.SUPPRESS) + parser.add_argument('--hide-project-names', dest='hide_project_names', + action='store_true', + help='Obfuscate project names. When a project ' + + 'folder is detected instead of using the ' + + 'folder name as the project, a ' + + '.wakatime-project file is created with a ' + + 'random project name.') parser.add_argument('--exclude', dest='exclude', action='append', help='Filename patterns to exclude from logging. ' + 'POSIX regex syntax. Can be used more than once.') @@ -267,6 +274,8 @@ def parse_arguments(): for pattern in option.split("\n"): if pattern.strip() != '': args.hide_filenames.append(pattern) + if not args.hide_project_names and configs.has_option('settings', 'hide_project_names'): + args.hide_project_names = configs.getboolean('settings', 'hide_project_names') if args.offline_deprecated: args.offline = False if args.offline and configs.has_option('settings', 'offline'): diff --git a/wakatime/project.py b/wakatime/project.py index 4350b62..5e88427 100644 --- a/wakatime/project.py +++ b/wakatime/project.py @@ -9,8 +9,11 @@ :license: BSD, see LICENSE for more details. """ +import os import logging +import random +from .compat import open from .projects.git import Git from .projects.mercurial import Mercurial from .projects.projectfile import ProjectFile @@ -62,7 +65,7 @@ def get_project_info(configs, heartbeat, data): branch_name = project.branch() break - if project_name is None: + if project_name is None and not data.get('hide_project_names'): project_name = data.get('project') or heartbeat.args.project if project_name is None or branch_name is None: @@ -76,6 +79,15 @@ def get_project_info(configs, heartbeat, data): if project.process(): project_name = project_name or project.name() branch_name = branch_name or project.branch() + if data.get('hide_project_names'): + branch_name = None + project_name = generate_project_name() + project_file = os.path.join(project.folder(), '.wakatime-project') + try: + with open(project_file, 'w') as fh: + fh.write(project_name) + except IOError: + project_name = None break if project_name is None: @@ -88,3 +100,42 @@ def get_configs_for_plugin(plugin_name, configs): if configs and configs.has_section(plugin_name): return dict(configs.items(plugin_name)) return None + + +def generate_project_name(): + """Generates a random project name.""" + + adjectives = [ + 'aged', 'ancient', 'autumn', 'billowing', 'bitter', 'black', 'blue', 'bold', + 'broad', 'broken', 'calm', 'cold', 'cool', 'crimson', 'curly', 'damp', + 'dark', 'dawn', 'delicate', 'divine', 'dry', 'empty', 'falling', 'fancy', + 'flat', 'floral', 'fragrant', 'frosty', 'gentle', 'green', 'hidden', 'holy', + 'icy', 'jolly', 'late', 'lingering', 'little', 'lively', 'long', 'lucky', + 'misty', 'morning', 'muddy', 'mute', 'nameless', 'noisy', 'odd', 'old', + 'orange', 'patient', 'plain', 'polished', 'proud', 'purple', 'quiet', 'rapid', + 'raspy', 'red', 'restless', 'rough', 'round', 'royal', 'shiny', 'shrill', + 'shy', 'silent', 'small', 'snowy', 'soft', 'solitary', 'sparkling', 'spring', + 'square', 'steep', 'still', 'summer', 'super', 'sweet', 'throbbing', 'tight', + 'tiny', 'twilight', 'wandering', 'weathered', 'white', 'wild', 'winter', 'wispy', + 'withered', 'yellow', 'young' + ] + nouns = [ + 'art', 'band', 'bar', 'base', 'bird', 'block', 'boat', 'bonus', + 'bread', 'breeze', 'brook', 'bush', 'butterfly', 'cake', 'cell', 'cherry', + 'cloud', 'credit', 'darkness', 'dawn', 'dew', 'disk', 'dream', 'dust', + 'feather', 'field', 'fire', 'firefly', 'flower', 'fog', 'forest', 'frog', + 'frost', 'glade', 'glitter', 'grass', 'hall', 'hat', 'haze', 'heart', + 'hill', 'king', 'lab', 'lake', 'leaf', 'limit', 'math', 'meadow', + 'mode', 'moon', 'morning', 'mountain', 'mouse', 'mud', 'night', 'paper', + 'pine', 'poetry', 'pond', 'queen', 'rain', 'recipe', 'resonance', 'rice', + 'river', 'salad', 'scene', 'sea', 'shadow', 'shape', 'silence', 'sky', + 'smoke', 'snow', 'snowflake', 'sound', 'star', 'sun', 'sun', 'sunset', + 'surf', 'term', 'thunder', 'tooth', 'tree', 'truth', 'union', 'unit', + 'violet', 'voice', 'water', 'waterfall', 'wave', 'wildflower', 'wind', 'wood' + ] + numbers = [str(x) for x in range(10)] + return ' '.join([ + random.choice(adjectives).capitalize(), + random.choice(nouns).capitalize(), + random.choice(numbers) + random.choice(numbers), + ]) diff --git a/wakatime/projects/base.py b/wakatime/projects/base.py index 4269d39..9a2e1f8 100644 --- a/wakatime/projects/base.py +++ b/wakatime/projects/base.py @@ -43,3 +43,8 @@ class BaseProject(object): """ Returns the current branch. """ raise NotYetImplemented() + + def folder(self): + """ Returns the project's top folder path. + """ + raise NotYetImplemented() diff --git a/wakatime/projects/git.py b/wakatime/projects/git.py index 9acdc78..4bddcf0 100644 --- a/wakatime/projects/git.py +++ b/wakatime/projects/git.py @@ -25,6 +25,7 @@ class Git(BaseProject): _submodule = None _project_name = None _head_file = None + _project_folder = None def process(self): return self._find_git_config_file(self.path) @@ -40,6 +41,9 @@ class Git(BaseProject): return self._get_branch_from_head_file(line) return u('master') + def folder(self): + return self._project_folder + def _find_git_config_file(self, path): path = os.path.realpath(path) if os.path.isfile(path): @@ -47,6 +51,7 @@ class Git(BaseProject): if os.path.isfile(os.path.join(path, '.git', 'config')): self._project_name = os.path.basename(path) self._head_file = os.path.join(path, '.git', 'HEAD') + self._project_folder = path return True link_path = self._path_from_gitdir_link_file(path) @@ -56,12 +61,14 @@ class Git(BaseProject): if self._is_worktree(link_path): self._project_name = self._project_from_worktree(link_path) self._head_file = os.path.join(link_path, 'HEAD') + self._project_folder = path return True # next check if this is a submodule if self._submodules_supported_for_path(path): self._project_name = os.path.basename(path) self._head_file = os.path.join(link_path, 'HEAD') + self._project_folder = path return True split_path = os.path.split(path) diff --git a/wakatime/projects/mercurial.py b/wakatime/projects/mercurial.py index 2a77489..b7647a3 100644 --- a/wakatime/projects/mercurial.py +++ b/wakatime/projects/mercurial.py @@ -47,6 +47,11 @@ class Mercurial(BaseProject): log.traceback(logging.WARNING) return u('default') + def folder(self): + if self.configDir: + return os.path.dirname(self.configDir) + return None + def _find_hg_config_dir(self, path): path = os.path.realpath(path) if os.path.isfile(path): diff --git a/wakatime/projects/subversion.py b/wakatime/projects/subversion.py index 3b0e32b..f5a6f38 100644 --- a/wakatime/projects/subversion.py +++ b/wakatime/projects/subversion.py @@ -41,6 +41,11 @@ class Subversion(BaseProject): return None # pragma: nocover return u(self.info['URL'].split('/')[-1].split('\\')[-1]) + def folder(self): + if 'Repository Root' not in self.info: + return None + return self.info['Repository Root'] + def _find_binary(self): if self.binary_location: return self.binary_location