Autopsy  4.19.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
AbstractAbstractFileNode.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2012-2021 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.datamodel;
20 
21 import java.beans.PropertyChangeEvent;
22 import java.beans.PropertyChangeListener;
23 import java.lang.ref.WeakReference;
24 import java.text.MessageFormat;
25 import java.util.ArrayList;
26 import java.util.EnumSet;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.logging.Level;
31 import java.util.stream.Collectors;
32 import org.apache.commons.lang3.StringUtils;
33 import org.apache.commons.lang3.tuple.Pair;
34 import org.openide.nodes.Sheet;
35 import org.openide.util.NbBundle;
36 import org.openide.util.WeakListeners;
50 import static org.sleuthkit.autopsy.datamodel.Bundle.*;
60 import org.sleuthkit.datamodel.AbstractFile;
61 import org.sleuthkit.datamodel.Content;
62 import org.sleuthkit.datamodel.ContentTag;
63 import org.sleuthkit.datamodel.Tag;
64 import org.sleuthkit.datamodel.TskCoreException;
68 import org.sleuthkit.datamodel.Score;
69 
75 public abstract class AbstractAbstractFileNode<T extends AbstractFile> extends AbstractContentNode<T> {
76 
77  private static final Logger logger = Logger.getLogger(AbstractAbstractFileNode.class.getName());
78 
79  private static final Set<Case.Events> CASE_EVENTS_OF_INTEREST = EnumSet.of(Case.Events.CURRENT_CASE,
81  private static final Set<IngestManager.IngestModuleEvent> INGEST_MODULE_EVENTS_OF_INTEREST = EnumSet.of(CONTENT_CHANGED);
82 
86  AbstractAbstractFileNode(T abstractFile) {
87  super(abstractFile);
88  String ext = abstractFile.getNameExtension();
89  if (StringUtils.isNotBlank(ext)) {
90  ext = "." + ext;
91  // If this is an archive file we will listen for ingest events
92  // that will notify us when new content has been identified.
93  if (FileTypeExtensions.getArchiveExtensions().contains(ext)) {
95  }
96  }
97 
98  try {
99  //See JIRA-5971
100  //Attempt to cache file path during construction of this UI component.
101  this.content.getUniquePath();
102  } catch (TskCoreException ex) {
103  logger.log(Level.SEVERE, String.format("Failed attempt to cache the "
104  + "unique path of the abstract file instance. Name: %s (objID=%d)",
105  this.content.getName(), this.content.getId()), ex);
106  }
107 
109  backgroundTasksPool.submit(new TranslationTask(
110  new WeakReference<>(this), weakPcl));
111  }
112 
113  // Listen for case events so that we can detect when the case is closed
114  // or when tags are added.
116  }
117 
127  @Override
128  protected void finalize() throws Throwable {
129  super.finalize();
130  removeListeners();
131  }
132 
133  private void removeListeners() {
136  }
137 
138  private final PropertyChangeListener pcl = (PropertyChangeEvent evt) -> {
139  String eventType = evt.getPropertyName();
140 
141  // Is this a content changed event?
142  if (eventType.equals(IngestManager.IngestModuleEvent.CONTENT_CHANGED.toString())) {
143  if ((evt.getOldValue() instanceof ModuleContentEvent) == false) {
144  return;
145  }
146  ModuleContentEvent moduleContentEvent = (ModuleContentEvent) evt.getOldValue();
147  if ((moduleContentEvent.getSource() instanceof Content) == false) {
148  return;
149  }
150  Content newContent = (Content) moduleContentEvent.getSource();
151 
152  // Does the event indicate that content has been added to *this* file?
153  if (getContent().getId() == newContent.getId()) {
154  // If so, refresh our children.
155  try {
156  // We only want to refresh our parents children if we are in the
157  // data sources branch of the tree. The parent nodes in other
158  // branches of the tree (e.g. File Types and Deleted Files) do
159  // not need to be refreshed.
160  BaseChildFactory.post(getParentNode().getName(), new RefreshKeysEvent());
161  } catch (NullPointerException ex) {
162  // Skip
163  } catch (NoSuchEventBusException ex) {
164  logger.log(Level.WARNING, "Failed to post key refresh event", ex); //NON-NLS
165  }
166  }
167  } else if (eventType.equals(Case.Events.CURRENT_CASE.toString())) {
168  if (evt.getNewValue() == null) {
169  // case was closed. Remove listeners so that we don't get called with a stale case handle
170  removeListeners();
171  }
172  /*
173  * No need to do any asynchrony around tag added, deleted or CR
174  * change events, they are so infrequent and user driven that we can
175  * just keep a simple blocking approach, where we go out to the
176  * database ourselves.
177  */
178  } else if (eventType.equals(Case.Events.CONTENT_TAG_ADDED.toString())) {
180  if (event.getAddedTag().getContent().equals(content)) {
181  List<Tag> tags = this.getAllTagsFromDatabase();
182  Pair<Score, String> scorePropAndDescr = getScorePropertyAndDescription(tags);
183  Score value = scorePropAndDescr.getLeft();
184  String descr = scorePropAndDescr.getRight();
185  List<CorrelationAttributeInstance> listWithJustFileAttr = new ArrayList<>();
187  if (corrInstance != null) {
188  listWithJustFileAttr.add(corrInstance);
189  }
190  updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), descr, value),
191  new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, listWithJustFileAttr))
192  );
193  }
194  } else if (eventType.equals(Case.Events.CONTENT_TAG_DELETED.toString())) {
196  if (event.getDeletedTagInfo().getContentID() == content.getId()) {
197  List<Tag> tags = getAllTagsFromDatabase();
198  Pair<Score, String> scorePropAndDescr = getScorePropertyAndDescription(tags);
199  Score value = scorePropAndDescr.getLeft();
200  String descr = scorePropAndDescr.getRight();
201  List<CorrelationAttributeInstance> listWithJustFileAttr = new ArrayList<>();
203  if (corrInstance != null) {
204  listWithJustFileAttr.add(corrInstance);
205  }
206  updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), descr, value),
207  new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, listWithJustFileAttr))
208  );
209  }
210  } else if (eventType.equals(Case.Events.CR_COMMENT_CHANGED.toString())) {
212  if (event.getContentID() == content.getId()) {
213  List<Tag> tags = getAllTagsFromDatabase();
214  List<CorrelationAttributeInstance> listWithJustFileAttr = new ArrayList<>();
216  if (corrInstance != null) {
217  listWithJustFileAttr.add(corrInstance);
218  }
219  updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, getCommentProperty(tags, listWithJustFileAttr)));
220  }
221  } else if (eventType.equals(NodeSpecificEvents.TRANSLATION_AVAILABLE.toString())) {
222  this.setDisplayName(evt.getNewValue().toString());
223  //Set the tooltip
224  this.setShortDescription(content.getName());
225  updateSheet(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, content.getName()));
226  } else if (eventType.equals(NodeSpecificEvents.SCO_AVAILABLE.toString()) && !UserPreferences.getHideSCOColumns()) {
227  SCOData scoData = (SCOData) evt.getNewValue();
228  if (scoData.getScoreAndDescription() != null) {
229  updateSheet(new NodeProperty<>(SCORE.toString(), SCORE.toString(), scoData.getScoreAndDescription().getRight(), scoData.getScoreAndDescription().getLeft()));
230  }
231  if (scoData.getComment() != null) {
232  updateSheet(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), NO_DESCR, scoData.getComment()));
233  }
234  if (scoData.getCountAndDescription() != null) {
235  updateSheet(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), scoData.getCountAndDescription().getRight(), scoData.getCountAndDescription().getLeft()));
236  }
237  }
238  };
247  private final PropertyChangeListener weakPcl = WeakListeners.propertyChange(pcl, null);
248 
249  /*
250  * This is called when the node is first initialized. Any new updates or
251  * changes happen by directly manipulating the sheet. That means we can fire
252  * off background events everytime this method is called and not worry about
253  * duplicated jobs.
254  */
255  @Override
256  protected synchronized Sheet createSheet() {
257  Sheet sheet = new Sheet();
258  Sheet.Set sheetSet = Sheet.createPropertiesSet();
259  sheet.put(sheetSet);
260 
261  //This will fire off fresh background tasks.
262  List<NodeProperty<?>> newProperties = getProperties();
263  newProperties.forEach((property) -> {
264  sheetSet.put(property);
265  });
266 
267  return sheet;
268  }
269 
270  @NbBundle.Messages({"AbstractAbstractFileNode.nameColLbl=Name",
271  "AbstractAbstractFileNode.originalName=Original Name",
272  "AbstractAbstractFileNode.createSheet.score.name=S",
273  "AbstractAbstractFileNode.createSheet.comment.name=C",
274  "AbstractAbstractFileNode.createSheet.count.name=O",
275  "AbstractAbstractFileNode.locationColLbl=Location",
276  "AbstractAbstractFileNode.modifiedTimeColLbl=Modified Time",
277  "AbstractAbstractFileNode.changeTimeColLbl=Change Time",
278  "AbstractAbstractFileNode.accessTimeColLbl=Access Time",
279  "AbstractAbstractFileNode.createdTimeColLbl=Created Time",
280  "AbstractAbstractFileNode.sizeColLbl=Size",
281  "AbstractAbstractFileNode.flagsDirColLbl=Flags(Dir)",
282  "AbstractAbstractFileNode.flagsMetaColLbl=Flags(Meta)",
283  "AbstractAbstractFileNode.modeColLbl=Mode",
284  "AbstractAbstractFileNode.useridColLbl=UserID",
285  "AbstractAbstractFileNode.groupidColLbl=GroupID",
286  "AbstractAbstractFileNode.metaAddrColLbl=Meta Addr.",
287  "AbstractAbstractFileNode.attrAddrColLbl=Attr. Addr.",
288  "AbstractAbstractFileNode.typeDirColLbl=Type(Dir)",
289  "AbstractAbstractFileNode.typeMetaColLbl=Type(Meta)",
290  "AbstractAbstractFileNode.knownColLbl=Known",
291  "AbstractAbstractFileNode.md5HashColLbl=MD5 Hash",
292  "AbstractAbstractFileNode.sha256HashColLbl=SHA-256 Hash",
293  "AbstractAbstractFileNode.objectId=Object ID",
294  "AbstractAbstractFileNode.mimeType=MIME Type",
295  "AbstractAbstractFileNode.extensionColLbl=Extension"})
297 
298  NAME(AbstractAbstractFileNode_nameColLbl()),
299  ORIGINAL_NAME(AbstractAbstractFileNode_originalName()),
300  SCORE(AbstractAbstractFileNode_createSheet_score_name()),
301  COMMENT(AbstractAbstractFileNode_createSheet_comment_name()),
302  OCCURRENCES(AbstractAbstractFileNode_createSheet_count_name()),
303  LOCATION(AbstractAbstractFileNode_locationColLbl()),
304  MOD_TIME(AbstractAbstractFileNode_modifiedTimeColLbl()),
305  CHANGED_TIME(AbstractAbstractFileNode_changeTimeColLbl()),
306  ACCESS_TIME(AbstractAbstractFileNode_accessTimeColLbl()),
307  CREATED_TIME(AbstractAbstractFileNode_createdTimeColLbl()),
308  SIZE(AbstractAbstractFileNode_sizeColLbl()),
309  FLAGS_DIR(AbstractAbstractFileNode_flagsDirColLbl()),
310  FLAGS_META(AbstractAbstractFileNode_flagsMetaColLbl()),
311  MODE(AbstractAbstractFileNode_modeColLbl()),
312  USER_ID(AbstractAbstractFileNode_useridColLbl()),
313  GROUP_ID(AbstractAbstractFileNode_groupidColLbl()),
314  META_ADDR(AbstractAbstractFileNode_metaAddrColLbl()),
315  ATTR_ADDR(AbstractAbstractFileNode_attrAddrColLbl()),
316  TYPE_DIR(AbstractAbstractFileNode_typeDirColLbl()),
317  TYPE_META(AbstractAbstractFileNode_typeMetaColLbl()),
318  KNOWN(AbstractAbstractFileNode_knownColLbl()),
319  MD5HASH(AbstractAbstractFileNode_md5HashColLbl()),
320  SHA256HASH(AbstractAbstractFileNode_sha256HashColLbl()),
321  ObjectID(AbstractAbstractFileNode_objectId()),
322  MIMETYPE(AbstractAbstractFileNode_mimeType()),
323  EXTENSION(AbstractAbstractFileNode_extensionColLbl());
324 
325  final private String displayString;
326 
327  private AbstractFilePropertyType(String displayString) {
328  this.displayString = displayString;
329  }
330 
331  @Override
332  public String toString() {
333  return displayString;
334  }
335  }
336 
340  private List<NodeProperty<?>> getProperties() {
341  List<NodeProperty<?>> properties = new ArrayList<>();
342  properties.add(new NodeProperty<>(NAME.toString(), NAME.toString(), NO_DESCR, getContentDisplayName(content)));
343  /*
344  * Initialize an empty place holder value. At the bottom, we kick off a
345  * background task that promises to update these values.
346  */
347 
349  properties.add(new NodeProperty<>(ORIGINAL_NAME.toString(), ORIGINAL_NAME.toString(), NO_DESCR, ""));
350  }
351 
352  // Create place holders for S C O
354  properties.add(new NodeProperty<>(SCORE.toString(), SCORE.toString(), VALUE_LOADING, ""));
355  properties.add(new NodeProperty<>(COMMENT.toString(), COMMENT.toString(), VALUE_LOADING, ""));
357  properties.add(new NodeProperty<>(OCCURRENCES.toString(), OCCURRENCES.toString(), VALUE_LOADING, ""));
358  }
359  // Get the SCO columns data in a background task
360  backgroundTasksPool.submit(new GetSCOTask(
361  new WeakReference<>(this), weakPcl));
362  }
363 
364  properties.add(new NodeProperty<>(MOD_TIME.toString(), MOD_TIME.toString(), NO_DESCR, TimeZoneUtils.getFormattedTime(content.getMtime())));
365  properties.add(new NodeProperty<>(CHANGED_TIME.toString(), CHANGED_TIME.toString(), NO_DESCR, TimeZoneUtils.getFormattedTime(content.getCtime())));
366  properties.add(new NodeProperty<>(ACCESS_TIME.toString(), ACCESS_TIME.toString(), NO_DESCR, TimeZoneUtils.getFormattedTime(content.getAtime())));
367  properties.add(new NodeProperty<>(CREATED_TIME.toString(), CREATED_TIME.toString(), NO_DESCR, TimeZoneUtils.getFormattedTime(content.getCrtime())));
368  properties.add(new NodeProperty<>(SIZE.toString(), SIZE.toString(), NO_DESCR, content.getSize()));
369  properties.add(new NodeProperty<>(FLAGS_DIR.toString(), FLAGS_DIR.toString(), NO_DESCR, content.getDirFlagAsString()));
370  properties.add(new NodeProperty<>(FLAGS_META.toString(), FLAGS_META.toString(), NO_DESCR, content.getMetaFlagsAsString()));
371  properties.add(new NodeProperty<>(KNOWN.toString(), KNOWN.toString(), NO_DESCR, content.getKnown().getName()));
372  properties.add(new NodeProperty<>(LOCATION.toString(), LOCATION.toString(), NO_DESCR, getContentPath(content)));
373  properties.add(new NodeProperty<>(MD5HASH.toString(), MD5HASH.toString(), NO_DESCR, StringUtils.defaultString(content.getMd5Hash())));
374  properties.add(new NodeProperty<>(SHA256HASH.toString(), SHA256HASH.toString(), NO_DESCR, StringUtils.defaultString(content.getSha256Hash())));
375  properties.add(new NodeProperty<>(MIMETYPE.toString(), MIMETYPE.toString(), NO_DESCR, StringUtils.defaultString(content.getMIMEType())));
376  properties.add(new NodeProperty<>(EXTENSION.toString(), EXTENSION.toString(), NO_DESCR, content.getNameExtension()));
377 
378  return properties;
379  }
380 
390  @NbBundle.Messages("AbstractAbstractFileNode.tagsProperty.displayName=Tags")
391  @Deprecated
392  protected void addTagProperty(Sheet.Set sheetSet) {
393  List<ContentTag> tags = getContentTagsFromDatabase();
394  sheetSet.put(new NodeProperty<>("Tags", AbstractAbstractFileNode_tagsProperty_displayName(),
395  NO_DESCR, tags.stream().map(t -> t.getName().getDisplayName())
396  .distinct()
397  .collect(Collectors.joining(", "))));
398  }
399 
410  @Deprecated
411  protected static String getHashSetHitsCsvList(AbstractFile file) {
412  try {
413  return StringUtils.join(file.getHashSetNames(), ", ");
414  } catch (TskCoreException tskCoreException) {
415  logger.log(Level.WARNING, "Error getting hashset hits: ", tskCoreException); //NON-NLS
416  return "";
417  }
418  }
419 
420  @NbBundle.Messages({
421  "AbstractAbstractFileNode.createSheet.count.displayName=O",
422  "AbstractAbstractFileNode.createSheet.count.hashLookupNotRun.description=Hash lookup had not been run on this file when the column was populated",
423  "# {0} - occurrenceCount",
424  "AbstractAbstractFileNode.createSheet.count.description=There were {0} datasource(s) found with occurrences of the MD5 correlation value"})
425  @Override
426  protected Pair<Long, String> getCountPropertyAndDescription(CorrelationAttributeInstance attributeInstance, String defaultDescription) {
427  Long count = -1L; //The column renderer will not display negative values, negative value used when count unavailble to preserve sorting
428  String description = defaultDescription;
429  try {
430  //don't perform the query if there is no correlation value
431  if (attributeInstance != null && StringUtils.isNotBlank(attributeInstance.getCorrelationValue())) {
433  description = Bundle.AbstractAbstractFileNode_createSheet_count_description(count);
434  } else if (attributeInstance != null) {
435  description = Bundle.AbstractAbstractFileNode_createSheet_count_hashLookupNotRun_description();
436  }
437  } catch (CentralRepoException ex) {
438  logger.log(Level.WARNING, "Error getting count of datasources with correlation attribute", ex);
440  logger.log(Level.WARNING, "Unable to normalize data to get count of datasources with correlation attribute", ex);
441  }
442  return Pair.of(count, description);
443  }
444 
445  @NbBundle.Messages({
446  "AbstractAbstractFileNode.createSheet.comment.displayName=C"})
447  @Override
448  protected HasCommentStatus getCommentProperty(List<Tag> tags, List<CorrelationAttributeInstance> attributes) {
449 
451 
452  for (Tag tag : tags) {
453  if (!StringUtils.isBlank(tag.getComment())) {
454  //if the tag is null or empty or contains just white space it will indicate there is not a comment
456  break;
457  }
458  }
459  if (attributes != null && !attributes.isEmpty()) {
460  for (CorrelationAttributeInstance attribute : attributes) {
461  if (attribute != null && !StringUtils.isBlank(attribute.getComment())) {
464  } else {
466  }
467  break;
468  }
469  }
470  }
471  return status;
472  }
473 
480  String getTranslatedFileName() {
481  try {
482  return FileNameTranslationUtil.translate(content.getName());
483  } catch (NoServiceProviderException | TranslationException ex) {
484  logger.log(Level.WARNING, MessageFormat.format("Error translating file name (objID={0}))", content.getId()), ex);
485  return "";
486  }
487  }
488 
494  List<ContentTag> getContentTagsFromDatabase() {
495  List<ContentTag> tags = new ArrayList<>();
496  try {
497  tags.addAll(Case.getCurrentCaseThrows().getServices().getTagsManager().getContentTagsByContent(content));
498  } catch (TskCoreException | NoCurrentCaseException ex) {
499  logger.log(Level.SEVERE, "Failed to get tags for content " + content.getName(), ex);
500  }
501  return tags;
502  }
503 
504  @Override
505  protected List<Tag> getAllTagsFromDatabase() {
506  return new ArrayList<>(getContentTagsFromDatabase());
507  }
508 
509  static String getContentPath(AbstractFile file) {
510  try {
511  return file.getUniquePath();
512  } catch (TskCoreException ex) {
513  logger.log(Level.SEVERE, "Except while calling Content.getUniquePath() on " + file.getName(), ex); //NON-NLS
514  return ""; //NON-NLS
515  }
516  }
517 
518  static String getContentDisplayName(AbstractFile file) {
519  String name = file.getName();
520  switch (name) {
521  case "..":
522  return DirectoryNode.DOTDOTDIR;
523  case ".":
524  return DirectoryNode.DOTDIR;
525  default:
526  return name;
527  }
528  }
529 
540  static public void fillPropertyMap(Map<String, Object> map, AbstractFile content) {
541  map.put(NAME.toString(), getContentDisplayName(content));
542  map.put(LOCATION.toString(), getContentPath(content));
543  map.put(MOD_TIME.toString(), TimeZoneUtils.getFormattedTime(content.getMtime()));
544  map.put(CHANGED_TIME.toString(), TimeZoneUtils.getFormattedTime(content.getCtime()));
545  map.put(ACCESS_TIME.toString(), TimeZoneUtils.getFormattedTime(content.getAtime()));
546  map.put(CREATED_TIME.toString(), TimeZoneUtils.getFormattedTime(content.getCrtime()));
547  map.put(SIZE.toString(), content.getSize());
548  map.put(FLAGS_DIR.toString(), content.getDirFlagAsString());
549  map.put(FLAGS_META.toString(), content.getMetaFlagsAsString());
550  map.put(KNOWN.toString(), content.getKnown().getName());
551  map.put(MD5HASH.toString(), StringUtils.defaultString(content.getMd5Hash()));
552  map.put(SHA256HASH.toString(), StringUtils.defaultString(content.getSha256Hash()));
553  map.put(MIMETYPE.toString(), StringUtils.defaultString(content.getMIMEType()));
554  map.put(EXTENSION.toString(), content.getNameExtension());
555  }
556 }
synchronized void updateSheet(NodeProperty<?>...newProps)
void removeIngestModuleEventListener(final PropertyChangeListener listener)
static synchronized IngestManager getInstance()
static String getFormattedTime(long epochTime)
HasCommentStatus getCommentProperty(List< Tag > tags, List< CorrelationAttributeInstance > attributes)
static final Set< IngestManager.IngestModuleEvent > INGEST_MODULE_EVENTS_OF_INTEREST
static void fillPropertyMap(Map< String, Object > map, AbstractFile content)
Pair< Long, String > getCountPropertyAndDescription(CorrelationAttributeInstance attributeInstance, String defaultDescription)
static CorrelationAttributeInstance getCorrAttrForFile(AbstractFile file)
Pair< Score, String > getScorePropertyAndDescription(List< Tag > tags)
static void post(String nodeName, Object event)
Long getCountCasesWithOtherInstances(CorrelationAttributeInstance instance)
void addIngestModuleEventListener(final PropertyChangeListener listener)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static void addEventTypeSubscriber(Set< Events > eventTypes, PropertyChangeListener subscriber)
Definition: Case.java:711
static void removeEventTypeSubscriber(Set< Events > eventTypes, PropertyChangeListener subscriber)
Definition: Case.java:756

Copyright © 2012-2021 Basis Technology. Generated on: Thu Sep 30 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.