Files
LinkScope_Client/Core/Interface/MenuBar.py
AccentuSoft 7507990ca5 Push commits:
Move saved Resolutions parameters to global scope.

Move all Program settings to global scope.

Update & Refactor Settings Object.

Update Whats My Name - make detection more precise & add sites.

Update README.md

Update version number

Finalize importing tabs from TOR.

Change settings labels so they look better in menus. Add Tor browser path setting.

Fix bug with deselected entities when zooming out.

Transition to pyqtdarktheme for theme & styling.

Check if firefox snap exists if firefox package is not present.

Make sure that every file from the directories holding resolutions is present.

Refactor importing of tabs.
2023-02-07 18:04:19 +02:00

3112 lines
159 KiB
Python

#!/usr/bin/env python3
import contextlib
import hashlib
import re
import json
import platform
import sqlite3
import tempfile
import shutil
import os
import time
import magic
import lz4.block
import pandas as pd
import tldextract
from typing import Union
from urllib import parse
from pathlib import Path
from datetime import datetime
from uuid import uuid4
from playwright.sync_api import sync_playwright, Error, TimeoutError
from PySide6 import QtWidgets, QtGui, QtCore
from Core.GlobalVariables import user_agents
from Core.Interface.Entity import BaseNode
from Core.ResourceHandler import StringPropertyInput, FilePropertyInput, SingleChoicePropertyInput, \
MultiChoicePropertyInput
class MenuBar(QtWidgets.QMenuBar):
browserTabsImportDoneSignalListener = QtCore.Signal(list, str)
def __init__(self, parent):
super().__init__(parent=parent)
self.browserTabsImportDoneSignalListener.connect(self.importBrowserTabsFindings)
fileMenu = self.addMenu("File")
saveAction = QtGui.QAction("&Save",
self,
statusTip="Save The Project",
triggered=self.save)
saveAction.setShortcut("Ctrl+S")
fileMenu.addAction(saveAction)
saveAsAction = QtGui.QAction("Save As",
self,
statusTip="Save the Project in a new directory, under a new name.",
triggered=self.saveAs)
fileMenu.addAction(saveAsAction)
renameAction = QtGui.QAction("Rename Project",
self,
statusTip="Rename the Project.",
triggered=self.rename)
fileMenu.addAction(renameAction)
importMenu = self.addMenu("Import")
fromBrowserAction = QtGui.QAction("From Browser",
self,
statusTip="Import open tabs as Website and materials entities.",
triggered=self.importFromBrowser)
fromTorBrowserAction = QtGui.QAction("From TOR Browser",
self,
statusTip="Import open TOR tabs as Website and materials entities.",
triggered=self.importFromTORBrowser)
fromFileAction = QtGui.QAction("From File",
self,
statusTip="Import entities from a file.",
triggered=self.importFromFile)
graphMLCanvasAction = QtGui.QAction("From GraphML - Canvas",
self,
statusTip="Import canvas from a GraphML file.",
triggered=self.parent().importCanvasFromGraphML)
graphMLDatabaseAction = QtGui.QAction("From GraphML - Database",
self,
statusTip="Import database from a GraphML file.",
triggered=self.parent().importDatabaseFromGraphML)
importMenu.addAction(fromBrowserAction)
importMenu.addAction(fromTorBrowserAction)
importMenu.addAction(fromFileAction)
importMenu.addAction(graphMLCanvasAction)
importMenu.addAction(graphMLDatabaseAction)
exportMenu = self.addMenu("Export")
canvasPictureAction = QtGui.QAction("Save Picture of Canvas", self,
statusTip="Save a picture of your canvas",
triggered=self.savePic)
GraphMLCanvas = QtGui.QAction('To GraphML - Canvas',
self,
statusTip="Export Canvas to GraphML",
triggered=self.parent().exportCanvasToGraphML)
GraphMLDatabase = QtGui.QAction('To GraphML - Database',
self,
statusTip="Export Database to GraphML",
triggered=self.parent().exportDatabaseToGraphML)
exportMenu.addAction(canvasPictureAction)
exportMenu.addAction(GraphMLCanvas)
exportMenu.addAction(GraphMLDatabase)
editSettingsMenu = fileMenu.addMenu("Edit Settings")
editLog = QtGui.QAction('Logging Settings',
self,
statusTip="Edit Logging Settings",
triggered=self.editLogSettings)
editResolution = QtGui.QAction('Resolutions Settings',
self,
statusTip="Edit Resolutions Settings",
triggered=self.editResolutionsSettings)
editProject = QtGui.QAction('Project Settings',
self,
statusTip="Edit Project Settings",
triggered=self.editProjectSettings)
editProgram = QtGui.QAction('Program Settings',
self,
statusTip="Edit Program Settings",
triggered=self.editProgramSettings)
editGraphics = QtGui.QAction('Graphics Settings',
self,
statusTip="Edit Graphics Settings",
triggered=self.editGraphicsSettings)
editSettingsMenu.addAction(editLog)
editSettingsMenu.addAction(editResolution)
editSettingsMenu.addAction(editProject)
editSettingsMenu.addAction(editProgram)
editSettingsMenu.addAction(editGraphics)
exitAction = QtGui.QAction("Exit",
self,
statusTip="Save, Close Project and Exit",
triggered=self.exitSoftware)
fileMenu.addAction(exitAction)
viewMenu = self.addMenu("View")
findAction = QtGui.QAction("&Find",
self,
statusTip="Find Links or Entities by their Primary Field",
triggered=self.findEntityOrLink)
findAction.setShortcut("Ctrl+F")
viewMenu.addAction(findAction)
typeFindAction = QtGui.QAction("Find Entity of Type",
self,
statusTip="Find Entities of a certain type by their Primary Field",
triggered=self.findEntitiesOfType)
viewMenu.addAction(typeFindAction)
regexFindAction = QtGui.QAction("Regex Find",
self,
statusTip="Find Links or Entities by their Primary Field using Regex",
triggered=self.findEntityOrLinkRegex)
viewMenu.addAction(regexFindAction)
regexFindAction.setShortcut("Ctrl+Shift+F")
regexTypeFindAction = QtGui.QAction("Regex Find Entity of Type",
self,
statusTip="Find Entities of a certain type by their Primary Field using "
"Regex",
triggered=self.findEntitiesOfTypeRegex)
viewMenu.addAction(regexTypeFindAction)
viewMenu.addSeparator()
resolutionFindAction = QtGui.QAction("Find &Resolutions",
self,
statusTip="Find Resolutions based on a set of parameters",
triggered=self.findResolutions)
resolutionFindAction.setShortcut("Ctrl+R")
viewMenu.addAction(resolutionFindAction)
runningResolutionsAction = QtGui.QAction("View Running Resolutions",
self,
statusTip="View Running Resolutions",
triggered=self.runningResolutions)
viewMenu.addAction(runningResolutionsAction)
viewMenu.addSeparator()
rearrangeGraphAction = QtGui.QAction("Rearrange Canvas Graph",
self,
statusTip="Rearrange the nodes on the current Canvas to a default "
"configuration according to the currently configured graphing "
"algorithm.",
triggered=self.rearrangeGraph)
viewMenu.addAction(rearrangeGraphAction)
rearrangeAsTimelineAction = QtGui.QAction("Rearrange Canvas Graph as Timeline",
self,
statusTip="Rearrange the nodes on the current Canvas to a "
"Left-to-Right half-tree according to the entities' "
"creation date.",
triggered=self.rearrangeGraphToTimeLine)
viewMenu.addAction(rearrangeAsTimelineAction)
viewMenu.addSeparator()
dockbarVisibilityMenu = viewMenu.addMenu("Toggle Dockbar Visibility")
self.dockbarOneVisibilityAction = QtGui.QAction("Toggle Dockbar One",
self,
statusTip="Toggle the visibility of Dockbar One",
triggered=self.toggleDockbarOneVisibility)
dockbarVisibilityMenu.addAction(self.dockbarOneVisibilityAction)
self.dockbarTwoVisibilityAction = QtGui.QAction("Toggle Dockbar Two",
self,
statusTip="Toggle the visibility of Dockbar Two",
triggered=self.toggleDockbarTwoVisibility)
dockbarVisibilityMenu.addAction(self.dockbarTwoVisibilityAction)
self.dockbarThreeVisibilityAction = QtGui.QAction("Toggle Dockbar Three",
self,
statusTip="Toggle the visibility of Dockbar Three",
triggered=self.toggleDockbarThreeVisibility)
dockbarVisibilityMenu.addAction(self.dockbarThreeVisibilityAction)
toolbarVisibilityMenu = viewMenu.addMenu("Toggle Toolbar Visibility")
self.primaryToolbarVisibilityAction = QtGui.QAction("Toggle Primary Toolbar",
self,
statusTip="Toggle the visibility of the Primary Toolbar",
triggered=self.togglePrimaryToolbarVisibility)
toolbarVisibilityMenu.addAction(self.primaryToolbarVisibilityAction)
nodeOperationsMenu = self.addMenu("Node Operations")
actionSelectAllNodes = QtGui.QAction('Select All Nodes',
self,
statusTip="Select all nodes on this canvas.",
triggered=self.selectAllNodes)
actionSelectAllNodes.setShortcut('Ctrl+A')
nodeOperationsMenu.addAction(actionSelectAllNodes)
actionSelectChildren = QtGui.QAction('Select Child Nodes',
self,
statusTip="Select the child entities of the selected nodes.",
triggered=self.selectChildNodes)
actionSelectChildren.setShortcut('Ctrl+Shift+C')
nodeOperationsMenu.addAction(actionSelectChildren)
actionSelectParents = QtGui.QAction('Select Parent Nodes',
self,
statusTip="Select the parent entities of the selected nodes.",
triggered=self.selectParentNodes)
actionSelectParents.setShortcut('Ctrl+Shift+P')
nodeOperationsMenu.addAction(actionSelectParents)
nodeOperationsMenu.addSeparator()
actionMacrosWizard = QtGui.QAction('Macros...',
self,
statusTip="View, manage and run Macros.",
triggered=self.viewMacrosWizard)
actionMacrosWizard.setShortcut('Ctrl+M')
nodeOperationsMenu.addAction(actionMacrosWizard)
nodeOperationsMenu.addSeparator()
downloadWebsitesAction = QtGui.QAction("Download Selected Websites",
self,
statusTip="Download a full copy of the websites pointed to by the URLs "
"of the selected 'Website' nodes.",
triggered=self.downloadWebsites)
nodeOperationsMenu.addAction(downloadWebsitesAction)
screenshotWebsitesAction = QtGui.QAction("Screenshot Selected Websites",
self,
statusTip="Take a screenshot of the websites pointed to by the URLs "
"of the selected 'Website' nodes.",
triggered=self.screenshotWebsites)
nodeOperationsMenu.addAction(screenshotWebsitesAction)
nodeOperationsMenu.addSeparator()
openWebsiteInBrowserTabAction = QtGui.QAction("Open Selected Websites in Browser",
self,
statusTip="Open the Selected Website entities in new "
"Browser tabs.",
triggered=self.openWebsite)
nodeOperationsMenu.addAction(openWebsiteInBrowserTabAction)
openWebsiteInBrowserTabAction.setShortcut("Ctrl+W")
openURLInBrowserTabAction = QtGui.QAction("Open Contents of URL Attributes in Browser",
self,
statusTip="Open new browser tabs for each valid URL value in all "
"attributes of all selected entities that contain the "
"string 'URL' (case insensitive).",
triggered=self.openURLs)
nodeOperationsMenu.addAction(openURLInBrowserTabAction)
openURLInBrowserTabAction.setShortcut("Ctrl+Shift+W")
nodeOperationsMenu.addSeparator()
searchOnlineAction = QtGui.QAction("Search Engine Query for Selected Entities",
self,
statusTip="Open Browser tabs running search engine queries on a variety of "
"search engines for the primary fields of the selected entities.",
triggered=self.searchOnline)
nodeOperationsMenu.addAction(searchOnlineAction)
searchImageOnlineAction = QtGui.QAction("Open Image Reverse Search Engines in Browser",
self,
statusTip="Open Browser tabs running image search engines. This is a "
"helper function to save time, it does not search "
"automatically.",
triggered=self.searchImageOnline)
nodeOperationsMenu.addAction(searchImageOnlineAction)
nodeOperationsMenu.addSeparator()
notesToTextFilesAction = QtGui.QAction("Save Notes Fields to Text Files",
self,
statusTip="Save the 'Notes' fields of the selected nodes as text files.",
triggered=self.entityNotesToTextFile)
nodeOperationsMenu.addAction(notesToTextFilesAction)
nodeOperationsMenu.addSeparator()
detectCyclesAction = QtGui.QAction("Extract Cycles",
self,
statusTip="Discover cycles in the graph that include the selected nodes, "
"and extract them to different canvases.",
triggered=self.extractCycles)
nodeOperationsMenu.addAction(detectCyclesAction)
projectMenu = self.addMenu("Project Operations")
generateReportAction = QtGui.QAction("Generate Report",
self,
statusTip="Generate a report from the set of currently selected nodes.",
triggered=self.generateReport)
projectMenu.addAction(generateReportAction)
queryAction = QtGui.QAction("Query Wizard",
self,
statusTip="Run LQL Queries.",
triggered=self.queryWizard)
projectMenu.addAction(queryAction)
modulesMenu = self.addMenu("Modules")
reloadModulesAction = QtGui.QAction("Reload Modules", self,
statusTip="Reload all Entities and Transforms from Modules",
triggered=self.reloadModules)
modulesMenu.addAction(reloadModulesAction)
serverMenu = self.addMenu("&Server")
connectAction = QtGui.QAction("Connect", self,
statusTip="Connect to a Server",
triggered=self.serverConnectionWizard)
serverMenu.addAction(connectAction)
disconnectAction = QtGui.QAction("Disconnect", self,
statusTip="Disconnect from any connected server",
triggered=self.disconnectFromServer)
serverMenu.addAction(disconnectAction)
serverMenu.addSeparator()
createOrOpenProjectAction = QtGui.QAction("Create or Open Project", self,
statusTip="Create or Open a new Server project.",
triggered=self.serverOpenOrCreateProject)
serverMenu.addAction(createOrOpenProjectAction)
closeCurrentProjectAction = QtGui.QAction("Close Current Project", self,
statusTip="Close the Server project that is currently open.",
triggered=self.serverCloseProject)
serverMenu.addAction(closeCurrentProjectAction)
deleteProjectAction = QtGui.QAction("Delete Current Project", self,
statusTip="Delete the Server project that is currently opened.",
triggered=self.serverDeleteProject)
serverMenu.addAction(deleteProjectAction)
serverMenu.addSeparator()
syncCanvasAction = QtGui.QAction("Sync Current Canvas", self,
statusTip="Sync the selected canvas with the server's version, or create "
"it on the server if it does not exist.",
triggered=self.syncCurrentCanvas)
serverMenu.addAction(syncCanvasAction)
unSyncCanvasAction = QtGui.QAction("UnSync Current Canvas", self,
statusTip="UnSync the selected canvas from the server's version.",
triggered=self.unSyncCurrentCanvas)
serverMenu.addAction(unSyncCanvasAction)
serverMenu.addSeparator()
reSyncDatabase = QtGui.QAction("Force Re-Sync Database", self,
statusTip="Re-Sync the project's local database with the server's version.",
triggered=self.forceDatabaseSync)
serverMenu.addAction(reSyncDatabase)
reloadServerResolutionsAction = QtGui.QAction("Reload Server Resolutions", self,
statusTip="Reload resolutions from server",
triggered=self.reloadServerResolutions)
serverMenu.addAction(reloadServerResolutionsAction)
reloadServerProjectsAction = QtGui.QAction("Reload Server Projects List", self,
statusTip="Reload the list of projects made available "
"by the server. WARNING: Will disconnect from "
"current project, if any.",
triggered=self.reloadServerProjectsList)
serverMenu.addAction(reloadServerProjectsAction)
reloadServerProjectCanvasesAction = QtGui.QAction("Reload Server Project Canvases List", self,
statusTip="Reload the list of canvases for the current "
"server project. WARNING: Will unsync "
"currently synced canvases, if any.",
triggered=self.reloadServerProjectCanvasesList)
serverMenu.addAction(reloadServerProjectCanvasesAction)
reloadServerProjectFilesAction = QtGui.QAction("Reload Server Project Files List", self,
statusTip="Reload the list of files for the current "
"server project",
triggered=self.reloadServerProjectFilesList)
serverMenu.addAction(reloadServerProjectFilesAction)
serverMenu.addSeparator()
uploadFilesAction = QtGui.QAction("Upload Selected Files", self,
statusTip="Upload selected Materials entity files to Server.",
triggered=self.uploadFiles)
serverMenu.addAction(uploadFilesAction)
downloadFilesAction = QtGui.QAction("Download Files", self,
statusTip="Download files from the Server that correspond to the selected "
"Materials entities.",
triggered=self.downloadFiles)
serverMenu.addAction(downloadFilesAction)
serverMenu.addSeparator()
manageCollectorsAction = QtGui.QAction("Manage Collectors", self,
statusTip="Start and / or stop Collectors for this project.",
triggered=self.manageCollectors)
serverMenu.addAction(manageCollectorsAction)
# Use this function to change the labels of functions when necessary.
# Not very efficient, but much more intuitive than creating a ton of different menus
# and mashing them together.
def mousePressEvent(self, arg__1: QtGui.QMouseEvent) -> None:
super(MenuBar, self).mousePressEvent(arg__1)
self.dockbarOneVisibilityAction.setText("Hide Dockbar One" if self.parent().dockbarOne.isVisible() else
"Show Dockbar One")
self.dockbarTwoVisibilityAction.setText("Hide Dockbar Two" if self.parent().dockbarTwo.isVisible() else
"Show Dockbar Two")
self.dockbarThreeVisibilityAction.setText("Hide Dockbar Three" if self.parent().dockbarThree.isVisible() else
"Show Dockbar Three")
self.primaryToolbarVisibilityAction.setText("Hide Primary Toolbar"
if self.parent().primaryToolbar.isVisible() else
"Show Primary Toolbar")
def toggleDockbarOneVisibility(self) -> None:
if self.parent().dockbarOne.isVisible():
self.parent().dockbarOne.setVisible(False)
else:
self.parent().dockbarOne.setVisible(True)
def toggleDockbarTwoVisibility(self) -> None:
if self.parent().dockbarTwo.isVisible():
self.parent().dockbarTwo.setVisible(False)
else:
self.parent().dockbarTwo.setVisible(True)
def toggleDockbarThreeVisibility(self) -> None:
if self.parent().dockbarThree.isVisible():
self.parent().dockbarThree.setVisible(False)
else:
self.parent().dockbarThree.setVisible(True)
def togglePrimaryToolbarVisibility(self) -> None:
if self.parent().primaryToolbar.isVisible():
self.parent().primaryToolbar.setVisible(False)
else:
self.parent().primaryToolbar.setVisible(True)
def importFromFile(self) -> None:
importDialog = ImportFromFileDialog(self)
importDialogAccept = importDialog.exec_()
newNodes = []
newLinks = []
fileDirectory = Path(importDialog.fileDirectoryLine.text())
if importDialogAccept and fileDirectory != '':
if fileDirectory.exists() and fileDirectory.is_file():
sceneToAddTo = None
try:
if importDialog.textFileChoice.isChecked():
fileContents = []
with open(fileDirectory, 'r') as importFile:
# Read a maximum of 3 lines from the file:
for index, line in enumerate(importFile):
fileContents.append(line.strip())
if index > 2:
break
importTextFileDialog = ImportFromTextFileDialog(self, fileContents)
if importTextFileDialog.exec_():
if importTextFileDialog.importToCanvasCheckbox.isChecked():
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
importTextFileDialog.importToCanvasDropdown.currentText())
selectedEntityType = importTextFileDialog.importTypeDropdown.currentText()
primary_field = importTextFileDialog.typePrimaryFieldValueLabel.text()
with open(fileDirectory, 'r') as importFile:
for line in importFile:
lineValue = line.strip()
if lineValue:
primaryAttr = lineValue.strip()
newEntityJSON = {primary_field: primaryAttr,
'Entity Type': selectedEntityType}
if newEntityJSON not in newNodes:
existingEntity = self.parent().LENTDB.getEntityOfType(primaryAttr,
selectedEntityType)
if existingEntity is None:
newNodes.append(newEntityJSON)
else:
existingEntity.update(newEntityJSON)
# We'll refresh the timeline later.
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
elif importDialog.CSVFileChoice.isChecked():
try:
csvDF = pd.read_excel(fileDirectory)
except ValueError:
csvDF = pd.read_csv(fileDirectory)
# If we have no rows or columns, we cannot import anything.
# This is essentially a sanity check.
if len(csvDF.index) < 1:
raise ValueError("Invalid import file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid import file data - Not enough columns.")
# Remove duplicate column names
# When importing, all columns should be given unique values,
# so this should be just another sanity check.
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
importEntityCSVDialog = ImportEntityFromCSVFile(self, csvDF)
if importEntityCSVDialog.exec_():
attributeRows = [comboBox.currentText()
if comboBox.currentText() else
csvDF.columns[index]
for index, comboBox in
enumerate(importEntityCSVDialog.fieldMappingComboBoxes)]
if importEntityCSVDialog.importToCanvasCheckbox.isChecked():
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(
importEntityCSVDialog.importToCanvasDropdown.currentText())
entityTypeToImportAs = importEntityCSVDialog.entityTypeChoiceDropdown.currentText()
for row in csvDF.itertuples(index=False):
newEntityJSON = {str(value).strip(): str(row[index]).strip()
for index, value in enumerate(attributeRows)}
newEntityJSON['Entity Type'] = entityTypeToImportAs
if newEntityJSON not in newNodes:
primaryAttr = newEntityJSON[
self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
entityTypeToImportAs)]
existingEntity = self.parent().LENTDB.getEntityOfType(
primaryAttr, entityTypeToImportAs)
if existingEntity is None:
newNodes.append(newEntityJSON)
else:
existingEntity.update(newEntityJSON)
# We'll refresh the timeline later.
self.parent().LENTDB.addEntity(existingEntity, updateTimeline=False)
elif importDialog.CSVFileChoiceLinks.isChecked():
try:
csvDF = pd.read_excel(fileDirectory)
except ValueError:
csvDF = pd.read_csv(fileDirectory)
# If we have no rows or columns, we cannot import anything.
# This is essentially a sanity check.
if len(csvDF.index) < 1:
raise ValueError("Invalid import file data - Not enough rows.")
if len(csvDF.columns) < 1:
raise ValueError("Invalid import file data - Not enough columns.")
# Remove duplicate column names
# When importing, all columns should be given unique values,
# so this should be just another sanity check.
csvDF = csvDF.loc[:, ~csvDF.columns.duplicated()]
# Fill NaN values with an empty string
csvDF.fillna('')
importLinksCSVDialog = ImportLinksFromCSVFile(self, csvDF)
if importLinksCSVDialog.exec_():
unmapped = []
for columnIndex, columnValue in enumerate(importLinksCSVDialog.fieldMappingComboBoxes):
columnMapping = columnValue.currentText()
if columnMapping:
csvDF.rename(columns={csvDF.columns[columnIndex]: columnMapping}, inplace=True)
elif not importLinksCSVDialog.fieldIncludeCheckBoxes[columnIndex].isChecked():
unmapped.append(csvDF.columns[columnIndex])
if unmapped:
fieldsRemainingDF = csvDF[unmapped].copy()
for unmappedField in unmapped:
csvDF.drop(unmappedField, axis=1, inplace=True)
createLinkEntitiesDialog = ImportLinkEntitiesFromCSVFile(self, fieldsRemainingDF)
if createLinkEntitiesDialog.exec_():
randomizePrimary = createLinkEntitiesDialog.randomizeLabel.isChecked()
extraLink = createLinkEntitiesDialog.extraLinkLabel.isChecked()
entityOneType = importLinksCSVDialog.entityOneTypeChoiceDropdown.currentText()
entityTwoType = importLinksCSVDialog.entityTwoTypeChoiceDropdown.currentText()
attributeRows = [comboBox.currentText()
if comboBox.currentText()
else fieldsRemainingDF.columns[index]
for index, comboBox
in enumerate(createLinkEntitiesDialog.fieldMappingComboBoxes)]
entityTypeToImportAs = \
createLinkEntitiesDialog.entityTypeChoiceDropdown.currentText()
newEntityPrimaryAttribute = \
self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(entityTypeToImportAs)
for entityRow, linkRow in zip(fieldsRemainingDF.itertuples(index=False),
csvDF.itertuples(index=False)):
linkJSON = {}
entityOneJSON = {}
entityTwoJSON = {}
resolutionID = ""
notes = ""
for count, column in enumerate(linkRow):
column = str(column)
mapping = csvDF.columns[count]
if mapping == 'Entity One':
entityOneJSON = self.parent().LENTDB.getEntityOfType(column,
entityOneType)
# Add the entity if it doesn't exist.
if entityOneJSON is None:
entityOneJSON = self.parent().LENTDB.addEntity({
self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
entityOneType): column, 'Entity Type': entityOneType})
elif mapping == 'Entity Two':
entityTwoJSON = self.parent().LENTDB.getEntityOfType(column,
entityTwoType)
# Add the entity if it doesn't exist.
if entityTwoJSON is None:
entityTwoJSON = self.parent().LENTDB.addEntity({
self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
entityTwoType): column, 'Entity Type': entityTwoType})
elif mapping == 'Notes':
notes = column
elif mapping == 'Resolution ID':
resolutionID = column
else:
linkJSON[mapping] = column
# We still need to check if both nodes exist, since errors may have occurred
# during their creation.
if (entityOneJSON is not None) and (entityTwoJSON is not None):
linkJSON['Notes'] = notes
if importLinksCSVDialog.randAsIs.isChecked():
linkJSON['Resolution'] = resolutionID
elif importLinksCSVDialog.randMerge.isChecked():
linkJSON['Resolution'] = resolutionID + ' | ' + str(uuid4())
elif importLinksCSVDialog.randReplace.isChecked():
linkJSON['Resolution'] = str(uuid4())
# If 'Resolution ID' is not mapped, generate random IDs.
if not linkJSON.get('Resolution'):
linkJSON['Resolution'] = str(uuid4())
linkJSONOne = dict(linkJSON)
linkJSONOne['Resolution'] += ' OUT'
linkJSONTwo = dict(linkJSON)
linkJSONTwo['Resolution'] += ' IN'
newEntityJSON = {str(value): str(entityRow[index]).strip()
for index, value in enumerate(attributeRows)}
newEntityJSON['Entity Type'] = entityTypeToImportAs
if randomizePrimary:
newEntityJSON[newEntityPrimaryAttribute] += f' | {str(uuid4())}'
newNode = self.parent().LENTDB.getEntityOfType(
newEntityJSON[newEntityPrimaryAttribute], entityTypeToImportAs)
if newNode is None:
newNode = self.parent().LENTDB.addEntity(newEntityJSON,
updateTimeline=False)
else:
newNode.update(newEntityJSON)
self.parent().LENTDB.addEntity(newNode, updateTimeline=False)
linkJSONOne['uid'] = (entityOneJSON['uid'], newNode['uid'])
linkJSONTwo['uid'] = (newNode['uid'], entityTwoJSON['uid'])
if self.parent().LENTDB.addLink(linkJSONOne) is not None:
newLinks.append((entityOneJSON['uid'], newNode['uid'],
linkJSONOne['Resolution']))
if self.parent().LENTDB.addLink(linkJSONTwo) is not None:
newLinks.append((newNode['uid'], entityTwoJSON['uid'],
linkJSONTwo['Resolution']))
if extraLink:
linkJSONThree = dict(linkJSON)
linkJSONThree['uid'] = (entityOneJSON['uid'], entityTwoJSON['uid'])
if self.parent().LENTDB.addLink(linkJSONThree) is not None:
newLinks.append((entityOneJSON['uid'], entityTwoJSON['uid'],
linkJSONThree['Resolution']))
else:
entityOneType = importLinksCSVDialog.entityOneTypeChoiceDropdown.currentText()
entityTwoType = importLinksCSVDialog.entityTwoTypeChoiceDropdown.currentText()
for row in csvDF.itertuples(index=False):
linkJSON = {}
entityOneJSON = {}
entityTwoJSON = {}
resolutionID = ""
notes = ""
for count, column in enumerate(row):
column = str(column)
mapping = csvDF.columns[count]
if mapping == 'Entity One':
entityOneJSON = self.parent().LENTDB.getEntityOfType(column,
entityOneType)
elif mapping == 'Entity Two':
entityTwoJSON = self.parent().LENTDB.getEntityOfType(column,
entityTwoType)
elif mapping == 'Notes':
notes = column
elif mapping == 'Resolution ID':
resolutionID = column
else:
linkJSON[mapping] = column
if (entityOneJSON is not None) and (entityTwoJSON is not None):
linkJSON['uid'] = (entityOneJSON['uid'], entityTwoJSON['uid'])
linkJSON['Notes'] = notes
if importLinksCSVDialog.randAsIs.isChecked():
linkJSON['Resolution'] = resolutionID
elif importLinksCSVDialog.randMerge.isChecked():
linkJSON['Resolution'] = resolutionID + ' | ' + str(uuid4())
elif importLinksCSVDialog.randReplace.isChecked():
linkJSON['Resolution'] = str(uuid4())
# If 'Resolution ID' is not mapped, generate random IDs.
if not linkJSON.get('Resolution'):
linkJSON['Resolution'] = str(uuid4())
if self.parent().LENTDB.addLink(linkJSON) is not None:
newLinks.append((entityOneJSON['uid'], entityTwoJSON['uid'],
linkJSON['Resolution']))
newNodeUIDs = [newEntity['uid'] for newEntity in self.parent().LENTDB.addEntities(newNodes)]
if sceneToAddTo is not None:
for newNodeUID in newNodeUIDs:
if newNodeUID is not None:
sceneToAddTo.addNodeProgrammatic(newNodeUID)
sceneToAddTo.rearrangeGraph()
if newLinks:
self.parent().centralWidget().tabbedPane.addLinksToTabs(newLinks, 'File Links',
linkGroupingOverride=True)
self.parent().LENTDB.resetTimeline()
except PermissionError:
self.parent().MESSAGEHANDLER.error('No permission to access the file at the path provided.',
popUp=True, exc_info=False)
except UnicodeDecodeError:
self.parent().MESSAGEHANDLER.error('File path provided points to a binary file that cannot be '
'interpreted as text.', popUp=True, exc_info=False)
except FileNotFoundError:
self.parent().MESSAGEHANDLER.error('File path provided does not point to an existing file.',
popUp=True, exc_info=False)
except TypeError:
self.parent().MESSAGEHANDLER.error('Type Error occurred while processing file. Please ensure that '
'you have selected a file of the appropriate type.',
popUp=True, exc_info=False)
except Exception as e:
self.parent().MESSAGEHANDLER.error('Exception occurred while processing file: ' + str(e),
popUp=True, exc_info=False)
else:
self.parent().MESSAGEHANDLER.error('Invalid file path provided!', popUp=True, exc_info=False)
def savePic(self) -> None:
canvasSaveDialog = CanvasPictureDialog(self)
canvasSaveDialogAccept = canvasSaveDialog.exec_()
fileDirectory = canvasSaveDialog.fileDirectory
if canvasSaveDialogAccept and fileDirectory != '':
canvas = canvasSaveDialog.chosenCanvasDropdown.currentText()
justViewport = canvasSaveDialog.justViewportChoice.isChecked()
transparentBackground = canvasSaveDialog.transparentChoice.isChecked()
picture = self.parent().getPictureOfCanvas(canvas, justViewport, transparentBackground)
picture.save(fileDirectory, "PNG")
def save(self) -> None:
self.parent().saveProject()
def saveAs(self) -> None:
self.parent().saveAsProject()
def rename(self) -> None:
self.parent().renameProjectPromptName()
def editSettings(self) -> None:
self.parent().editSettings()
def editLogSettings(self) -> None:
self.parent().editLogSettings()
def editProjectSettings(self) -> None:
self.parent().editProjectSettings()
def editProgramSettings(self) -> None:
self.parent().editProgramSettings()
def editGraphicsSettings(self) -> None:
self.parent().changeGraphics()
def editResolutionsSettings(self) -> None:
self.parent().editResolutionsSettings()
def exitSoftware(self) -> None:
self.parent().close()
def reloadModules(self) -> None:
self.parent().reloadModules()
def runningResolutions(self) -> None:
self.parent().cleanUpLocalFinishedResolutions()
runningResolutionsDialog = ViewAndStopResolutionsDialog(self.parent())
runningResolutionsDialog.exec()
# Server Operations
def serverConnectionWizard(self) -> None:
serverWizard = ServerConnectWizard(self)
serverWizard.exec_()
serverPassword = serverWizard.serverPasswordTextbox.text()
serverIP = serverWizard.serverIPTextbox.text()
serverPort = int(serverWizard.serverPortTextbox.text())
if not serverWizard.confirmConnect:
self.parent().MESSAGEHANDLER.debug("Cancelled connecting to server")
return
if isinstance(serverPassword, str) and \
isinstance(serverIP, str) and \
isinstance(serverPort, int) and \
0 < serverPort < 65535:
self.parent().connectToServer(serverPassword, serverIP, serverPort)
else:
self.parent().MESSAGEHANDLER.info("Invalid Information Provided",
popUp=True)
def disconnectFromServer(self) -> None:
self.parent().disconnectFromServer()
def serverOpenOrCreateProject(self) -> None:
if self.parent().FCOM.isConnected():
serverProjectDialog = ServerCreateOrOpenProject(self.parent())
if serverProjectDialog.exec():
if serverProjectDialog.openProject:
self.parent().FCOM.openProject(serverProjectDialog.projectName,
serverProjectDialog.projectPass)
else:
self.parent().FCOM.createProject(serverProjectDialog.projectName,
serverProjectDialog.projectPass)
else:
self.parent().MESSAGEHANDLER.warning('Not Connected to Server!', popUp=True)
self.parent().setStatus('Must connect to a server before opening a project.')
def serverCloseProject(self) -> None:
self.parent().closeCurrentServerProject()
def serverDeleteProject(self) -> None:
if self.parent().FCOM.isConnected():
current_project = self.parent().SETTINGS.value("Project/Server/Project")
if current_project != "":
confirmProjectDeleteDialog = DeleteProjectConfirmationDialog(self.parent(), current_project)
if confirmProjectDeleteDialog.exec_():
self.parent().MESSAGEHANDLER.info('Deleting server project: ' + current_project)
self.parent().FCOM.deleteProject(current_project)
else:
self.parent().MESSAGEHANDLER.warning('Cannot Delete Server Project: No Server Project is '
'currently open!', popUp=True)
self.parent().setStatus('Not connected to a Server Project, nothing to delete.')
else:
self.parent().MESSAGEHANDLER.warning('Cannot Delete Server Project: Not Connected to Server!', popUp=True)
self.parent().setStatus('Not connected to Server, nothing to delete.')
# For canvas stuff, check if canvas name exists.
def syncCurrentCanvas(self) -> None:
self.parent().syncCanvasByName()
def unSyncCurrentCanvas(self) -> None:
self.parent().unSyncCurrentCanvas()
def reloadServerResolutions(self) -> None:
if self.parent().FCOM.isConnected():
self.parent().RESOLUTIONMANAGER.removeServerResolutions()
self.parent().dockbarOne.resolutionsPalette.loadAllResolutions()
self.parent().FCOM.askServerForResolutions()
def reloadServerProjectsList(self) -> None:
if self.parent().FCOM.isConnected():
if self.parent().SETTINGS.value("Project/Server/Project") != '':
self.parent().closeCurrentServerProject()
self.parent().receiveProjectsListListener([])
self.parent().FCOM.askProjectsList()
def reloadServerProjectCanvasesList(self) -> None:
if self.parent().FCOM.isConnected():
project_name = self.parent().SETTINGS.value("Project/Server/Project")
self.parent().unSyncCanvasByName(None)
self.parent().receiveProjectCanvasesListListener([])
self.parent().FCOM.askProjectCanvasesList(project_name)
def reloadServerProjectFilesList(self):
if self.parent().FCOM.isConnected():
project_name = self.parent().SETTINGS.value("Project/Server/Project")
self.parent().receiveFileListListener([])
self.parent().FCOM.askServerForFileList(project_name)
def forceDatabaseSync(self) -> None:
self.parent().syncDatabase()
def uploadFiles(self) -> None:
self.parent().uploadFiles()
def downloadFiles(self) -> None:
self.parent().downloadFiles()
def manageCollectors(self) -> None:
with self.parent().serverCollectorsLock:
currentCollectors = dict(self.parent().collectors)
runningCollectors = dict(self.parent().runningCollectors)
collectorsDialog = CollectorsDialog(self.parent(),
currentCollectors if self.parent().FCOM.isConnected() else None,
runningCollectors)
collectorsDialog.exec()
def selectAllNodes(self) -> None:
self.parent().centralWidget().tabbedPane.getCurrentScene().selectAllNodes()
def selectChildNodes(self) -> None:
self.parent().centralWidget().tabbedPane.getCurrentScene().selectChildNodes()
def selectParentNodes(self) -> None:
self.parent().centralWidget().tabbedPane.getCurrentScene().selectParentNodes()
def viewMacrosWizard(self) -> None:
self.parent().showMacrosDialog()
def findEntityOrLink(self) -> None:
self.parent().findEntityOrLinkOnCanvas()
def findEntityOrLinkRegex(self) -> None:
self.parent().findEntityOrLinkOnCanvas(regex=True)
def findEntitiesOfType(self) -> None:
self.parent().findEntityOfTypeOnCanvas()
def findEntitiesOfTypeRegex(self) -> None:
self.parent().findEntityOfTypeOnCanvas(regex=True)
def findResolutions(self) -> None:
self.parent().findResolution()
def openWebsite(self) -> None:
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
if itemJSON.get('Entity Type') == 'Website':
try:
QtGui.QDesktopServices.openUrl(itemJSON['URL'])
except KeyError:
continue
def openURLs(self) -> None:
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
for attribute in itemJSON:
if 'url' in attribute.lower():
value = itemJSON[attribute]
if isinstance(value, str):
try:
parsedValue = parse.urlparse(value)
if all([parsedValue.scheme, parsedValue.netloc]):
QtGui.QDesktopServices.openUrl(value)
except (KeyError, AttributeError):
continue
def searchOnline(self) -> None:
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
selectedURLs = SearchEngineDialog(self)
if selectedURLs.exec_():
selectedEngineURLs = [engineCheckbox.searchAttr for engineCheckbox in selectedURLs.searchEngineWidgets
if engineCheckbox.isChecked()]
searchTerms = []
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
primaryField = str(itemJSON[list(itemJSON)[1]])
primaryField = parse.quote(primaryField, "")
searchTerms.append('"' + primaryField + '"')
for searchEngineURL in selectedEngineURLs:
try:
QtGui.QDesktopServices.openUrl(searchEngineURL + " ".join(searchTerms))
except KeyError:
continue
def searchImageOnline(self) -> None:
selectedURLs = SearchImageEngineDialog(self)
if selectedURLs.exec_():
selectedEngineURLs = [engineCheckbox.searchAttr for engineCheckbox in selectedURLs.searchEngineWidgets
if engineCheckbox.isChecked()]
for searchEngineURL in selectedEngineURLs:
try:
QtGui.QDesktopServices.openUrl(searchEngineURL)
except KeyError:
continue
def rearrangeGraph(self) -> None:
self.parent().centralWidget().tabbedPane.getCurrentScene().rearrangeGraph()
def rearrangeGraphToTimeLine(self) -> None:
self.parent().centralWidget().tabbedPane.getCurrentScene().rearrangeGraphTimeline()
def generateReport(self):
self.parent().generateReport()
def queryWizard(self):
self.parent().launchQueryWizard()
def downloadWebsites(self) -> None:
websiteEntities = []
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
if itemJSON.get('Entity Type') == 'Website':
try:
websiteEntities.append((itemJSON['uid'], itemJSON['URL']))
except KeyError:
continue
if not websiteEntities:
self.parent().MESSAGEHANDLER.warning('Please select the "Website" nodes that correspond to the sites that '
'you wish to download, and re-run the Download Selected Websites '
'operation.',
popUp=True)
return
downloadThread = SaveWebsiteThread(self, self.parent(), websiteEntities)
steps = len(websiteEntities) + 1
progress = QtWidgets.QProgressDialog('Downloading websites, please wait...',
'Abort', 0, steps, self.parent())
progress.setMinimumDuration(1500)
downloadThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: downloadThread.cancelOperation())
downloadThread.start()
def screenshotWebsites(self) -> None:
websiteEntities = []
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
if itemJSON.get('Entity Type') == 'Website':
try:
websiteEntities.append((itemJSON['uid'], itemJSON['URL']))
except KeyError:
continue
if not websiteEntities:
self.parent().MESSAGEHANDLER.warning('Please select the "Website" nodes that correspond to the sites that '
'you wish to download, and re-run the Download Selected Websites '
'operation.',
popUp=True)
return
screenshotThread = ScreenshotWebsiteThread(self, self.parent(), websiteEntities)
steps = len(websiteEntities) + 1
progress = QtWidgets.QProgressDialog('Screenshotting websites, please wait...',
'Abort', 0, steps, self.parent())
progress.setMinimumDuration(1500)
screenshotThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: screenshotThread.cancelOperation())
screenshotThread.start()
def entityNotesToTextFile(self) -> None:
baseFilesPath = Path(self.parent().SETTINGS.value('Project/FilesDir'))
currentScene = self.parent().centralWidget().tabbedPane.getCurrentScene()
newNodes = []
for item in currentScene.selectedItems():
if isinstance(item, BaseNode):
itemJSON = self.parent().LENTDB.getEntity(item.uid)
itemJSONNotes = itemJSON.get('Notes', '').strip()
if itemJSONNotes != '':
itemPrimaryField = itemJSON[self.parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
itemJSON['Entity Type'])]
fileName = itemPrimaryField + ' | ' + itemJSON.get('Date Last Edited', str(time.time_ns())) + '.txt'
fileName = fileName.replace('/', '+')
fileName = fileName.replace('\\', '+')
with open(baseFilesPath / fileName, "w") as f:
f.write(itemJSONNotes)
newNodes.append([{'Document Name': fileName,
'File Path': fileName,
'Entity Type': 'Document'},
{itemJSON['uid']: {'Resolution': 'Notes to Text File', 'Notes': ''}}])
self.parent().facilitateResolutionSignalListener.emit('Notes to Text File Operation', newNodes)
def extractCycles(self) -> None:
self.parent().extractCycles()
def importFromBrowser(self) -> None:
"""
Import session tabs to canvas. Optionally, also take a screenshot of them.
Assumes default browser profiles.
:return:
"""
if platform.system() not in ['Linux', 'Windows']:
self.parent().setStatus('Importing tabs not supported on platforms other than Linux and Windows.')
self.parent().MESSAGEHANDLER.warning('Importing tabs not supported on platforms '
'other than Linux and Windows.', popUp=True)
return
importDialog = BrowserImportDialog(self)
if importDialog.exec_():
importTabsThread = ImportBrowserTabsThread(importDialog, self.parent(), self)
steps = 3
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
'Abort Import', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(0)
importTabsThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: importTabsThread.cancelOperation())
importTabsThread.start()
def importFromTORBrowser(self) -> None:
"""
Import TOR session tabs to canvas.
:return:
"""
importDialog = TORBrowserImportDialog(self)
if importDialog.exec_():
importTorTabsThread = ImportTorBrowserTabsThread(importDialog, self.parent(), self)
steps = 4
progress = QtWidgets.QProgressDialog('Importing tabs, please wait...',
'Abort Import', 0, steps, self)
progress.setWindowModality(QtCore.Qt.WindowModality.WindowModal)
progress.setMinimumDuration(0)
importTorTabsThread.progressSignal.connect(progress.setValue)
progress.canceled.connect(lambda: importTorTabsThread.cancelOperation())
importTorTabsThread.start()
def firefoxCookiesHelper(self, cookiesDatabasePath: Path) -> list:
"""
Used by threads to get firefox's cookies.
@param cookiesDatabasePath:
@return:
"""
newCookiesDatabase = tempfile.mkstemp(suffix='.sqlite')
newCookiesDatabasePath = Path(newCookiesDatabase[1])
# Try to copy the database a few times, so we can access it
# with sqlite (original is locked)
copiedFile = False
for _ in range(5):
shutil.copyfile(cookiesDatabasePath, newCookiesDatabasePath)
originalDigest = self.cookieFileHashHelper(cookiesDatabasePath)
newDigest = self.cookieFileHashHelper(newCookiesDatabasePath)
if originalDigest == newDigest:
copiedFile = True
break
else:
time.sleep(0.2)
browserCookies = []
if not copiedFile:
self.parent().warningSignalListener.emit('Could not access Firefox cookies.', True)
else:
cookiesDB = sqlite3.connect(newCookiesDatabasePath)
for cookie in cookiesDB.execute('SELECT name,value,host,path,expiry,isSecure,'
'isHttpOnly,sameSite FROM moz_cookies'):
newCookie = {'name': cookie[0], 'value': cookie[1],
'domain': cookie[2], 'path': cookie[3],
'expires': cookie[4], 'secure': bool(cookie[5]),
'httpOnly': bool(cookie[6])}
if cookie[7] == 0:
newCookie['sameSite'] = "None"
elif cookie[7] == 1:
newCookie['sameSite'] = "Lax"
elif cookie[7] == 2:
newCookie['sameSite'] = "Strict"
browserCookies.append(newCookie)
cookiesDB.close()
newCookiesDatabasePath.unlink(missing_ok=True)
return browserCookies
def cookieFileHashHelper(self, filePath: Path):
cookieHash = hashlib.md5() # nosec
with open(filePath, 'rb') as cookieFile:
for chunk in iter(lambda: cookieFile.read(4096), b""):
cookieHash.update(chunk)
return cookieHash.digest()
def importBrowserTabsFindings(self, resolution_result: list, canvasToImportTo: Union[str, None] = None) -> None:
for finding in resolution_result:
if len(finding) < 2:
# Add dummy parents
finding.append({'@^@^@^@': {'Resolution': 'Browser Import', 'Notes': ''}})
newNodeUIDs = self.parent().centralWidget().tabbedPane.facilitateResolution('Importing Entities from Browser',
resolution_result)
if canvasToImportTo:
sceneToAddTo = self.parent().centralWidget().tabbedPane.getSceneByName(canvasToImportTo)
for newNodeUID in newNodeUIDs:
if newNodeUID is not None and newNodeUID not in sceneToAddTo.sceneGraph.nodes:
sceneToAddTo.addNodeProgrammatic(newNodeUID)
sceneToAddTo.rearrangeGraph()
class DeleteProjectConfirmationDialog(QtWidgets.QDialog):
def __init__(self, mainWindowObject, currentServerProject: str):
super(DeleteProjectConfirmationDialog, self).__init__()
self.setModal(True)
self.setLayout(QtWidgets.QVBoxLayout())
self.mainWindowObject = mainWindowObject
self.setWindowTitle('Delete Server Project')
resolutionsLabel = QtWidgets.QLabel(f'Delete Project: "{currentServerProject}" ?')
resolutionsLabel.setWordWrap(True)
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(resolutionsLabel)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.accept)
self.layout().addWidget(buttonsWidget)
class BrowserImportDialog(QtWidgets.QDialog):
def __init__(self, parent):
super(BrowserImportDialog, self).__init__(parent=parent)
self.setWindowTitle('Import Browser Tabs')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Choose open browser(s) to import tabs from. Note that support'
' for chromium-based browsers is experimental, and may not'
' always import all open tabs.\n\nAlso note that session files are used '
'to read session data, so please ensure that you\'ve waited about half '
'a minute or so before running this.')
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
firefoxGroup = QtWidgets.QGroupBox('Firefox Options')
firefoxGroupLayout = QtWidgets.QVBoxLayout()
firefoxGroup.setLayout(firefoxGroupLayout)
self.firefoxChoice = QtWidgets.QCheckBox('Get tabs from Firefox')
self.firefoxSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
firefoxGroupLayout.addWidget(self.firefoxChoice)
firefoxGroupLayout.addWidget(self.firefoxSessionChoice)
chromeGroup = QtWidgets.QGroupBox('Chrome Options')
chromeGroupLayout = QtWidgets.QVBoxLayout()
chromeGroup.setLayout(chromeGroupLayout)
self.chromeChoice = QtWidgets.QCheckBox('Get tabs from Chrome / Chromium (Experimental)')
chromeGroupLayout.addWidget(self.chromeChoice)
dialogLayout.addWidget(firefoxGroup, 1, 0, 1, 2)
dialogLayout.addWidget(chromeGroup, 2, 0, 1, 2)
self.importScreenshotsCheckbox = QtWidgets.QCheckBox('Take screenshots of sites')
dialogLayout.addWidget(self.importScreenshotsCheckbox, 3, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 4, 0, 1, 1)
dialogLayout.addWidget(self.importToCanvasDropdown, 4, 1, 1, 1)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
cancelButton = QtWidgets.QPushButton('Cancel')
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
self.setMaximumWidth(450)
self.setMinimumWidth(450)
self.setMaximumHeight(400)
self.setMinimumHeight(400)
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
class TORBrowserImportDialog(QtWidgets.QDialog):
def __init__(self, parent):
super(TORBrowserImportDialog, self).__init__(parent=parent)
self.setWindowTitle('Import TOR Browser Tabs')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
self.entireSessionChoice = QtWidgets.QCheckBox('Get entire session instead of latest tabs')
dialogLayout.addWidget(self.entireSessionChoice, 0, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 1, 0, 1, 2)
dialogLayout.addWidget(self.importToCanvasDropdown, 2, 0, 1, 2)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
cancelButton = QtWidgets.QPushButton('Cancel')
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
dialogLayout.addWidget(cancelButton, 4, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 4, 1, 1, 1)
class ServerConnectWizard(QtWidgets.QDialog):
"""
Dialog Window that lets the user input the details of the server that
they want to connect to.
"""
def __init__(self, parent):
super().__init__(parent=parent)
self.setLayout(QtWidgets.QFormLayout())
self.setWindowTitle("Server Connection Wizard")
# self.setMinimumSize(550,200)
serverIPLabel = QtWidgets.QLabel("Server IP:")
self.serverIPTextbox = QtWidgets.QLineEdit()
self.layout().addRow(serverIPLabel, self.serverIPTextbox)
serverPortLabel = QtWidgets.QLabel("Server Port:")
self.serverPortTextbox = QtWidgets.QLineEdit("3777")
self.layout().addRow(serverPortLabel, self.serverPortTextbox)
serverPasswordLabel = QtWidgets.QLabel("Server Password:")
self.serverPasswordTextbox = QtWidgets.QLineEdit()
self.serverPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.layout().addRow(serverPasswordLabel, self.serverPasswordTextbox)
self.confirmConnect = False
confirmButton = QtWidgets.QPushButton("Confirm")
cancelButton = QtWidgets.QPushButton("Cancel")
self.layout().addRow(cancelButton, confirmButton)
confirmButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
def accept(self):
self.confirmConnect = True
super().accept()
class ServerCreateOrOpenProject(QtWidgets.QDialog):
def __init__(self, mainWindow):
super(ServerCreateOrOpenProject, self).__init__(parent=mainWindow)
self.setModal(True)
with mainWindow.serverProjectsLock:
serverProjects = mainWindow.serverProjects
self.setLayout(QtWidgets.QVBoxLayout())
self.openProject = True
self.projectName = ''
self.projectPass = ''
openProjectWidget = QtWidgets.QWidget()
openProjectLayout = QtWidgets.QFormLayout()
openProjectWidget.setLayout(openProjectLayout)
self.openProjectDropdown = QtWidgets.QComboBox()
self.openProjectDropdown.setToolTip('Select the name of the server project to work on.')
self.openProjectDropdown.setEditable(False)
self.openProjectDropdown.addItems(serverProjects)
self.openProjectPassword = QtWidgets.QLineEdit('')
self.openProjectPassword.setToolTip('Enter the password of the selected server project.')
self.openProjectPassword.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
openProjectLayout.addRow('Open Project:', self.openProjectDropdown)
openProjectLayout.addRow('Password:', self.openProjectPassword)
openProjectButton = QtWidgets.QPushButton('Open Project')
self.layout().addWidget(openProjectWidget)
self.layout().addWidget(openProjectButton)
createProjectWidget = QtWidgets.QWidget()
createProjectLayout = QtWidgets.QFormLayout()
createProjectWidget.setLayout(createProjectLayout)
self.createProjectNameTextbox = QtWidgets.QLineEdit('')
self.createProjectNameTextbox.setToolTip('Specify the name of the server project. Must be unique.')
self.createProjectPasswordTextbox = QtWidgets.QLineEdit('')
self.createProjectPasswordTextbox.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password)
self.createProjectPasswordTextbox.setToolTip('Specify a password to be entered'
' in order to access this project.')
createProjectLayout.addRow('Project Name:', self.createProjectNameTextbox)
createProjectLayout.addRow('Project Password:', self.createProjectPasswordTextbox)
createProjectButton = QtWidgets.QPushButton('Create Server Project')
self.layout().addWidget(createProjectWidget)
self.layout().addWidget(createProjectButton)
openProjectButton.clicked.connect(self.openExistingProject)
createProjectButton.clicked.connect(self.createNewProject)
def createNewProject(self) -> None:
self.projectName = self.createProjectNameTextbox.text()
if self.projectName == '':
self.parent().MESSAGEHANDLER.warning('Project name cannot be blank.', popUp=True)
return
self.projectPass = self.createProjectPasswordTextbox.text()
self.openProject = False
self.accept()
def openExistingProject(self) -> None:
self.projectName = self.openProjectDropdown.currentText()
if self.projectName == '':
self.parent().MESSAGEHANDLER.warning('Project name cannot be blank.', popUp=True)
return
self.projectPass = self.openProjectPassword.text()
self.openProject = True
self.accept()
class ViewAndStopResolutionsDialog(QtWidgets.QDialog):
def __init__(self, mainWindowObject):
super(ViewAndStopResolutionsDialog, self).__init__()
self.setModal(True)
self.setLayout(QtWidgets.QVBoxLayout())
self.mainWindowObject = mainWindowObject
resolutionsLabel = QtWidgets.QLabel('Running Resolutions:')
resolutionsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.layout().addWidget(resolutionsLabel)
scrollArea = QtWidgets.QScrollArea()
scrollAreaLayout = QtWidgets.QFormLayout()
scrollArea.setLayout(scrollAreaLayout)
for resolution in mainWindowObject.resolutions:
if resolution[1]:
resolutionName = 'Server Resolution: ' + resolution[0].resolution
stopResolutionButton = ViewAndStopResolutionsDialogOption(resolution[0],
resolution[1], mainWindowObject)
scrollAreaLayout.addRow(QtWidgets.QLabel(resolutionName), stopResolutionButton)
elif (not resolution[0].done) and resolution[0].return_results:
resolutionName = 'Local Resolution: ' + resolution[0].resolution
stopResolutionButton = ViewAndStopResolutionsDialogOption(resolution[0],
resolution[1], mainWindowObject)
scrollAreaLayout.addRow(QtWidgets.QLabel(resolutionName), stopResolutionButton)
self.layout().addWidget(scrollArea)
self.setMinimumWidth(scrollArea.width())
self.resize(scrollArea.width(), 400)
class ViewAndStopResolutionsDialogOption(QtWidgets.QPushButton):
def __init__(self, resolutionThread, fromServer, mainWindowObject):
super(ViewAndStopResolutionsDialogOption, self).__init__()
self.mainWindowObject = mainWindowObject
self.resolutionThread = resolutionThread
self.fromServer = fromServer
self.setText('Stop Resolution')
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
if self.fromServer:
self.mainWindowObject.FCOM.abortResolution(self.resolutionThread.resolution, self.resolutionThread.uid)
else:
self.resolutionThread.return_results = False
self.resolutionThread.exit()
self.mainWindowObject.cleanUpLocalFinishedResolutions()
# Remove from parent's layout
self.parent().layout().removeRow(self)
class CollectorsDialog(QtWidgets.QDialog):
def __init__(self, mainWindow, collectorsDict: dict = None, runningCollectors: dict = None):
super(CollectorsDialog, self).__init__()
self.mainWindow = mainWindow
self.baseLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.baseLayout)
self.setModal(True)
self.runningCollectorTreeItems = {}
collectorsLabel = QtWidgets.QLabel("Collectors")
collectorsLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.baseLayout.addWidget(collectorsLabel)
if collectorsDict is None:
notConnectedToServerLabel = QtWidgets.QLabel("Not connected to server, Collectors unavailable.")
self.baseLayout.addWidget(notConnectedToServerLabel)
else:
connectedToServerFormWidget = QtWidgets.QWidget()
self.connectedToServerFormWidgetLayout = QtWidgets.QVBoxLayout()
for category in collectorsDict:
categoryLabel = QtWidgets.QLabel(f"Category: {str(category)}")
self.connectedToServerFormWidgetLayout.addWidget(categoryLabel)
for collector in collectorsDict[category]:
newCollectorWidget = QtWidgets.QWidget()
newCollectorWidgetLayout = QtWidgets.QGridLayout()
newCollectorWidget.setLayout(newCollectorWidgetLayout)
newCollectorLabel = QtWidgets.QLabel(collectorsDict[category][collector]['name'])
newCollectorLabel.setToolTip(collectorsDict[category][collector]['description'])
newCollectorButton = QtWidgets.QPushButton('Create New')
newCollectorButton.clicked.connect(lambda: self.startSelectedCollector(
collectorsDict[category][collector]))
newCollectorWidgetLayout.addWidget(newCollectorLabel, 0, 0)
newCollectorWidgetLayout.addWidget(newCollectorButton, 0, 1)
newCollectorInstanceTree = QtWidgets.QTreeWidget()
newCollectorInstanceTree.setColumnCount(2)
newCollectorInstanceTree.setHeaderLabels(['UID', 'Stop Button'])
newCollectorInstanceTree.setSelectionBehavior(
QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
newCollectorInstanceTree.header().setStretchLastSection(False)
newCollectorInstanceTree.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
newCollectorWidgetLayout.addWidget(newCollectorInstanceTree, 1, 0, 2, 2)
if runningCollectors is not None:
for value in runningCollectors.values():
for runningCollector in value[collector]:
newTreeItem = QtWidgets.QTreeWidgetItem(newCollectorInstanceTree)
newTreeItem.setText(0, runningCollector['uid'])
stopButton = QtWidgets.QPushButton("Stop")
stopButton.clicked.connect(lambda: self.stopSelectedCollector(runningCollector['uid']))
newCollectorInstanceTree.setItemWidget(newTreeItem, 1, stopButton)
self.runningCollectorTreeItems[runningCollector['uid']] = (newTreeItem,
newCollectorInstanceTree)
self.connectedToServerFormWidgetLayout.addWidget(newCollectorWidget)
connectedToServerFormWidget.setLayout(self.connectedToServerFormWidgetLayout)
self.baseLayout.addWidget(connectedToServerFormWidget)
closeButton = QtWidgets.QPushButton('Close')
closeButton.clicked.connect(self.accept)
self.baseLayout.addWidget(closeButton)
def startSelectedCollector(self, collectorToStartDict: dict):
newCollector = CollectorStartDialog(self.mainWindow.LENTDB, collectorToStartDict)
if newCollector.exec_():
collector_name = collectorToStartDict['name']
try:
self.mainWindow.FCOM.startServerCollector(collector_name,
newCollector.chosenItems,
newCollector.chosenParameters)
self.mainWindow.MESSAGEHANDLER.info('Starting server collector: ' + collector_name)
except Exception as e:
self.mainWindow.MESSAGEHANDLER.error(f'Error starting server collector: {str(e)}')
def stopSelectedCollector(self, collectorToStop: str):
self.mainWindow.MESSAGEHANDLER.info(f'Stopping server collector with UID: {collectorToStop}')
self.mainWindow.FCOM.stopServerCollector(collectorToStop)
self.runningCollectorTreeItems[collectorToStop][1].takeTopLevelItem(
self.runningCollectorTreeItems[collectorToStop][1].indexOfTopLevelItem(
self.runningCollectorTreeItems[collectorToStop][0]))
currentClientCollectors = self.mainWindow.getClientCollectors()
try:
currentClientCollectors.pop(collectorToStop)
self.mainWindow.stopRunningCollector(collectorToStop)
self.mainWindow.setClientCollectors(currentClientCollectors)
except KeyError:
self.mainWindow.MESSAGEHANDLER.error('Trying to stop Collector that was never ran. Was confirmation that '
'the collector was started by the server received?', popUp=False)
class CollectorStartDialog(QtWidgets.QDialog):
def __init__(self, entityDB, collectorDict: dict):
super(CollectorStartDialog, self).__init__()
self.entityDB = entityDB
self.setModal(True)
self.setWindowTitle('Collector Wizard')
self.parametersList = []
self.chosenParameters = {}
self.chosenItems = []
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
self.childWidget = QtWidgets.QTabWidget()
dialogLayout.addWidget(self.childWidget, 0, 0, 4, 2)
dialogLayout.setRowStretch(0, 1)
dialogLayout.setColumnStretch(0, 1)
entitySelectTab = QtWidgets.QWidget()
entitySelectTab.setLayout(QtWidgets.QVBoxLayout())
originTypes = collectorDict['originTypes']
labelText = collectorDict['description'] + "\n\n"
labelText += 'Select the entities to use for this collector.\nAccepted Origin Types: ' + \
', '.join(collectorDict['originTypes'])
entitySelectTabLabel = QtWidgets.QLabel(labelText)
entitySelectTabLabel.setWordWrap(True)
entitySelectTabLabel.setMaximumWidth(600)
entitySelectTabLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
entitySelectTab.layout().addWidget(entitySelectTabLabel)
self.entitySelector = QtWidgets.QTreeWidget()
self.entitySelector.setHeaderLabels(['Primary Field', 'Entity Type', 'Icon'])
self.entitySelector.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
self.entitySelector.setSortingEnabled(True)
# Stretch the first column, since it contains the primary field.
self.entitySelector.header().setStretchLastSection(False)
self.entitySelector.header().setSectionResizeMode(0, QtWidgets.QHeaderView.ResizeMode.Stretch)
relevantEntityFields = [(entity['uid'], entity[list(entity)[1]], entity['Entity Type'], entity['Icon'])
for entity in entityDB.getAllEntities()
if entity['Entity Type'] in originTypes or '*' in originTypes]
for eligibleEntity in relevantEntityFields:
newTreeWidgetItem = QtWidgets.QTreeWidgetItem(self.entitySelector)
newTreeWidgetItemPixmap = QtGui.QPixmap()
newTreeWidgetItemPixmap.loadFromData(eligibleEntity[3])
newTreeWidgetItem.setText(0, eligibleEntity[1])
newTreeWidgetItem.setText(1, eligibleEntity[2])
newTreeWidgetItem.setIcon(2, newTreeWidgetItemPixmap)
# Hidden, so we can pull the UID later.
newTreeWidgetItem.setText(3, eligibleEntity[0])
self.entitySelector.setSelectionMode(self.entitySelector.SelectionMode.MultiSelection)
entitySelectTab.layout().addWidget(self.entitySelector)
self.childWidget.addTab(entitySelectTab, 'Entities')
parameters = collectorDict['parameters']
for key in parameters:
propertyWidget = QtWidgets.QWidget()
propertyKeyLayout = QtWidgets.QVBoxLayout()
propertyWidget.setLayout(propertyKeyLayout)
propertyLabel = QtWidgets.QLabel(parameters[key].get('description'))
propertyLabel.setWordWrap(True)
propertyLabel.setMaximumWidth(600)
propertyLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
propertyKeyLayout.addWidget(propertyLabel)
propertyType = parameters[key].get('type')
propertyValue = parameters[key].get('value')
propertyDefaultValue = parameters[key].get('default')
if propertyType == 'String':
propertyInputField = StringPropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'File':
propertyInputField = FilePropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'SingleChoice':
propertyInputField = SingleChoicePropertyInput(propertyValue, propertyDefaultValue)
elif propertyType == 'MultiChoice':
propertyInputField = MultiChoicePropertyInput(propertyValue, propertyDefaultValue)
else:
# If value has invalid type, skip to the next property.
propertyInputField = None
if propertyInputField is not None:
propertyKeyLayout.addWidget(propertyInputField)
propertyKeyLayout.setStretch(1, 1)
self.childWidget.addTab(propertyWidget, key)
self.parametersList.append((key, propertyInputField))
nextButton = QtWidgets.QPushButton('Next')
nextButton.clicked.connect(self.nextTab)
previousButton = QtWidgets.QPushButton('Previous')
previousButton.clicked.connect(self.previousTab)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.clicked.connect(self.accept)
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
dialogLayout.addWidget(previousButton, 4, 0, 1, 1)
dialogLayout.addWidget(nextButton, 4, 1, 1, 1)
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
def nextTab(self):
currentIndex = self.childWidget.currentIndex()
if currentIndex < self.childWidget.count():
self.childWidget.setCurrentIndex(currentIndex + 1)
def previousTab(self):
currentIndex = self.childWidget.currentIndex()
if currentIndex > 0:
self.childWidget.setCurrentIndex(currentIndex - 1)
def accept(self) -> None:
for item in self.entitySelector.selectedItems():
self.chosenItems.append(self.entityDB.getEntity(item.text(3)))
for resolutionParameterName, resolutionParameterInput in self.parametersList:
value = resolutionParameterInput.getValue()
if value == '':
msgBox = QtWidgets.QMessageBox()
msgBox.setModal(True)
QtWidgets.QMessageBox.warning(msgBox,
"Not all parameters were filled in",
"Some of the required parameters for the collector have been left blank."
" Please fill them in before proceeding.")
return
self.chosenParameters[resolutionParameterName] = value
super(CollectorStartDialog, self).accept()
class ImportLinksFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportLinksFromCSVFile, self).__init__(parent=parent)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
self.setWindowTitle("Import Links from CSV")
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Import Links from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the Entity Type of the Parent entity (Entity One) and the Entity Type of "
"the Child entity (Entity Two). Then, map these entities to columns in the CSV "
"file. You may also map the rest of the fields to the other columns of the CSV "
"file. Lastly, choose how to import and / or generate the ID for each Resolution.")
descLabel.setWordWrap(True)
entityOneTypeChoiceWidget = QtWidgets.QWidget()
entityOneTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityOneTypeChoiceWidget.setLayout(entityOneTypeChoiceLayout)
entityOneTypeChoiceLabel = QtWidgets.QLabel("Entity One type:")
self.entityOneTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityOneTypeChoiceDropdown.setEditable(False)
self.entityOneTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
entityOneTypeChoiceLayout.addWidget(entityOneTypeChoiceLabel)
entityOneTypeChoiceLayout.addWidget(self.entityOneTypeChoiceDropdown)
entityTwoTypeChoiceWidget = QtWidgets.QWidget()
entityTwoTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityTwoTypeChoiceWidget.setLayout(entityTwoTypeChoiceLayout)
entityTwoTypeChoiceLabel = QtWidgets.QLabel("Entity Two type:")
self.entityTwoTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityTwoTypeChoiceDropdown.setEditable(False)
self.entityTwoTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
entityTwoTypeChoiceLayout.addWidget(entityTwoTypeChoiceLabel)
entityTwoTypeChoiceLayout.addWidget(self.entityTwoTypeChoiceDropdown)
self.fieldIncludeCheckBoxes = []
tableFieldCheckBoxesWidget = QtWidgets.QWidget()
tableFieldCheckBoxesWidgetLayout = QtWidgets.QHBoxLayout()
tableFieldCheckBoxesWidget.setLayout(tableFieldCheckBoxesWidgetLayout)
for _ in range(columnNumber):
checkBoxWidget = QtWidgets.QCheckBox('Include Field? ')
checkBoxWidget.setToolTip('Check the box to include this column as an attribute for the resolution.')
self.fieldIncludeCheckBoxes.append(checkBoxWidget)
tableFieldCheckBoxesWidgetLayout.addWidget(checkBoxWidget)
self.fieldMappingComboBoxes = []
tableFieldAttributeMapping = QtWidgets.QWidget()
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
for fieldIndex in range(columnNumber):
fieldMappingWidget = QtWidgets.QComboBox()
fieldMappingWidget.setEditable(False)
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
fieldMappingWidget.addItems(['', 'Entity One', 'Entity Two', 'Resolution ID', 'Notes'])
self.fieldMappingComboBoxes.append(fieldMappingWidget)
tableFieldAttributeMappingLayout.addWidget(fieldMappingWidget)
csvTable = QtWidgets.QTableWidget(3, columnNumber, self)
csvTable.setHorizontalHeaderLabels(csvTableContents.columns)
for row in csvTableContents[:3].itertuples():
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
randomizationLabel = QtWidgets.QLabel("If the resolution identifiers (i.e. 'Resolution ID') are not guaranteed "
"to be unique, you can configure whether you'd like to leave them as-is, "
"append a random token, or ignore any resolution identifiers and just "
"have random tokens as the Resolution IDs.\nPlease select what you would "
"like to do:")
randomizationLabel.setWordWrap(True)
randomizationLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
self.randAsIs = QtWidgets.QRadioButton("Use 'Resolution ID' as-is.")
self.randMerge = QtWidgets.QRadioButton("Append a random token to the values mapped to the 'Resolution ID' "
"field.")
self.randReplace = QtWidgets.QRadioButton("Ignore the 'Resolution ID' mapping, instead generate random values "
"for Resolution IDs.")
self.randAsIs.setChecked(True)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.checkIfEntitiesAreMapped)
importLayout.addWidget(titleLabel)
importLayout.addWidget(descLabel)
importLayout.addWidget(entityOneTypeChoiceWidget)
importLayout.addWidget(entityTwoTypeChoiceWidget)
importLayout.addWidget(tableFieldAttributeMapping)
importLayout.addWidget(csvTable)
importLayout.addWidget(tableFieldCheckBoxesWidget)
importLayout.addWidget(randomizationLabel)
importLayout.addWidget(self.randAsIs)
importLayout.addWidget(self.randMerge)
importLayout.addWidget(self.randReplace)
importLayout.addWidget(buttonsWidget)
def changeMappingForField(self, newIndex):
for comboBox, checkBox in zip(self.fieldMappingComboBoxes, self.fieldIncludeCheckBoxes):
if comboBox.currentIndex() == newIndex:
if newIndex == 0:
checkBox.setEnabled(True)
else:
if comboBox.hasFocus():
checkBox.setEnabled(False)
checkBox.setChecked(True)
else:
comboBox.setCurrentIndex(0)
checkBox.setEnabled(True)
checkBox.setChecked(False)
def checkIfEntitiesAreMapped(self):
# Check if Entity One and Entity Two labels are assigned, do not proceed if not.
isOneAssigned = False
isTwoAssigned = False
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == 1:
isOneAssigned = True
elif comboBox.currentIndex() == 2:
isTwoAssigned = True
if isOneAssigned and isTwoAssigned:
self.accept()
else:
self.parent().parent().MESSAGEHANDLER.warning('Need to at least configure mappings for Entity One (parent) '
'and Entity Two (child) before proceeding.', popUp=True)
class ImportLinkEntitiesFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportLinkEntitiesFromCSVFile, self).__init__(parent=parent)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
self.setWindowTitle("Create Entities from Link Fields")
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Create Entities from Link Fields")
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the entity type to import the remaining fields as. "
"Then, configure the mapping between the CSV fields and entity attributes. To do "
"so, select from the drop-down boxes the entity attributes that correspond to "
"each field in the CSV file. The drop-down boxes left blank will have the "
"corresponding CSV column names be treated as attribute names for the new "
"entities.")
descLabel.setWordWrap(True)
entityTypeChoiceWidget = QtWidgets.QWidget()
entityTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityTypeChoiceWidget.setLayout(entityTypeChoiceLayout)
entityTypeChoiceLabel = QtWidgets.QLabel("Entity Type to Import As:")
self.entityTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityTypeChoiceDropdown.setEditable(False)
self.entityTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
self.entityTypeChoiceDropdown.currentIndexChanged.connect(self.pickEntityToImportAs)
entityTypeChoiceLayout.addWidget(entityTypeChoiceLabel)
entityTypeChoiceLayout.addWidget(self.entityTypeChoiceDropdown)
self.randomizeLabel = QtWidgets.QCheckBox('Add random token to Primary Field?')
self.randomizeLabel.setToolTip("Choose whether you want to append a random token to the primary fields of "
"the imported entities.")
self.randomizeLabel.setChecked(True)
self.extraLinkLabel = QtWidgets.QCheckBox('Include links bypassing imported entities?')
self.extraLinkLabel.setToolTip("Check this box if you want links created between the parent and child "
"entities, as well as links between the parent, new node and child entities.")
self.extraLinkLabel.setChecked(False)
self.fieldMappingComboBoxes = []
tableFieldAttributeMapping = QtWidgets.QWidget()
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
for _ in range(columnNumber):
fieldMappingWidget = QtWidgets.QComboBox()
fieldMappingWidget.setEditable(False)
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
fieldMappingWidget.addItem('')
self.fieldMappingComboBoxes.append(fieldMappingWidget)
tableFieldAttributeMappingLayout.addWidget(fieldMappingWidget)
csvTable = QtWidgets.QTableWidget(3, columnNumber, self)
csvTable.setHorizontalHeaderLabels(csvTableContents.columns)
for row in csvTableContents[:3].itertuples():
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.confirmThatPrimaryFieldIsMapped)
importLayout.addWidget(titleLabel)
importLayout.addWidget(descLabel)
importLayout.addWidget(entityTypeChoiceWidget)
importLayout.addWidget(tableFieldAttributeMapping)
importLayout.addWidget(csvTable)
importLayout.addWidget(self.randomizeLabel)
importLayout.addWidget(self.extraLinkLabel)
importLayout.addWidget(buttonsWidget)
self.pickEntityToImportAs()
def pickEntityToImportAs(self):
currentEntityAttributes = [''] + self.parent().parent().RESOURCEHANDLER.getEntityAttributes(
self.entityTypeChoiceDropdown.currentText())
for comboBox in self.fieldMappingComboBoxes:
comboBox.blockSignals(True)
comboBox.clear()
comboBox.blockSignals(False)
comboBox.addItems(currentEntityAttributes)
def changeMappingForField(self, newIndex):
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == newIndex and not comboBox.hasFocus():
comboBox.setCurrentIndex(0)
def confirmThatPrimaryFieldIsMapped(self):
primaryField = self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
self.entityTypeChoiceDropdown.currentText())
primaryFieldMapped = any(comboBox.currentText() == primaryField for comboBox in self.fieldMappingComboBoxes)
if primaryFieldMapped:
self.accept()
else:
self.parent().parent().MESSAGEHANDLER.warning('Primary field (' + primaryField +
') needs to be mapped before proceeding.', popUp=True)
class ImportEntityFromCSVFile(QtWidgets.QDialog):
def __init__(self, parent, csvTableContents: pd.DataFrame):
super(ImportEntityFromCSVFile, self).__init__(parent=parent)
self.setModal(True)
importLayout = QtWidgets.QVBoxLayout()
self.setLayout(importLayout)
self.setWindowTitle("Import Entities from CSV")
columnNumber = len(csvTableContents.columns)
titleLabel = QtWidgets.QLabel("Import Entities from CSV")
titleLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descLabel = QtWidgets.QLabel("Select the entity type to import the entities from the CSV file as. "
"Then, configure the mapping between the CSV fields and entity attributes. To do "
"so, select from the drop-down boxes the entity attributes that correspond to "
"each field in the CSV file. The drop-down boxes left blank will have the "
"corresponding CSV column names be treated as attribute names for the new "
"entities.")
descLabel.setWordWrap(True)
entityTypeChoiceWidget = QtWidgets.QWidget()
entityTypeChoiceLayout = QtWidgets.QHBoxLayout()
entityTypeChoiceWidget.setLayout(entityTypeChoiceLayout)
entityTypeChoiceLabel = QtWidgets.QLabel("Entity Type to Import As:")
self.entityTypeChoiceDropdown = QtWidgets.QComboBox()
self.entityTypeChoiceDropdown.setEditable(False)
self.entityTypeChoiceDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
self.entityTypeChoiceDropdown.currentIndexChanged.connect(self.pickEntityToImportAs)
entityTypeChoiceLayout.addWidget(entityTypeChoiceLabel)
entityTypeChoiceLayout.addWidget(self.entityTypeChoiceDropdown)
self.fieldMappingComboBoxes = []
tableFieldAttributeMapping = QtWidgets.QWidget()
tableFieldAttributeMappingLayout = QtWidgets.QHBoxLayout()
tableFieldAttributeMapping.setLayout(tableFieldAttributeMappingLayout)
for _ in range(columnNumber):
fieldMappingWidget = QtWidgets.QComboBox()
fieldMappingWidget.setEditable(False)
fieldMappingWidget.currentIndexChanged.connect(self.changeMappingForField)
fieldMappingWidget.addItem('')
self.fieldMappingComboBoxes.append(fieldMappingWidget)
tableFieldAttributeMappingLayout.addWidget(fieldMappingWidget)
csvTable = QtWidgets.QTableWidget(3, columnNumber, self)
csvTable.setHorizontalHeaderLabels(csvTableContents.columns)
for row in csvTableContents[:3].itertuples():
rowValues = list(row)
for column in range(columnNumber):
columnItem = QtWidgets.QTableWidgetItem(str(rowValues[column + 1]))
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
csvTable.setItem(rowValues[0], column, columnItem)
importToCanvasChoiceWidget = QtWidgets.QWidget()
importToCanvasChoiceLayout = QtWidgets.QHBoxLayout()
importToCanvasChoiceWidget.setLayout(importToCanvasChoiceLayout)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
importToCanvasChoiceLayout.addWidget(self.importToCanvasCheckbox)
importToCanvasChoiceLayout.addWidget(self.importToCanvasDropdown)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
confirmButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(confirmButton)
cancelButton.clicked.connect(self.reject)
confirmButton.clicked.connect(self.confirmThatPrimaryFieldIsMapped)
importLayout.addWidget(titleLabel)
importLayout.addWidget(descLabel)
importLayout.addWidget(entityTypeChoiceWidget)
importLayout.addWidget(tableFieldAttributeMapping)
importLayout.addWidget(csvTable)
importLayout.addWidget(importToCanvasChoiceWidget)
importLayout.addWidget(buttonsWidget)
self.pickEntityToImportAs()
def pickEntityToImportAs(self):
currentEntityAttributes = [''] + self.parent().parent().RESOURCEHANDLER.getEntityAttributes(
self.entityTypeChoiceDropdown.currentText())
for comboBox in self.fieldMappingComboBoxes:
comboBox.blockSignals(True)
comboBox.clear()
comboBox.blockSignals(False)
comboBox.addItems(currentEntityAttributes)
def changeMappingForField(self, newIndex):
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentIndex() == newIndex and not comboBox.hasFocus():
comboBox.setCurrentIndex(0)
def confirmThatPrimaryFieldIsMapped(self):
primaryField = self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
self.entityTypeChoiceDropdown.currentText())
primaryFieldMapped = False
for comboBox in self.fieldMappingComboBoxes:
if comboBox.currentText() == primaryField:
# Executing self.accept() will not actually end the prompt, so we use bools to check and confirm.
primaryFieldMapped = True
break
if primaryFieldMapped:
self.accept()
else:
self.parent().parent().MESSAGEHANDLER.warning('Primary field (' + primaryField +
') needs to be mapped before proceeding.', popUp=True)
class ImportFromFileDialog(QtWidgets.QDialog):
def popupFileDialog(self):
self.fileDirectory = QtWidgets.QFileDialog().getOpenFileName(
parent=self, caption='Select File to Import From',
dir=str(Path.home()),
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
filter="Import File (*.csv *.txt *.xls *.xlsx *.ods)")[0]
if self.fileDirectory != '':
self.fileDirectoryLine.setText(self.fileDirectory)
def __init__(self, parent):
super(ImportFromFileDialog, self).__init__(parent=parent)
self.fileDirectory = ''
self.setWindowTitle('Import Entities From File')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Select the file to import Entities or Links from:')
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
self.fileDirectoryButton = QtWidgets.QPushButton("Select file...")
self.fileDirectoryLine = QtWidgets.QLineEdit()
self.fileDirectoryLine.setReadOnly(True)
fileChoiceLabel = QtWidgets.QLabel('Specify the type of the chosen file and what to import:')
fileChoiceLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
fileChoiceLabel.setWordWrap(True)
self.textFileChoice = QtWidgets.QRadioButton('Text file - Entities Import')
self.textFileChoice.setChecked(True)
self.CSVFileChoice = QtWidgets.QRadioButton('Spreadsheet / CSV - Entities Import')
self.CSVFileChoiceLinks = QtWidgets.QRadioButton('Spreadsheet / CSV - Links Import')
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
dialogLayout.addWidget(fileChoiceLabel, 3, 0, 1, 2)
dialogLayout.addWidget(self.textFileChoice, 4, 0, 1, 2)
dialogLayout.addWidget(self.CSVFileChoice, 5, 0, 1, 2)
dialogLayout.addWidget(self.CSVFileChoiceLinks, 6, 0, 1, 2)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
cancelButton = QtWidgets.QPushButton('Cancel')
self.fileDirectoryButton.clicked.connect(self.popupFileDialog)
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
self.setMaximumWidth(500)
self.setMinimumWidth(300)
self.setMaximumHeight(300)
self.setMinimumHeight(300)
dialogLayout.addWidget(cancelButton, 7, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 7, 1, 1, 1)
class ImportFromTextFileDialog(QtWidgets.QDialog):
def __init__(self, parent, fileContents):
super(ImportFromTextFileDialog, self).__init__(parent=parent)
self.setWindowTitle('Import From Text File')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Importing entities from text file, one entity per line.')
descriptionLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
importTypeLabel = QtWidgets.QLabel('Choose the Entity Type to import as:')
importTypeLabel.setWordWrap(True)
dialogLayout.addWidget(importTypeLabel, 1, 0, 1, 1)
self.importTypeDropdown = QtWidgets.QComboBox()
self.importTypeDropdown.addItems(parent.parent().RESOURCEHANDLER.getAllEntities())
self.importTypeDropdown.setEditable(False)
dialogLayout.addWidget(self.importTypeDropdown, 1, 1, 1, 1)
self.importTypeDropdown.currentTextChanged.connect(self.updatePrimaryFieldValueLabel)
typePrimaryFieldLabel = QtWidgets.QLabel('Primary Field for chosen type:')
typePrimaryFieldLabel.setWordWrap(False)
dialogLayout.addWidget(typePrimaryFieldLabel, 2, 0, 1, 1)
self.typePrimaryFieldValueLabel = QtWidgets.QLineEdit('')
self.typePrimaryFieldValueLabel.setReadOnly(True)
dialogLayout.addWidget(self.typePrimaryFieldValueLabel, 2, 1, 1, 1)
textTable = QtWidgets.QTableWidget(3, 1, self)
textTable.setWordWrap(True)
textTable.setFixedWidth(450)
for lineIndex, lineValue in enumerate(fileContents):
columnItem = QtWidgets.QTableWidgetItem(lineValue)
columnItem.setFlags(columnItem.flags() & ~QtCore.Qt.ItemFlag.ItemIsEditable)
textTable.setItem(lineIndex, 0, columnItem)
textTable.setColumnWidth(0, 450)
textTable.setHorizontalHeaderLabels(['File Entities Preview'])
dialogLayout.addWidget(textTable, 3, 0, 1, 2)
self.importToCanvasCheckbox = QtWidgets.QCheckBox('Import To Canvas:')
self.importToCanvasDropdown = QtWidgets.QComboBox()
self.importToCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.importToCanvasDropdown.setEditable(False)
self.importToCanvasDropdown.setDisabled(True)
self.importToCanvasCheckbox.toggled.connect(lambda: self.importToCanvasDropdown.setDisabled(
self.importToCanvasDropdown.isEnabled()))
dialogLayout.addWidget(self.importToCanvasCheckbox, 4, 0, 1, 1)
dialogLayout.addWidget(self.importToCanvasDropdown, 4, 1, 1, 1)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
cancelButton = QtWidgets.QPushButton('Cancel')
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
acceptButton.setFocus()
self.setFixedWidth(472)
self.setFixedHeight(300)
dialogLayout.addWidget(cancelButton, 5, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 5, 1, 1, 1)
# Initialize the primary field value label to whatever is the primary field of the first entity
# in the drop down selection box.
self.updatePrimaryFieldValueLabel()
def updatePrimaryFieldValueLabel(self):
self.typePrimaryFieldValueLabel.setText(str(self.parent().parent().RESOURCEHANDLER.getPrimaryFieldForEntityType(
self.importTypeDropdown.currentText())))
class CanvasPictureDialog(QtWidgets.QDialog):
def __init__(self, parent):
super(CanvasPictureDialog, self).__init__(parent=parent)
self.fileDirectory = ""
self.setWindowTitle('Save Canvas Picture')
self.setModal(True)
dialogLayout = QtWidgets.QGridLayout()
self.setLayout(dialogLayout)
descriptionLabel = QtWidgets.QLabel('Choose the directory to save the Picture to:')
descriptionLabel.setWordWrap(True)
dialogLayout.addWidget(descriptionLabel, 0, 0, 1, 2)
self.fileDirectoryButton = QtWidgets.QPushButton("Select directory...")
self.fileDirectoryLine = QtWidgets.QLineEdit()
self.fileDirectoryLine.setReadOnly(True)
self.backgroundButtonsGroup = QtWidgets.QButtonGroup()
self.canvasViewportButtonsGroup = QtWidgets.QButtonGroup()
self.transparentChoice = QtWidgets.QRadioButton('Transparent')
self.backgroundButtonsGroup.addButton(self.transparentChoice)
self.transparentChoice.setChecked(True)
self.withBackgroundChoice = QtWidgets.QRadioButton('With Background')
self.backgroundButtonsGroup.addButton(self.withBackgroundChoice)
self.justViewportChoice = QtWidgets.QRadioButton('Just Viewport')
self.justViewportChoice.setToolTip('Save a picture of only the contents of the scene that are currently '
'visible (through the viewport).')
self.justViewportChoice.setChecked(True)
self.canvasViewportButtonsGroup.addButton(self.justViewportChoice)
self.entireCanvasChoice = QtWidgets.QRadioButton('Entire Canvas')
self.entireCanvasChoice.setToolTip('Save a picture of the entire contents of the canvas, not just the current '
'viewport contents.')
self.canvasViewportButtonsGroup.addButton(self.entireCanvasChoice)
dialogLayout.addWidget(self.fileDirectoryLine, 1, 0, 1, 2)
dialogLayout.addWidget(self.fileDirectoryButton, 2, 0, 1, 2)
dialogLayout.addWidget(self.transparentChoice, 3, 0, 1, 1)
dialogLayout.addWidget(self.withBackgroundChoice, 3, 1, 1, 1)
dialogLayout.addWidget(self.justViewportChoice, 4, 0, 1, 1)
dialogLayout.addWidget(self.entireCanvasChoice, 4, 1, 1, 1)
canvasLabel = QtWidgets.QLabel('Canvas:')
canvasLabel.setWordWrap(False)
dialogLayout.addWidget(canvasLabel, 5, 0, 1, 1)
self.chosenCanvasDropdown = QtWidgets.QComboBox()
self.chosenCanvasDropdown.addItems(list(parent.parent().centralWidget().tabbedPane.canvasTabs))
self.chosenCanvasDropdown.setEditable(False)
dialogLayout.addWidget(self.chosenCanvasDropdown, 5, 1, 1, 1)
acceptButton = QtWidgets.QPushButton('Accept')
acceptButton.setAutoDefault(True)
acceptButton.setDefault(True)
acceptButton.setFocus()
cancelButton = QtWidgets.QPushButton('Cancel')
self.fileDirectoryButton.clicked.connect(self.popupFileDialog)
acceptButton.clicked.connect(self.accept)
cancelButton.clicked.connect(self.reject)
self.setMaximumWidth(500)
self.setMinimumWidth(300)
self.setMaximumHeight(300)
self.setMinimumHeight(300)
dialogLayout.addWidget(cancelButton, 6, 0, 1, 1)
dialogLayout.addWidget(acceptButton, 6, 1, 1, 1)
def popupFileDialog(self):
saveAsDialog = QtWidgets.QFileDialog()
saveAsDialog.setOption(QtWidgets.QFileDialog.Option.DontUseNativeDialog, True)
saveAsDialog.setViewMode(QtWidgets.QFileDialog.ViewMode.List)
saveAsDialog.setNameFilter("Image (*.png)")
saveAsDialog.setAcceptMode(QtWidgets.QFileDialog.AcceptMode.AcceptSave)
saveAsDialog.setDirectory(str(Path.home()))
saveAsDialog.exec()
self.fileDirectory = saveAsDialog.selectedFiles()[0]
if self.fileDirectory != '':
if Path(self.fileDirectory).suffix != 'png':
self.fileDirectory = str(Path(self.fileDirectory).with_suffix('.png'))
self.fileDirectoryLine.setText(self.fileDirectory)
class SearchEngineDialog(QtWidgets.QDialog):
searchEngines = ['https://www.startpage.com/sp/search?q=', 'https://html.duckduckgo.com/html?q=',
'https://www.google.com/search?q=', 'https://www.bing.com/search?q=',
'https://www.yandex.com/search/?text=', 'https://gigablast.com/search?q=',
'https://www.etools.ch/searchSubmit.do?query=', 'https://lite.qwant.com/?q=',
'https://www.izito.com/search?q=', 'https://searx.org/search?q=']
def __init__(self, parent):
super(SearchEngineDialog, self).__init__(parent=parent)
self.setWindowTitle('Search Engine Lookup')
self.setModal(True)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
dialogLabel = QtWidgets.QLabel('Select the search engines to use:')
layout.addWidget(dialogLabel)
self.searchEngineWidgets = []
for engine in self.searchEngines:
engineCheckbox = QtWidgets.QCheckBox('/'.join(engine.split('/')[:3]))
engineCheckbox.searchAttr = engine
layout.addWidget(engineCheckbox)
self.searchEngineWidgets.append(engineCheckbox)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
acceptButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
acceptButton.clicked.connect(self.accept)
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(acceptButton)
layout.addWidget(buttonsWidget)
class SearchImageEngineDialog(QtWidgets.QDialog):
searchEngines = ['https://pimeyes.com/en', 'https://yandex.com/images/',
'https://www.bing.com/visualsearch ', 'https://www.google.com/imghp',
'https://tineye.com/']
def __init__(self, parent):
super(SearchImageEngineDialog, self).__init__(parent=parent)
self.setWindowTitle('Image Search Engine Lookup')
self.setModal(True)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
dialogLabel = QtWidgets.QLabel('Select the image search engines to open in tabs:')
layout.addWidget(dialogLabel)
self.searchEngineWidgets = []
for engine in self.searchEngines:
engineCheckbox = QtWidgets.QCheckBox('/'.join(engine.split('/')[:3]))
engineCheckbox.searchAttr = engine
layout.addWidget(engineCheckbox)
self.searchEngineWidgets.append(engineCheckbox)
buttonsWidget = QtWidgets.QWidget()
buttonsWidgetLayout = QtWidgets.QHBoxLayout()
buttonsWidget.setLayout(buttonsWidgetLayout)
acceptButton = QtWidgets.QPushButton('Confirm')
cancelButton = QtWidgets.QPushButton('Cancel')
cancelButton.clicked.connect(self.reject)
acceptButton.clicked.connect(self.accept)
buttonsWidgetLayout.addWidget(cancelButton)
buttonsWidgetLayout.addWidget(acceptButton)
layout.addWidget(buttonsWidget)
class ScreenshotWebsiteThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
cancelled = False
def __init__(self, menuBar, mainWindow, websiteEntities):
super(ScreenshotWebsiteThread, self).__init__()
self.mainWindow = mainWindow
self.menuBar = menuBar
self.websiteEntities = websiteEntities
def cancelOperation(self):
self.cancelled = True
def run(self) -> None:
baseFilesPath = Path(self.mainWindow.SETTINGS.value('Project/FilesDir'))
progressValue = 1
self.progressSignal.emit(progressValue)
newNodes = []
with sync_playwright() as p:
browser = p.firefox.launch()
if platform.system() == 'Linux':
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Windows'][0]
)
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
tabsFilePath = tabsFilePath[0]
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
browserCookies = self.menuBar.firefoxCookiesHelper(cookiesDatabasePath)
context.add_cookies(browserCookies)
page = context.new_page()
for websiteEntity in self.websiteEntities:
if self.cancelled:
break
website = websiteEntity[1]
screenshotName = tldextract.extract(website).fqdn + ' Screenshot ' + str(time.time_ns()) + '.png'
currFileName = baseFilesPath / screenshotName
for _ in range(3):
try:
page.goto(website)
page.wait_for_load_state("networkidle")
page.screenshot(path=str(currFileName), full_page=True)
break
except TimeoutError:
pass
progressValue += 1
self.progressSignal.emit(progressValue)
newNodes.append([{'Image Name': screenshotName,
'File Path': screenshotName,
'Entity Type': 'Image'},
{websiteEntity[0]: {'Resolution': 'Website Screenshot', 'Notes': ''}}])
context.close()
browser.close()
self.progressSignal.emit(len(self.websiteEntities) + 1)
self.mainWindow.facilitateResolutionSignalListener.emit('Screenshot Websites Operation', newNodes)
class SaveWebsiteThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
cancelled = False
def __init__(self, menuBar, mainWindow, websiteEntities):
super(SaveWebsiteThread, self).__init__()
self.mainWindow = mainWindow
self.menuBar = menuBar
self.websiteEntities = websiteEntities
def cancelOperation(self):
self.cancelled = True
def run(self) -> None:
newNodes = []
baseFilesPath = Path(self.mainWindow.SETTINGS.value('Project/FilesDir'))
currTempDir = Path.home() # Failsafe in case we can't make directories in TEMP / tmp.
progressValue = 1
self.progressSignal.emit(progressValue)
def handle_response(response):
responseURL = response.url
responseURLFragments = responseURL.split('/')[3:]
savePath = currTempDir
with contextlib.suppress(Exception):
if response.ok:
for fragment in responseURLFragments[:-1]:
savePath /= fragment
savePath.mkdir(exist_ok=True)
try:
filename = responseURLFragments[-1]
except IndexError:
filename = ''
if not filename:
filename = 'index.html'
with open(savePath / filename, "wb") as fileToWrite:
fileToWrite.write(response.body())
with sync_playwright() as p:
browser = p.firefox.launch()
if platform.system() == 'Linux':
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Windows'][0]
)
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
tabsFilePath = tabsFilePath[0]
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
browserCookies = self.menuBar.firefoxCookiesHelper(cookiesDatabasePath)
context.add_cookies(browserCookies)
page = context.new_page()
page.on("response", handle_response)
for websiteEntity in self.websiteEntities:
if self.cancelled:
break
website = websiteEntity[1]
with tempfile.TemporaryDirectory() as tempDir:
currTempDir = Path(tempDir)
archiveDir = baseFilesPath / (tldextract.extract(website).fqdn + ' Snapshot ' + str(time.time_ns()))
for _ in range(3):
with contextlib.suppress(TimeoutError):
page.goto(website)
page.keyboard.press("End")
page.wait_for_load_state("networkidle")
break
progressValue += 1
self.progressSignal.emit(progressValue)
newNodeName = Path(shutil.make_archive(str(archiveDir), 'zip', currTempDir)).relative_to(
baseFilesPath)
newNodes.append([{'Archive Name': str(newNodeName),
'File Path': str(newNodeName),
'Entity Type': 'Archive'},
{websiteEntity[0]: {'Resolution': 'Website Snapshot', 'Notes': ''}}])
context.close()
browser.close()
self.progressSignal.emit(len(self.websiteEntities) + 1)
self.mainWindow.facilitateResolutionSignalListener.emit('Download Websites Operation', newNodes)
class ImportBrowserTabsThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
cancelled = False
def __init__(self, importDialog: BrowserImportDialog, mainWindowObject, menuObject):
super(ImportBrowserTabsThread, self).__init__(parent=mainWindowObject)
self.importDialog = importDialog
self.mainWindow = mainWindowObject
self.menuObject = menuObject
def cancelOperation(self):
self.cancelled = True
def run(self) -> None:
progressValue = 1
self.progressSignal.emit(progressValue)
returnResults = []
with sync_playwright() as p:
if self.importDialog.firefoxChoice.isChecked():
recordSession = self.importDialog.firefoxSessionChoice.isChecked()
try:
browser = p.firefox.launch()
if platform.system() == 'Linux':
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Linux'][0]
)
urlPath = Path.home() / '.mozilla' / 'firefox'
if not (urlPath / 'profiles.ini').exists():
urlPath = Path.home() / 'snap' / 'firefox' / 'common' / '.mozilla' / 'firefox'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Firefox']['Windows'][0]
)
urlPath = Path(os.environ['APPDATA']) / 'Mozilla' / 'Firefox' / 'Profiles'
if tabsFilePath := list(urlPath.glob('*default*/sessionstore-backups/recovery.jsonlz4')):
tabsFilePath = tabsFilePath[0]
if recordSession:
tabsToOpen = []
else:
tabsToOpen = set()
sitesScreenshotted = set()
tabsBytes = tabsFilePath.read_bytes()
if tabsBytes[:8] == b'mozLz40\0':
tabsBytes = lz4.block.decompress(tabsBytes[8:])
tabsJson = json.loads(tabsBytes)
for browserWindow in tabsJson['windows']:
for browserTab in browserWindow['tabs']:
if recordSession:
first = True
for browserEntry in browserTab['entries']:
url = browserEntry['url']
title = browserEntry.get('title', '')
if not url.startswith('about:'):
if first:
tabsToOpen.append((url, title, True))
first = False
else:
tabsToOpen.append((url, title, False))
else:
browserEntry = browserTab['entries'][browserTab['index'] - 1]
url = browserEntry['url']
if not url.startswith('about:'):
tabsToOpen.add((url, browserEntry['title']))
cookiesDatabasePath = tabsFilePath.parent.parent / 'cookies.sqlite'
browserCookies = self.menuObject.firefoxCookiesHelper(cookiesDatabasePath)
context.add_cookies(browserCookies)
page = context.new_page()
projectFilesDir = Path(self.mainWindow.SETTINGS.value("Project/FilesDir"))
historyMark = -1
for tabToOpen in tabsToOpen:
if self.cancelled:
break
urlTitle = tabToOpen[1]
actualURL = tabToOpen[0]
decodedPath = parse.unquote(actualURL)
parsedURL = parse.urlparse(decodedPath)
urlPath = parsedURL.path
if parsedURL.scheme == 'file':
try:
mime = magic.Magic(mime=True)
pathType = mime.from_file(urlPath)
except FileNotFoundError:
continue
if 'application' in pathType:
newEntity = [{'Document Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Document'}]
elif 'image' in pathType:
newEntity = [{'Image Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Image'}]
elif 'video' in pathType:
newEntity = [{'Video Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Video'}]
elif 'archive' in pathType:
newEntity = [{'Archive Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Archive'}]
elif parsedURL.scheme.startswith('http'):
newEntity = [{'URL': actualURL,
'Entity Type': 'Website'}]
if self.importDialog.importScreenshotsCheckbox.isChecked():
# If we time out, take a screenshot of the page as-is.
try:
page.goto(actualURL)
except TimeoutError:
pass
# Don't screenshot sites multiple times.
# Save in new variable to guard against redirects.
pageURL = page.url
if pageURL not in sitesScreenshotted:
sitesScreenshotted.add(pageURL)
urlSaveDir = projectFilesDir / urlTitle
try:
if not urlSaveDir.exists():
urlSaveDir.mkdir(mode=0o700)
except OSError:
urlSaveDir = None
if urlSaveDir is not None:
timeNow = str(datetime.now().timestamp() * 1000000).split('.')[0]
screenshotSavePath = str(urlSaveDir / (
actualURL.replace('/', '+') + ' ' + timeNow + ' screenshot.png'))
screenshotSavePath = screenshotSavePath.replace('\\', '+')
screenshotEntity = {'Image Name': f"{decodedPath} Screenshot {timeNow}",
'File Path': screenshotSavePath,
'Entity Type': 'Image'}
try:
page.screenshot(path=screenshotSavePath, full_page=True)
except Error:
try:
page.screenshot(path=screenshotSavePath, full_page=False)
except Error:
screenshotEntity = {'Phrase': f'Could not take screenshot of '
f'{decodedPath}',
'Entity Type': 'Phrase'}
returnResults.append(
[screenshotEntity,
{len(returnResults) + 1: {'Resolution': 'Screenshot of Tab',
'Notes': ''}}])
else:
newEntity = [{'Phrase': actualURL,
'Entity Type': 'Phrase'}]
if len(tabToOpen) == 3:
if historyMark != -1 and not tabToOpen[2]:
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
historyMark = len(returnResults)
returnResults.append(newEntity)
else:
self.mainWindow.warningSignalListener.emit('No Firefox session detected. Skipping importing '
'from Firefox.', True)
browser.close()
except Error as e:
self.mainWindow.warningSignalListener.emit(f'Cannot import tabs from Firefox: {str(repr(e))}', True)
if self.importDialog.chromeChoice.isChecked() and not self.cancelled:
progressValue = 2
self.progressSignal.emit(progressValue)
try:
browser = p.chromium.launch()
# NOTE: Cookies are not obtained for chromium based browsers.
if platform.system() == 'Linux':
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Chrome']['Linux'][0]
)
sessionFilePath = Path.home() / '.config' / 'google-chrome' / 'Default' / 'Sessions'
if not sessionFilePath.exists():
sessionFilePath = Path.home() / 'snap' / 'chromium' / 'common' / 'chromium' / \
'Default' / 'Sessions'
else: # We already checked before that the platform is either 'Linux' or 'Windows'.
context = browser.new_context(
viewport={'width': 1920, 'height': 1080},
user_agent=user_agents['Chrome']['Windows'][0]
)
sessionFilePath = Path.home() / 'AppData' / 'Local' / 'Google' / 'Chrome' / 'User Data' / \
'Default'
lastSessionOpenTabs = set()
# Get the session file with the largest timestamp (i.e. most recent session)
# Need the while loop to prevent race conditions
chromeSessionFileContents = None
if sessionFilePath.exists():
while True:
try:
latestTimestamp = max([int(sessionFile.split("Session_", 1)[1])
for sessionFile in os.listdir(sessionFilePath)
if 'Session_' in sessionFile])
sessionFilePath = sessionFilePath.joinpath("Session_" + str(latestTimestamp))
chromeSessionFileContents = sessionFilePath.read_bytes()
break
except (FileNotFoundError, IndexError):
pass
except ValueError:
# No session files means we can't do anything for chrome.
break
if chromeSessionFileContents is None:
self.mainWindow.warningSignalListener.emit('Chrome / Chromium session file does not exist or '
'is inaccessible. Cannot import tabs from '
'Chrome / Chromium.', True)
else:
projectFilesDir = Path(self.mainWindow.SETTINGS.value("Project/FilesDir"))
"""
WARNING: This is a hackish solution that does not always work. Documentation and tools to
work with chrome and chromium session files are lacking last I checked,
and most of the existing ones have been broken since the last update.
This is a best-effort solution for now, which should mostly work.
As for how it works, it's just pattern matching after some observations about the
file structure of chrome session files.
This will probably be revised in the future to make it actually parse chrome
session files.
"""
httpTabs = re.split(b'http', chromeSessionFileContents)
for httpTab in httpTabs[1:]:
try:
tabURL = re.split(b'\x00|\x0b', httpTab)[0].decode()
except UnicodeDecodeError:
continue
if tabURL.startswith('s://'):
tabURL = 'https://' + tabURL[4:]
else:
tabURL = 'http://' + tabURL[3:]
if tabURL[-1] == '/':
tabURL = tabURL[:-1]
lastSessionOpenTabs.add(tabURL)
fileTabs = re.split(b'file', chromeSessionFileContents)
for fileTab in fileTabs[1:]:
try:
tabURL = 'file://' + re.split(b'\x00|\x0b', fileTab)[0].decode()[3:]
except UnicodeDecodeError:
continue
lastSessionOpenTabs.add(tabURL)
page = context.new_page()
for tabURL in lastSessionOpenTabs:
if self.cancelled:
break
decodedPath = parse.unquote(tabURL)
parsedURL = parse.urlparse(decodedPath)
urlPath = parsedURL.path
urlTitle = parsedURL.netloc
if parsedURL.scheme == 'file':
urlTitle = Path(urlPath).name
try:
mime = magic.Magic(mime=True)
pathType = mime.from_file(urlPath)
except FileNotFoundError:
continue
if 'application' in pathType:
returnResults.append([{'Document Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Document'}])
elif 'image' in pathType:
returnResults.append([{'Image Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Image'}])
elif 'video' in pathType:
returnResults.append([{'Video Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Video'}])
elif 'archive' in pathType:
returnResults.append([{'Archive Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Archive'}])
elif parsedURL.scheme.startswith('http'):
returnResults.append([{'URL': tabURL,
'Entity Type': 'Website'}])
if self.importDialog.importScreenshotsCheckbox.isChecked():
# If we time out, take a screenshot of the page as-is.
try:
page.goto(tabURL)
except TimeoutError:
pass
urlSaveDir = projectFilesDir / urlTitle
try:
if not urlSaveDir.exists():
urlSaveDir.mkdir(mode=0o700)
except OSError:
urlSaveDir = None
if urlSaveDir is not None:
timeNow = str(datetime.now().timestamp() * 1000000).split('.')[0]
screenshotSavePath = str(urlSaveDir / (
tabURL.replace('/', '+') + ' ' + timeNow + ' screenshot.png'))
screenshotSavePath = screenshotSavePath.replace('\\', '+')
page.screenshot(path=screenshotSavePath, full_page=True)
returnResults.append(
[{'Image Name': f"{decodedPath} Screenshot {timeNow}",
'File Path': screenshotSavePath,
'Entity Type': 'Image'},
{len(returnResults) - 1: {'Resolution': 'Screenshot of Tab',
'Notes': ''}}])
browser.close()
except Error:
self.mainWindow.warningSignalListener.emit('Chrome / Chromium executable is not installed. Cannot '
'import tabs from Chrome / Chromium.', True)
if self.cancelled:
self.mainWindow.statusBarSignalListener.emit('Cancelled importing entities from Browser.')
else:
progressValue = 3
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults,
self.importDialog.importToCanvasDropdown.currentText() if
self.importDialog.importToCanvasCheckbox.isChecked() else '')
class ImportTorBrowserTabsThread(QtCore.QThread):
progressSignal = QtCore.Signal(int)
cancelled = False
def __init__(self, importDialog: TORBrowserImportDialog, mainWindowObject, menuObject):
super(ImportTorBrowserTabsThread, self).__init__(parent=mainWindowObject)
self.importDialog = importDialog
self.mainWindow = mainWindowObject
self.menuObject = menuObject
def cancelOperation(self):
self.cancelled = True
def run(self) -> None:
progressValue = 1
self.progressSignal.emit(progressValue)
returnResults = []
torBrowserProfilePath = Path(self.mainWindow.SETTINGS.value("Program/TOR Profile Location", "/"))
tabsFilePath = torBrowserProfilePath / 'sessionstore-backups' / 'recovery.jsonlz4'
if not tabsFilePath.exists():
progressValue = 4
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults,
'')
return
if self.importDialog.entireSessionChoice.isChecked():
tabsToOpen = []
else:
tabsToOpen = set()
tabsBytes = tabsFilePath.read_bytes()
if tabsBytes[:8] == b'mozLz40\0':
tabsBytes = lz4.block.decompress(tabsBytes[8:])
tabsJson = json.loads(tabsBytes)
for browserWindow in tabsJson['windows']:
for browserTab in browserWindow['tabs']:
if self.importDialog.entireSessionChoice.isChecked():
first = True
for browserEntry in browserTab['entries']:
url = browserEntry['url']
title = browserEntry.get('title', '')
if not url.startswith('about:'):
if first:
tabsToOpen.append((url, title, True))
first = False
else:
tabsToOpen.append((url, title, False))
else:
browserEntry = browserTab['entries'][browserTab['index'] - 1]
url = browserEntry['url']
if not url.startswith('about:'):
tabsToOpen.add((url, browserEntry['title']))
cookiesDatabasePath = torBrowserProfilePath / 'cookies.sqlite'
browserCookies = self.menuObject.firefoxCookiesHelper(cookiesDatabasePath)
historyMark = -1
for tabToOpen in tabsToOpen:
if self.cancelled:
break
urlTitle = tabToOpen[1]
actualURL = tabToOpen[0]
decodedPath = parse.unquote(actualURL)
parsedURL = parse.urlparse(decodedPath)
urlPath = parsedURL.path
if parsedURL.scheme == 'file':
try:
mime = magic.Magic(mime=True)
pathType = mime.from_file(urlPath)
except FileNotFoundError:
continue
if 'application' in pathType:
newEntity = [{'Document Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Document'}]
elif 'image' in pathType:
newEntity = [{'Image Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Image'}]
elif 'video' in pathType:
newEntity = [{'Video Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Video'}]
elif 'archive' in pathType:
newEntity = [{'Archive Name': urlTitle,
'File Path': urlPath,
'Entity Type': 'Archive'}]
elif parsedURL.scheme.startswith('http'):
if '.onion' in actualURL:
newEntity = [{'Onion URL': actualURL,
'Entity Type': 'Onion Website'}]
else:
newEntity = [{'URL': actualURL,
'Entity Type': 'Website'}]
else:
newEntity = [{'Phrase': actualURL,
'Entity Type': 'Phrase'}]
if len(tabToOpen) == 3:
if historyMark != -1 and not tabToOpen[2]:
newEntity.append({historyMark: {'Resolution': 'Next Page'}})
historyMark = len(returnResults)
returnResults.append(newEntity)
progressValue = 3
self.progressSignal.emit(progressValue)
if self.cancelled:
self.mainWindow.statusBarSignalListener.emit('Cancelled importing entities from Browser.')
else:
newTabEntities = len(returnResults)
for cookie in browserCookies:
cookieParents = []
cookieURI = cookie['domain'] + cookie['path']
if cookieURI.startswith('.'):
cookieURI = cookieURI[1:]
for index, tabEntity in enumerate(returnResults[:newTabEntities]):
for key, value in tabEntity[0].items():
if key != 'Entity Type' and cookieURI in value:
cookieParents.append(index)
cookieEntity = cookie
cookieEntity['Phrase'] = f'Cookie {uuid4()}'
cookieEntity['Entity Type'] = 'Phrase'
cookieParentDict: dict = {}
for cookieParent in cookieParents:
cookieParentDict[cookieParent] = {'Resolution': 'Site Cookie', 'Notes': ''}
cookieResult = [cookieEntity, cookieParentDict]
returnResults.append(cookieResult)
progressValue = 4
self.progressSignal.emit(progressValue)
self.menuObject.browserTabsImportDoneSignalListener.emit(
returnResults,
self.importDialog.importToCanvasDropdown.currentText() if
self.importDialog.importToCanvasCheckbox.isChecked() else '')