mirror of
https://github.com/overcuriousity/autopsy-flatpak.git
synced 2025-07-12 07:56:16 +00:00
Merge branch 'master' of https://github.com/sleuthkit/autopsy
This commit is contained in:
commit
008d11c494
@ -40,7 +40,6 @@ class KeywordSearchConfigurationAction extends CallableSystemAction{
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
panel.save();
|
|
||||||
dialog.close();
|
dialog.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -120,16 +120,5 @@ public class KeywordSearchConfigurationPanel extends javax.swing.JPanel {
|
|||||||
private javax.swing.JPanel jPanel2;
|
private javax.swing.JPanel jPanel2;
|
||||||
private javax.swing.JSplitPane mainSplitPane;
|
private javax.swing.JSplitPane mainSplitPane;
|
||||||
// End of variables declaration//GEN-END:variables
|
// End of variables declaration//GEN-END:variables
|
||||||
|
|
||||||
void save() {
|
|
||||||
KeywordSearchListsXML loader = KeywordSearchListsXML.getCurrent();
|
|
||||||
KeywordSearchIngestService service = KeywordSearchIngestService.getDefault();
|
|
||||||
if (IngestManager.getDefault().isServiceRunning(service)) {
|
|
||||||
for (KeywordSearchList list : loader.getListsL()) {
|
|
||||||
if (list.getUseForIngest()) {
|
|
||||||
service.addToKeywordLists(list.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -442,12 +442,6 @@ public final class KeywordSearchIngestService implements IngestServiceAbstractFi
|
|||||||
List<String> getKeywordLists() {
|
List<String> getKeywordLists() {
|
||||||
return keywordLists == null ? new ArrayList<String>() : keywordLists;
|
return keywordLists == null ? new ArrayList<String>() : keywordLists;
|
||||||
}
|
}
|
||||||
|
|
||||||
void addToKeywordLists(String name) {
|
|
||||||
if (!keywordLists.contains(name)) {
|
|
||||||
keywordLists.add(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if time to commit, if so, run commit.
|
* Check if time to commit, if so, run commit.
|
||||||
@ -904,4 +898,8 @@ public final class KeywordSearchIngestService implements IngestServiceAbstractFi
|
|||||||
void setSkipKnown(boolean skip) {
|
void setSkipKnown(boolean skip) {
|
||||||
this.skipKnown = skip;
|
this.skipKnown = skip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean getSkipKnown() {
|
||||||
|
return skipKnown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,7 +270,7 @@ public abstract class KeywordSearchListsAbstract {
|
|||||||
boolean deleteList(String name) {
|
boolean deleteList(String name) {
|
||||||
boolean deleted = false;
|
boolean deleted = false;
|
||||||
KeywordSearchList delList = getList(name);
|
KeywordSearchList delList = getList(name);
|
||||||
if (delList != null) {
|
if (delList != null && !delList.isLocked()) {
|
||||||
theLists.remove(name);
|
theLists.remove(name);
|
||||||
deleted = save();
|
deleted = save();
|
||||||
}
|
}
|
||||||
|
@ -19,28 +19,99 @@
|
|||||||
package org.sleuthkit.autopsy.keywordsearch;
|
package org.sleuthkit.autopsy.keywordsearch;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileReader;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author dfickling
|
* @author dfickling
|
||||||
|
* KeywordSearchListsEncase adds support for Encase tab-delimited
|
||||||
|
* keyword list exports to Autopsy.
|
||||||
|
*
|
||||||
|
* load() does the I/O operation, converting lines from the text file to
|
||||||
|
* an unsorted list of EncaseFileEntrys
|
||||||
|
* The next step is to recreate the original folder hierarchy,
|
||||||
|
* and finally the EncaseFileEntries are converted to KeywordSearchLists
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
public class KeywordSearchListsEncase extends KeywordSearchListsAbstract{
|
public class KeywordSearchListsEncase extends KeywordSearchListsAbstract{
|
||||||
|
|
||||||
|
ArrayList<EncaseFileEntry> entriesUnsorted;
|
||||||
|
EncaseFileEntry rootEntry;
|
||||||
|
|
||||||
public KeywordSearchListsEncase(String encasePath) {
|
public KeywordSearchListsEncase(String encasePath) {
|
||||||
super(encasePath);
|
super(encasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Follow the EncaseFileEntry hierarchy starting with given entry
|
||||||
|
* Create list for each Folder entry, add keyword for each Expression
|
||||||
|
* @param entry
|
||||||
|
* @param parentPath
|
||||||
|
*/
|
||||||
|
private void doCreateListsFromEntries(EncaseFileEntry entry, String parentPath) {
|
||||||
|
String name;
|
||||||
|
if(parentPath.isEmpty()) {
|
||||||
|
name = entry.name;
|
||||||
|
} else {
|
||||||
|
name = parentPath + "/" + entry.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Keyword> children = new ArrayList<Keyword>();
|
||||||
|
for(EncaseFileEntry child : entry.children) {
|
||||||
|
switch(child.type) {
|
||||||
|
case Folder:
|
||||||
|
doCreateListsFromEntries(child, name);
|
||||||
|
break;
|
||||||
|
case Expression:
|
||||||
|
if(child.flags.contains(EncaseFlag.pg)) { // Skip GREP keywords
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
children.add(new Keyword(child.value, true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Give each list a unique name
|
||||||
|
if(theLists.containsKey(name)) {
|
||||||
|
int i = 2;
|
||||||
|
while(theLists.containsKey(name + "(" + i + ")")) {
|
||||||
|
i+=1;
|
||||||
|
}
|
||||||
|
name = name + "(" + i + ")";
|
||||||
|
}
|
||||||
|
// Don't create lists if there are no keywords
|
||||||
|
if (!children.isEmpty()) {
|
||||||
|
KeywordSearchList newList = new KeywordSearchList(name, new Date(), new Date(),
|
||||||
|
true, true, children);
|
||||||
|
theLists.put(name, newList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert entriesUnsorted (a list of childless and parentless EncaseFileEntries) into an EncaseFileEntry structure
|
||||||
|
*/
|
||||||
|
private void doCreateEntryStructure(EncaseFileEntry parent) {
|
||||||
|
if (!parent.isFull()) {
|
||||||
|
EncaseFileEntry child = entriesUnsorted.remove(0);
|
||||||
|
child.hasParent = true;
|
||||||
|
child.parent = parent;
|
||||||
|
parent.addChild(child);
|
||||||
|
if(!child.isFull()) {
|
||||||
|
doCreateEntryStructure(child);
|
||||||
|
}
|
||||||
|
if (!parent.isFull()) {
|
||||||
|
doCreateEntryStructure(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent.hasParent) {
|
||||||
|
doCreateEntryStructure(parent.parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean save() {
|
public boolean save() {
|
||||||
@ -50,37 +121,36 @@ public class KeywordSearchListsEncase extends KeywordSearchListsAbstract{
|
|||||||
@Override
|
@Override
|
||||||
public boolean load() {
|
public boolean load() {
|
||||||
try {
|
try {
|
||||||
File theFile = new File(filePath);
|
|
||||||
BufferedReader readBuffer = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "utf-16"));
|
BufferedReader readBuffer = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "utf-16"));
|
||||||
String line;
|
String structLine;
|
||||||
// If any terms are grep terms, we'll show the user a dialog at the end.
|
String metaLine;
|
||||||
boolean anyGrep = false;
|
entriesUnsorted = new ArrayList<EncaseFileEntry>();
|
||||||
List<Keyword> words = new ArrayList<Keyword>();
|
for(int line = 1; line < 6; line++) {
|
||||||
while ((line = readBuffer.readLine()) != null) {
|
readBuffer.readLine();
|
||||||
String[] tabDelim = line.split("\t");
|
}
|
||||||
if (tabDelim.length > 2) {
|
while ((structLine = readBuffer.readLine()) != null && (metaLine = readBuffer.readLine()) != null) {
|
||||||
String expr = tabDelim[2];
|
String[] structArr = structLine.split("\t");
|
||||||
if (tabDelim.length > 8) {
|
String[] metaArr = metaLine.split("\t");
|
||||||
boolean literal = tabDelim[8].isEmpty() || !tabDelim[8].equals("1");
|
EncaseMetaType type = EncaseMetaType.getType(metaArr[0]);
|
||||||
anyGrep = anyGrep || !literal;
|
String childCount = structArr[1];
|
||||||
//TODO: Stop skipping non-literal search terms
|
String name = metaArr[1];
|
||||||
if (!expr.isEmpty() && !expr.equals("t") && literal) {
|
String value = metaArr[2];
|
||||||
words.add(new Keyword(expr, literal));
|
ArrayList<EncaseFlag> flags = new ArrayList<EncaseFlag>();
|
||||||
}
|
for(int i = 0; i < 17; i++) {
|
||||||
|
if(metaArr.length < i+4) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!metaArr[i+3].equals("")) {
|
||||||
|
flags.add(EncaseFlag.getFlag(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
entriesUnsorted.add(new EncaseFileEntry(name, value, Integer.parseInt(childCount), false, null, type, flags));
|
||||||
}
|
}
|
||||||
theLists.put(theFile.getName(),
|
this.rootEntry = entriesUnsorted.remove(0);
|
||||||
new KeywordSearchList(theFile.getName(),
|
doCreateEntryStructure(this.rootEntry);
|
||||||
new Date(theFile.lastModified()),
|
doCreateListsFromEntries(this.rootEntry, "");
|
||||||
new Date(theFile.lastModified()),
|
|
||||||
true, true, words));
|
|
||||||
if(anyGrep) {
|
|
||||||
JOptionPane.showMessageDialog(null, "Importing grep (regular expression) keywords is not currently supported. Any that were in the list "
|
|
||||||
+ theFile.getName() + " have not been imported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (FileNotFoundException ex) {
|
} catch (FileNotFoundException ex) {
|
||||||
logger.log(Level.INFO, "File at " + filePath + " does not exist!", ex);
|
logger.log(Level.INFO, "File at " + filePath + " does not exist!", ex);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
@ -89,4 +159,62 @@ public class KeywordSearchListsEncase extends KeywordSearchListsAbstract{
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private enum EncaseMetaType {
|
||||||
|
Expression, Folder;
|
||||||
|
|
||||||
|
static EncaseMetaType getType(String type) {
|
||||||
|
if(type.equals("5")) {
|
||||||
|
return Folder;
|
||||||
|
} else if(type.equals("")) {
|
||||||
|
return Expression;
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Unsupported EncaseMetaType: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Flags for EncaseFileEntries.
|
||||||
|
* p8 = UTF-8
|
||||||
|
* p7 = UTF-7
|
||||||
|
* pg = GREP
|
||||||
|
*/
|
||||||
|
private enum EncaseFlag {
|
||||||
|
pc, pu, pb, p8, p7, pg, an, ph, or, di, um, st, ww, pr, lo, ta, cp;
|
||||||
|
|
||||||
|
static EncaseFlag getFlag(int i) {
|
||||||
|
return EncaseFlag.values()[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entry in the Encase keyword list file.
|
||||||
|
*/
|
||||||
|
private class EncaseFileEntry {
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
int childCount;
|
||||||
|
List<EncaseFileEntry> children;
|
||||||
|
EncaseFileEntry parent;
|
||||||
|
EncaseMetaType type;
|
||||||
|
boolean hasParent;
|
||||||
|
ArrayList<EncaseFlag> flags;
|
||||||
|
EncaseFileEntry(String name, String value, int childCount, boolean hasParent, EncaseFileEntry parent, EncaseMetaType type, ArrayList<EncaseFlag> flags) {
|
||||||
|
this.name = name;
|
||||||
|
this.value = value;
|
||||||
|
this.childCount = childCount;
|
||||||
|
this.children = new ArrayList<EncaseFileEntry>();
|
||||||
|
this.hasParent = hasParent;
|
||||||
|
this.parent = parent;
|
||||||
|
this.type = type;
|
||||||
|
this.flags = flags;
|
||||||
|
}
|
||||||
|
boolean isFull() {
|
||||||
|
return children.size() == childCount;
|
||||||
|
}
|
||||||
|
void addChild(EncaseFileEntry child) {
|
||||||
|
children.add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -78,6 +78,9 @@
|
|||||||
<TableHeader reorderingAllowed="false" resizingAllowed="true"/>
|
<TableHeader reorderingAllowed="false" resizingAllowed="true"/>
|
||||||
</Property>
|
</Property>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
<Events>
|
||||||
|
<EventHandler event="keyPressed" listener="java.awt.event.KeyListener" parameters="java.awt.event.KeyEvent" handler="listsTableKeyPressed"/>
|
||||||
|
</Events>
|
||||||
</Component>
|
</Component>
|
||||||
</SubComponents>
|
</SubComponents>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -24,23 +24,18 @@
|
|||||||
*/
|
*/
|
||||||
package org.sleuthkit.autopsy.keywordsearch;
|
package org.sleuthkit.autopsy.keywordsearch;
|
||||||
|
|
||||||
import java.awt.Component;
|
import java.awt.event.KeyEvent;
|
||||||
import java.beans.PropertyChangeEvent;
|
import java.beans.PropertyChangeEvent;
|
||||||
import java.beans.PropertyChangeListener;
|
import java.beans.PropertyChangeListener;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.TreeSet;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import javax.swing.JFileChooser;
|
import javax.swing.JFileChooser;
|
||||||
import javax.swing.JOptionPane;
|
import javax.swing.JOptionPane;
|
||||||
import javax.swing.JTable;
|
|
||||||
import javax.swing.event.ListSelectionListener;
|
import javax.swing.event.ListSelectionListener;
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
import javax.swing.table.AbstractTableModel;
|
import javax.swing.table.AbstractTableModel;
|
||||||
import javax.swing.table.DefaultTableCellRenderer;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -103,6 +98,7 @@ class KeywordSearchListsManagementPanel extends javax.swing.JPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
this.skipNSRLCheckBox.setSelected(KeywordSearchIngestService.getDefault().getSkipKnown());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,13 +120,17 @@ class KeywordSearchListsManagementPanel extends javax.swing.JPanel {
|
|||||||
setMinimumSize(new java.awt.Dimension(200, 0));
|
setMinimumSize(new java.awt.Dimension(200, 0));
|
||||||
setPreferredSize(new java.awt.Dimension(200, 297));
|
setPreferredSize(new java.awt.Dimension(200, 297));
|
||||||
|
|
||||||
jScrollPane1.setBorder(null);
|
|
||||||
jScrollPane1.setPreferredSize(new java.awt.Dimension(200, 402));
|
jScrollPane1.setPreferredSize(new java.awt.Dimension(200, 402));
|
||||||
|
|
||||||
listsTable.setModel(tableModel);
|
listsTable.setModel(tableModel);
|
||||||
listsTable.setShowHorizontalLines(false);
|
listsTable.setShowHorizontalLines(false);
|
||||||
listsTable.setShowVerticalLines(false);
|
listsTable.setShowVerticalLines(false);
|
||||||
listsTable.getTableHeader().setReorderingAllowed(false);
|
listsTable.getTableHeader().setReorderingAllowed(false);
|
||||||
|
listsTable.addKeyListener(new java.awt.event.KeyAdapter() {
|
||||||
|
public void keyPressed(java.awt.event.KeyEvent evt) {
|
||||||
|
listsTableKeyPressed(evt);
|
||||||
|
}
|
||||||
|
});
|
||||||
jScrollPane1.setViewportView(listsTable);
|
jScrollPane1.setViewportView(listsTable);
|
||||||
|
|
||||||
newListButton.setText(org.openide.util.NbBundle.getMessage(KeywordSearchListsManagementPanel.class, "KeywordSearchListsManagementPanel.newListButton.text")); // NOI18N
|
newListButton.setText(org.openide.util.NbBundle.getMessage(KeywordSearchListsManagementPanel.class, "KeywordSearchListsManagementPanel.newListButton.text")); // NOI18N
|
||||||
@ -174,7 +174,7 @@ class KeywordSearchListsManagementPanel extends javax.swing.JPanel {
|
|||||||
layout.setVerticalGroup(
|
layout.setVerticalGroup(
|
||||||
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
|
||||||
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
|
||||||
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 267, Short.MAX_VALUE)
|
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 249, Short.MAX_VALUE)
|
||||||
.addGap(0, 0, 0)
|
.addGap(0, 0, 0)
|
||||||
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
.addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
|
||||||
.addComponent(newListButton)
|
.addComponent(newListButton)
|
||||||
@ -290,6 +290,18 @@ class KeywordSearchListsManagementPanel extends javax.swing.JPanel {
|
|||||||
KeywordSearchIngestService.getDefault().setSkipKnown(skipNSRLCheckBox.isSelected());
|
KeywordSearchIngestService.getDefault().setSkipKnown(skipNSRLCheckBox.isSelected());
|
||||||
}//GEN-LAST:event_skipNSRLCheckBoxActionPerformed
|
}//GEN-LAST:event_skipNSRLCheckBoxActionPerformed
|
||||||
|
|
||||||
|
private void listsTableKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_listsTableKeyPressed
|
||||||
|
if(evt.getKeyCode() == KeyEvent.VK_DELETE) {
|
||||||
|
int[] selected = listsTable.getSelectedRows();
|
||||||
|
if(selected.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
KeywordSearchListsXML deleter = KeywordSearchListsXML.getCurrent();
|
||||||
|
String listName = deleter.getListNames().get(selected[0]);
|
||||||
|
KeywordSearchListsXML.getCurrent().deleteList(listName);
|
||||||
|
}
|
||||||
|
}//GEN-LAST:event_listsTableKeyPressed
|
||||||
|
|
||||||
// Variables declaration - do not modify//GEN-BEGIN:variables
|
// Variables declaration - do not modify//GEN-BEGIN:variables
|
||||||
private javax.swing.JButton importButton;
|
private javax.swing.JButton importButton;
|
||||||
private javax.swing.JScrollPane jScrollPane1;
|
private javax.swing.JScrollPane jScrollPane1;
|
||||||
|
@ -158,12 +158,6 @@ class KeywordSearchListsViewerPanel extends AbstractKeywordSearchPerformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ingestListener = new ActionListener() {
|
|
||||||
@Override
|
|
||||||
public void actionPerformed(ActionEvent e) {
|
|
||||||
addToIngestAction(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
searchListener = new ActionListener() {
|
searchListener = new ActionListener() {
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
@ -329,12 +323,6 @@ class KeywordSearchListsViewerPanel extends AbstractKeywordSearchPerformer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addToIngestAction(ActionEvent e) {
|
|
||||||
for(KeywordSearchList list : listsTableModel.getSelectedListsL()){
|
|
||||||
KeywordSearchIngestService.getDefault().addToKeywordLists(list.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Keyword> getQueryList() {
|
public List<Keyword> getQueryList() {
|
||||||
List<Keyword> ret = new ArrayList<Keyword>();
|
List<Keyword> ret = new ArrayList<Keyword>();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user