Prepare open source release

main
Joe Rozner 2020-05-13 12:13:59 -07:00
commit e91b9f3dc3
98 changed files with 7574 additions and 0 deletions

7
.clang-format Normal file
View File

@ -0,0 +1,7 @@
BasedOnStyle: Google
ColumnLimit: 100
# Not sure how to get BreakBeforeBraces: BS_Stroustrup to work -- this should hopefully be roughly equivalent
BreakBeforeBraces: Custom
BraceWrapping:
BeforeCatch: true
BeforeElse: true

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
# IDE Projects/data
*.pro.user
.vscode

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "tools/UGlobalHotkey"]
path = tools/UGlobalHotkey
url = git@github.com:JoelAtDeluxe/UGlobalHotkey.git

52
Code-of-Conduct.md Normal file
View File

@ -0,0 +1,52 @@
# Verizon Media Open Source Code of Conduct
## Summary
This Code of Conduct is our way to encourage good behavior and discourage bad behavior in our open source projects. We invite participation from many people to bring different perspectives to our projects. We will do our part to foster a welcoming and professional environment free of harassment. We expect participants to communicate professionally and thoughtfully during their involvement with this project.
Participants may lose their good standing by engaging in misconduct. For example: insulting, threatening, or conveying unwelcome sexual content. We ask participants who observe conduct issues to report the incident directly to the project's Response Team at opensource-conduct@verizonmedia.com. Verizon Media will assign a respondent to address the issue. We may remove harassers from this project.
This code does not replace the terms of service or acceptable use policies of the websites used to support this project. We acknowledge that participants may be subject to additional conduct terms based on their employment which may govern their online expressions.
## Details
This Code of Conduct makes our expectations of participants in this community explicit.
* We forbid harassment and abusive speech within this community.
* We request participants to report misconduct to the projects Response Team.
* We urge participants to refrain from using discussion forums to play out a fight.
### Expected Behaviors
We expect participants in this community to conduct themselves professionally. Since our primary mode of communication is text on an online forum (e.g. issues, pull requests, comments, emails, or chats) devoid of vocal tone, gestures, or other context that is often vital to understanding, it is important that participants are attentive to their interaction style.
* **Assume positive intent.** We ask community members to assume positive intent on the part of other peoples communications. We may disagree on details, but we expect all suggestions to be supportive of the community goals.
* **Respect participants.** We expect occasional disagreements. Open Source projects are learning experiences. Ask, explore, challenge, and then _respectfully_ state if you agree or disagree. If your idea is rejected, be more persuasive not bitter.
* **Welcoming to new members.** New members bring new perspectives. Some ask questions that have been addressed before. _Kindly_ point to existing discussions. Everyone is new to every project once.
* **Be kind to beginners.** Beginners use open source projects to get experience. They might not be talented coders yet, and projects should not accept poor quality code. But we were all beginners once, and we need to engage kindly.
* **Consider your impact on others.** Your work will be used by others, and you depend on the work of others. We expect community members to be considerate and establish a balance their self-interest with communal interest.
* **Use words carefully.** We may not understand intent when you say something ironic. Often, people will misinterpret sarcasm in online communications. We ask community members to communicate plainly.
* **Leave with class.** When you wish to resign from participating in this project for any reason, you are free to fork the code and create a competitive project. Open Source explicitly allows this. Your exit should not be dramatic or bitter.
### Unacceptable Behaviors
Participants remain in good standing when they do not engage in misconduct or harassment (some examples follow). We do not list all forms of harassment, nor imply some forms of harassment are not worthy of action. Any participant who *feels* harassed or *observes* harassment, should report the incident to the Response Team.
* **Don't be a bigot.** Calling out project members by their identity or background in a negative or insulting manner. This includes, but is not limited to, slurs or insinuations related to protected or suspect classes e.g. race, color, citizenship, national origin, political belief, religion, sexual orientation, gender identity and expression, age, size, culture, ethnicity, genetic features, language, profession, national minority status, mental or physical ability.
* **Don't insult.** Insulting remarks about a persons lifestyle practices.
* **Don't dox.** Revealing private information about other participants without explicit permission.
* **Don't intimidate.** Threats of violence or intimidation of any project member.
* **Don't creep.** Unwanted sexual attention or content unsuited for the subject of this project.
* **Don't inflame.** We ask that victim of harassment not address their grievances in the public forum, as this often intensifies the problem. Report it, and let us address it off-line.
* **Don't disrupt.** Sustained disruptions in a discussion.
### Reporting Issues
If you experience or witness misconduct, or have any other concerns about the conduct of members of this project, please report it by contacting our Response Team at opensource-conduct@verizonmedia.com who will handle your report with discretion. Your report should include:
* Your preferred contact information. We cannot process anonymous reports.
* Names (real or usernames) of those involved in the incident.
* Your account of what occurred, and if the incident is ongoing. Please provide links to or transcripts of the publicly available records (e.g. a mailing list archive or a public IRC logger), so that we can review it.
* Any additional information that may be helpful to achieve resolution.
After filing a report, a representative will contact you directly to review the incident and ask additional questions. If a member of the Verizon Media Response Team is named in an incident report, that member will be recused from handling your incident. If the complaint originates from a member of the Response Team, it will be addressed by a different member of the Response Team. We will consider reports to be confidential for the purpose of protecting victims of abuse.
### Scope
Verizon Media will assign a Response Team member with admin rights on the project and legal rights on the project copyright. The Response Team is empowered to restrict some privileges to the project as needed. Since this project is governed by an open source license, any participant may fork the code under the terms of the project license. The Response Teams goal is to preserve the project if possible, and will restrict or remove participation from those who disrupt the project.
This code does not replace the terms of service or acceptable use policies that are provided by the websites used to support this community. Nor does this code apply to communications or actions that take place outside of the context of this community. Many participants in this project are also subject to codes of conduct based on their employment. This code is a social-contract that informs participants of our social expectations. It is not a terms of service or legal contract.
## License and Acknowledgment.
This text is shared under the [CC-BY-4.0 license](https://creativecommons.org/licenses/by/4.0/). This code is based on a study conducted by the [TODO Group](https://todogroup.org/) of many codes used in the open source community. If you have feedback about this code, contact our Response Team at the address listed above.

26
Contributing.md Normal file
View File

@ -0,0 +1,26 @@
# How to contribute
First, thanks for taking the time to contribute to our project! There are many ways you can help out.
### Questions
If you have a question that needs an answer, [create an issue](https://help.github.com/articles/creating-an-issue/), and label it as a question.
### Issues for bugs or feature requests
If you encounter any bugs in the code, or want to request a new feature or enhancement, please [create an issue](https://help.github.com/articles/creating-an-issue/) to report it. Kindly add a label to indicate what type of issue it is.
### Contribute Code
We welcome your pull requests for bug fixes. To implement something new, please create an issue first so we can discuss it together.
***Creating a Pull Request***
Please follow [best practices](https://github.com/trein/dev-best-practices/wiki/Git-Commit-Best-Practices) for creating git commits.
When your code is ready to be submitted, [submit a pull request](https://help.github.com/articles/creating-a-pull-request/) to begin the code review process.
We only seek to accept code that you are authorized to contribute to the project. We have added a pull request template on our projects so that your contributions are made with the following confirmation:
> I confirm that this contribution is made under the terms of the license found in the root directory of this repository's source tree and that I have the authority necessary to make this contribution on behalf of its copyright owner.
## Code of Conduct
We encourage inclusive and professional interactions on our project. We welcome everyone to open an issue, improve the documentation, report bug or ssubmit a pull request. By participating in this project, you agree to abide by the [Verizon Media Code of Conduct](Code-of-Conduct.md). If you feel there is a conduct issue related to this project, please raise it per the Code of Conduct process and we will address it.

674
LICENSE Normal file
View File

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

163
README.md Normal file
View File

@ -0,0 +1,163 @@
# AScreen
A Qt tray-type application that allows for capturing (via user-selectable area or entire window) screenshots, or codeblocks associated with a particular ASHIRT instance.
## Table of Contents
- [Background](#background)
- [Install](#install)
- [Configuration](#configuration)
- [Contribute](#contribute)
- [License](#license)
## Background
This application allows users to connect to a remote ASHIRT backend and create and submit new evidence. Screenshots are taken using a custom, user-defined key, or alternately by selecting the appropriate action in the tray menu. Codeblocks can be added via an action in the tray. Both can be managed from within the application.
## Install
Official releases for Mac and Linux will be provided via the releases tab in GitHub along with source code for users to build themselves if desired.
## Non-tray OSes
Current Status: Non-functional
Some OSes/desktops do not support a tray (e.g. ice3 window manager). Currently, in these cases, the application will not work, and simply exit instead. Eventually, a simple CLI will be set up to continue to interact with this application.
## Getting Started
On the first launch, the user must first set up an appropriate configuration. When the tray displays, open the tray and select `Settings`. From here, you will be presented with some options:
| Field | Meaning |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Evidence Repository | Where evidence is stored. Note that this is a jumping off point. Files are stored in a subdirectory using the operation name |
| Access Key | The (shorter) base-64 key given by the AShirt front end (look for this in Account Settings) |
| Secret Key | The (longer) base-64 key given by the AShirt front end |
| Host Path | The http location to the AShirt server |
| Capture Area Command | The CLI command to take a screenshot of an arbitrary area and save to a file. More on this below |
| [Capture Area Command] Shortcut | The key combination used (at a system level) to trigger the capture area command |
| Capture Window Command | The CLI command to take of a given window, and save to a file |
| [Capture Area Command] Shortcut | The key combination used (at a system level) to trigger the capture window command |
Once the above is configured, save the settings and you can now select an operation. Open the tray, and under `Select Operation`, choose an operation to start using the application. (Note: you may need to choose `Refresh Operations` in the submenu)
## Screenshot Commands
This application requires taking screenshots from the command line. The application _must_:
1. Allow for saving the screenshot to a named file.
2. Create the file _must_ before the application exits.
Theorectically, any application that satisfies this requirement will work. For Mac, the system command to do this is pre-populated, since this is a standard feature. For Linux, there are a number of screenshot commands, and so none are provided. For Windows, a 3rd party application must be used, as there is currently no way to save a screenshot to a named file.
This tool will replace the above filename with `%file` as noted below:
| OS/DE/App | Capture Window | Capture Area | Notes |
| ----------- | ---------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Linux/Gnome | gnome-screenshot -w -f %file | gnome-screenshot -a -f %file | Capture window captures the focused window, rather than allowing a selection; adding the `--delay` flag can help mitigate choosing the wrong window |
| MacOS X | screencapture -w %file | screencapture -s %file | |
Note: this application expects a _single, basic command_. While piping output to another command _may_ work, it is not guaranteed. Likewise, providing multiple commands on the same "line" _may_ work, but is also not guaranteed. Offically, both of these techniques are unsupported.
### Shortcuts
Global shortcut keys can be registered with your computer, depending on the exact operating system. These shortcuts may conflict with shortcuts for a given application, where it is unclear which shortcut will trigger. All this is to say that this feature, while supported, may not work perfectly every time. That said, here is how you configure shortcuts:
Within `Settings` next to each capture command is a small text box to provide the shortcut. Each shortcut should add in one or more modifier keys (e.g. `ctrl`) in order to provide less of a chance to interfere with other system/application commands. These modifier keys have reserved names for shortcuts, noted in the below table:
| Key | Name | Alternate Names |
| ------------------------------ | --------- | --------------- |
| Shift | `shift` | `shft` |
| Control | `control` | `ctrl` |
| Alt | `alt` | -- |
| Windows/Meta/MacOS Command key | `meta` | `win` |
To specify a shortcut pattern, simply decide on what set of modifier keys you want, plus a single alphanumeric key (or F- key), and separate these by `+`.
E.g. `Ctrl+Shift+p`
## Switching and Pausing Operations
Evidence is tied to an operation, and when an operation is paused, or inactive, then the hotkey commands will not work. It may be useful to pause an operation (within this application) once you know you will not be taking additional screenshots. Simply navigate to `Select Operation` > `Pause Operation` to pause an operation, and go to the same place (now called `Enable Operation`) to resume this application. Note: pausing an operation a really more of a pause of this application. You can still use the AShirt website. However, when switching operations, if you were previously in a paused state, you will remain in a paused state.
To change operations, similarly to pause operation, navigate to `Select Operation` and choose one of the operations exposed in the list. If the operation you are looking for is not in the list, try pressing the `Refresh Operations`, or check with the operation owner to ensure that you have write access to that operation.
## Managing Evidence
Previous evidence can be reviewed by navigating to `View Accumulated Evidence`, which will present a screen showing evidence for the current operation. Selecting a row in the evidence list will show:
- A preview of the evidence (Images can be scaled by changing the window size, or my shrinking the description box -- mouse over the divider separating the description from the image)
- The description of the evidence
- Any (active) tags associated with the evidence.
From here you can submit the evidence, if not already submitted. Or, you may delete the file (even if previously submitted -- doing so will remove the file locally, but keep the website copy)
### Filtering Evidence
Filtering can be done by specifying items in `key:value` format. Multiple filters can be added by adding a space between each filter. Keys and values are case insensitive.
| Action | Key | Values | Alias(es) | Notes |
| ----------------------------------------- | ----------- | -------------------------------------------------- | ------------------------- | ------------------------------------------------------------------ |
| Show submit errors | `err` | `t`/`f`, or `y`/`n` | `error`, `fail`, `failed` | Also works with `true`/`false` `yes`/`no` |
| Show evidence for operations | `op` | operation slug | `operation` | Pre-populated with current operation, when reset button is pressed |
| Show evidence taken _before_ a given date | `before` | `today`, `yesterday` or date in yyyy-MM-dd format, | `to`, `til`, `until` | Starts at midnight of the given day |
| Show evidence taken _after_ a given date | `after` | `today`, `yesterday` or date in yyyy-MM-dd format, | `from` | Start just before midnight of the _next_ given day |
| Show evidence taken _on_ a given date | `on` | `today`, `yesterday` or date in yyyy-MM-dd format, | -- | |
| Show evidence that has not been submitted | `submitted` | `t`/`f`, or `y`/`n` | -- | Also works with `true`/`false`, `yes`/`no` |
#### Date filtering
When trying to apply both a "before" date and "after" date filter, the system will adjust the times so that the "before" date is always _after_ the "after" date. Meaning, the timespan must be inclusive. For example, a range of "before March" and "after May" (excluding March and April) is not valid, and will be revised to "After March, Before May"
When applying only one date, the range is unbounded on the other end. That is, dates are implicily "from the start of time" to "until the end of time"
## Local Files
You should never need to access these files outside of the application, however, for clarity, the following files are generate and maintained by this application:
| File type | Path | Notes |
| -------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Screenshots | `$eviRepo/$operationSlug/ashirt_screenshot_$randomCharacters.png` | Presently, random (english) characters tacked on to the end of a screenshot, to add uniqueness and prevent overwriting |
| Codeblocks | `$eviRepo/$operationSlug/ashirt_codeblock_$randomCharacters.json` | Presently, random (english) characters tacked on to the end of the codeblock filename, to add uniqueness and prevent overwriting |
| Configuration | `$userSettingsDirectory/ashirt/screenshot.json` | Manages connection info / configuration in "settings" menu |
| Local Database | `$userSettingsDirectory/ashirt/screenshot.sqlite` | |
| Settings | `$userSettingsDirectory/Verizon Media Group/AShirt Screenshot.conf` | Manages state info -- e.g. last used operation ; Managed by Qt |
### Variable locations
The above paths reference some variables. Some of these values change depending on what operating system is being used (or in how it is configured). The exact paths are unknown, but this may help you find these files:
| Path Variable | Notes |
| ------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `$userSettingsDirectory` | Where user-specific configuration/settings files are stored. |
| [For Linux] | On the command line, run `echo $XDG_CONFIG_HOME` |
| [For Mac OSX] | Check `/Users/(username)/Library/Preferences` |
| [For windows] | This may be in the System Registry |
| `$eviRepo` | The Evidence Repository value in the "settings" window |
| `$operationSlug` | The operation slug for a given operation. This is a unique representation of an operation name |
| `$randomCharacters` | Six random english characters, case-insensitive (for those operating systems that support this). e.g. `fTaNpS` |
## Developer Notes
Interested in contributing? See the [developer notes](Readme_Developer.md) for style guide, organization, etc
## Configuration
All configuration options are managed through the application UI.
## Contribute
Please refer to [the contributing.md file](Contributing.md) for information about how to get involved. We welcome issues, questions, and pull requests.
## License
GPL-3.0-or-later
## Credits / Contributors / Thanks
- Joel Smith
- Alex David
## Maintainers
- Joe Rozner joe.rozer@verizonmedia.com

60
Readme_Developer.md Normal file
View File

@ -0,0 +1,60 @@
# Developer Notes
## Build Requirements
This application is built off of Qt 5, and utilizes's Qt's networking and Sql featuresets. To build, your specific system may need the following:
1. Qt 5, `qmake`, and possibly Qt Creator IDE.
1. Binaries located [here](https://www.qt.io/download-qt-installer?hsCtaTracking=99d9dd4f-5681-48d2-b096-470725510d34%7C074ddad0-fdef-4e53-8aa8-5e8a876d6ab4). You may need to alter which downloader you install.
2. SQLite C driver (for SQLite version 3)
1. On Fedora, this can be installed with `yum install sqlite-devel`
2. On Arch systems, this can be installed with `pacman -S sqlite-doc`
## Adding a db migration
Updating the database schema is slightly complicated, and the following rules must be followed:
1. Use the helper script `bin/create-migration.sh`
* This will create file in the `migrations` folder with the indicated name and a timestamp
* This should also add this migration to the qrc file. However, if this is not done, you can do this manually by editing the `rs_migrations.qrc` file.
2. Inside the new migration file, add the necessary sql to apply the db change under `-- +migrate Up`
3. Inside the new migration file, add the necessary sql to _undo_ the db change under `-- +migrate Down`
4. Only one statement is allowed under each heading. If multiple statements need to be applied, they should done as multiple migration files
* This is a sqlite3/Qt limitation.
## Adding a new Evidence Filter
Evidence filters require modification in a few files and functions. Here is a rough checklist:
| | File | Function | Notes |
| ------------------------ | ---------------------- | ---------------------- | ------------------------------------------------------------------- |
| <input type="checkbox"/> | evidencefilter.h | | Need to add `FILTER_KEY_` and `FILTER_KEYS_` values |
| <input type="checkbox"/> | evidencefilter.cpp | standardizeFilterKey | Needed to map filter key alias to the one true filter key |
| <input type="checkbox"/> | evidencefilter.cpp | toString | Need to represent a filter key/value as a string |
| <input type="checkbox"/> | evidencefilter.cpp | parseFilter | Need to be able to read filter key/value from a string |
| <input type="checkbox"/> | databaseconnection.cpp | getEvidenceWithFilters | Need to translate the filter key/value to an appropriate sql clause |
Currently, there is already built-in support for adding filters of type:
* String (use the Operation filter as a guide)
* Boolean/Tri (Tris represent Yes/No/Any here, use Error filter as a guide)
* Date Range (use To/From filters as a guide)
## Formatting
This application adopts a modified [Google code style](https://google.github.io/styleguide/cppguide.html), applied via `clang-format`. Note that while formatting style is adhered to, other parts may not be followed, due to not starting with this style in mind.
Google style changes:
* Line limit to 100 characters
* This is simply easier to read when dealing with long lines
* Line breaks before `else` and `catch`.
* This is mostly because I find these easier to read.
To apply code formatting (Linux/Bash), run `find src/ -iname "*.cpp" -o -iname "*.h" | xargs clang-format -i`
## Known Issues
1. Evidence manager columns aren't ideally sized
2. No CLI for non-qt/non-tray OSes
3. Remove dock icon on mac?
1. Possibly useful: https://github.com/keepassxreboot/keepassxc/commit/45344bb2acea61806d5e7f5f9eadfa779ca536ae#diff-a9e708931297992b08350ff7122fcb91R157

115
ascreen.pro Normal file
View File

@ -0,0 +1,115 @@
QT += core gui network sql
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
INCLUDEPATH += src
SOURCES += \
src/components/aspectratio_pixmap_label/aspectratiopixmaplabel.cpp \
src/components/aspectratio_pixmap_label/imageview.cpp \
src/components/code_editor/codeblockview.cpp \
src/components/code_editor/codeeditor.cpp \
src/components/error_view/errorview.cpp \
src/components/evidence_editor/evidenceeditor.cpp \
src/components/evidencepreview.cpp \
src/components/loading/qprogressindicator.cpp \
src/components/loading_button/loadingbutton.cpp \
src/components/tag_editor/tageditor.cpp \
src/db/databaseconnection.cpp \
src/forms/evidence_filter/evidencefilter.cpp \
src/forms/evidence_filter/evidencefilterform.cpp \
src/forms/getinfo/getinfo.cpp \
src/helpers/clipboard/clipboardhelper.cpp \
src/models/codeblock.cpp \
src/helpers/multipartparser.cpp \
src/hotkeymanager.cpp \
src/main.cpp \
src/traymanager.cpp \
src/helpers/screenshot.cpp \
src/helpers/stopreply.cpp \
src/forms/buttonboxform.cpp \
src/forms/credits/credits.cpp \
src/forms/evidence/evidencemanager.cpp \
src/forms/settings/settings.cpp
HEADERS += \
src/components/aspectratio_pixmap_label/aspectratiopixmaplabel.h \
src/components/aspectratio_pixmap_label/imageview.h \
src/components/code_editor/codeblockview.h \
src/components/code_editor/codeeditor.h \
src/components/error_view/errorview.h \
src/components/evidence_editor/deleteevidenceresponse.h \
src/components/evidence_editor/evidenceeditor.h \
src/components/evidence_editor/saveevidenceresponse.h \
src/components/evidencepreview.h \
src/components/loading/qprogressindicator.h \
src/components/loading_button/loadingbutton.h \
src/components/tag_editor/tageditor.h \
src/db/databaseconnection.h \
src/exceptions/databaseerr.h \
src/exceptions/fileerror.h \
src/forms/evidence_filter/evidencefilter.h \
src/forms/evidence_filter/evidencefilterform.h \
src/forms/getinfo/getinfo.h \
src/helpers/clipboard/clipboardhelper.h \
src/helpers/ui_helpers.h \
src/models/codeblock.h \
src/helpers/file_helpers.h \
src/helpers/http_status.h \
src/hotkeymanager.h \
src/models/evidence.h \
src/models/tag.h \
src/traymanager.h \
src/appconfig.h \
src/appsettings.h \
src/helpers/jsonhelpers.h \
src/helpers/multipartparser.h \
src/helpers/netman.h \
src/helpers/pathseparator.h \
src/helpers/screenshot.h \
src/helpers/stopreply.h \
src/dtos/tag.h \
src/dtos/operation.h \
src/forms/buttonboxform.h \
src/forms/credits/credits.h \
src/forms/evidence/evidencemanager.h \
src/forms/settings/settings.h
FORMS += \
src/forms/credits/credits.ui \
src/forms/evidence/evidencemanager.ui \
src/forms/evidence_filter/evidencefilterform.ui \
src/forms/getinfo/getinfo.ui \
src/forms/settings/settings.ui
include(tools/UGlobalHotkey/uglobalhotkey.pri)
macx {
ICON = icons/ascreen.icns
}
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
DISTFILES += \
bin/update_migration_resource.py \
sadbear.png
RESOURCES += \
res_migrations.qrc \
res_icons.qrc

33
bin/create-migration.sh Executable file
View File

@ -0,0 +1,33 @@
#! /usr/bin/env bash
# exit on error
set -e
# set cwd to project root
cd "$(dirname "$0")/.."
now=$(date -u +%Y%m%d%H%M%S)
desc="$*"
if [ -z "$desc" ]; then
read -p 'Migration name: ' desc
fi
# sanitize description (spaces -> dashes)
desc=${desc// /-}
migrationsPath="./migrations"
filename="$now-$desc.sql"
filepath="$migrationsPath/$filename"
touch $filepath
echo "-- +migrate Up" >> $filepath
echo "" >> $filepath
echo "-- +migrate Down" >> $filepath
resourceFile="$(pwd)/res_migrations.qrc"
./bin/update_migration_resource.py "$resourceFile" "migrations/$filename"

View File

@ -0,0 +1,31 @@
#! /usr/bin/env python
import xml.etree.ElementTree as ET
import sys
def main():
if len(sys.argv) < 3:
print("Project root not provided.")
return
migration_file = sys.argv[1]
new_filename = sys.argv[2]
tree = ET.parse(migration_file)
root = tree.getroot()
for child in root:
if child.attrib.get('prefix') == '/':
newFileEntry = ET.SubElement(child, "file")
newFileEntry.text = new_filename
# try to keep pretty -- not really necessary
if len(child) > 1:
child[-2].tail = "\n "
newFileEntry.tail ="\n "
break
tree.write(migration_file)
if __name__ == "__main__":
main()

BIN
icons/ascreen.icns Normal file

Binary file not shown.

1
icons/shirt-dark.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#1A1A1A" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M74.506,16.064H61.021c-1.042,4.856-5.355,8.499-10.523,8.499c-5.166,0-9.479-3.642-10.52-8.499H26.495L1.908,36.545 l15.637,16.758l8.95-7.229v44.159h48.011V46.074l8.949,7.229l15.637-16.758L74.506,16.064z"></path></svg>

After

Width:  |  Height:  |  Size: 462 B

1
icons/shirt-light.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#FFFFFF" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M74.506,16.064H61.021c-1.042,4.856-5.355,8.499-10.523,8.499c-5.166,0-9.479-3.642-10.52-8.499H26.495L1.908,36.545 l15.637,16.758l8.95-7.229v44.159h48.011V46.074l8.949,7.229l15.637-16.758L74.506,16.064z"></path></svg>

After

Width:  |  Height:  |  Size: 462 B

1
icons/shirt-red.svg Normal file
View File

@ -0,0 +1 @@
<svg height='100px' width='100px' fill="#FF0000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path d="M74.506,16.064H61.021c-1.042,4.856-5.355,8.499-10.523,8.499c-5.166,0-9.479-3.642-10.52-8.499H26.495L1.908,36.545 l15.637,16.758l8.95-7.229v44.159h48.011V46.074l8.949,7.229l15.637-16.758L74.506,16.064z"></path></svg>

After

Width:  |  Height:  |  Size: 462 B

1
libs/libUGlobalHotkey.so Symbolic link
View File

@ -0,0 +1 @@
libUGlobalHotkey.so.1.0.0

View File

@ -0,0 +1,8 @@
-- +migrate Up
CREATE TABLE migrations (
migration_name TEXT NOT NULL,
applied_at TIMESTAMP
);
-- +migrate Down
DROP TABLE migrations;

View File

@ -0,0 +1,13 @@
-- +migrate Up
CREATE TABLE screenshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
operation_slug TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
error TEXT NOT NULL DEFAULT '',
recorded_date TIMESTAMP NOT NULL,
upload_date TIMESTAMP
);
-- +migrate Down
DROP TABLE screenshots;

View File

@ -0,0 +1,10 @@
-- +migrate Up
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
screenshot_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
name TEXT NOT NULL
);
-- +migrate Down
DROP TABLE tags;

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE screenshots RENAME TO evidence;
-- +migrate Down
ALTER TABLE evidence RENAME TO screenshots;

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE evidence ADD COLUMN content_type TEXT NOT NULL DEFAULT 'image';
-- +migrate Down
-- cannot do a proper migrate down (SQLite does not support ALTER TABLE DROP COLUMN)

View File

@ -0,0 +1,3 @@
-- +migrate Up
UPDATE evidence SET content_type='image';
-- +migrate Down

View File

@ -0,0 +1,5 @@
-- +migrate Up
ALTER TABLE tags RENAME COLUMN screenshot_id TO evidence_id;
-- +migrate Down
ALTER TABLE tags RENAME COLUMN evidence_id TO screenshot_id;

7
res_icons.qrc Normal file
View File

@ -0,0 +1,7 @@
<RCC>
<qresource prefix="/">
<file>icons/shirt-dark.svg</file>
<file>icons/shirt-light.svg</file>
<file>icons/shirt-red.svg</file>
</qresource>
</RCC>

11
res_migrations.qrc Normal file
View File

@ -0,0 +1,11 @@
<RCC>
<qresource prefix="/">
<file>migrations/20200521190124-initial.sql</file>
<file>migrations/20200521210407-add-screenshots-table.sql</file>
<file>migrations/20200521210435-add-tags-table.sql</file>
<file>migrations/20200625191727-support-codeblocks-p1.sql</file>
<file>migrations/20200625192018-support-codeblocks-p2.sql</file>
<file>migrations/20200625192444-support-codeblocks-p3.sql</file>
<file>migrations/20200625203249-support-codeblocks-p4.sql</file>
</qresource>
</RCC>

140
src/appconfig.h Normal file
View File

@ -0,0 +1,140 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef DATA_H
#define DATA_H
#include <QDir>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardPaths>
#include <QStringLiteral>
#include <cstdlib>
#include <stdexcept>
#include "exceptions/fileerror.h"
// AppConfig is a singleton construct for accessing the application's configuration.
// singleton design borrowed from:
// https://stackoverflow.com/questions/1008019/c-singleton-design-pattern
class AppConfig {
public:
static AppConfig &getInstance() {
static AppConfig instance;
return instance;
}
AppConfig(AppConfig const &) = delete;
void operator=(AppConfig const &) = delete;
QString evidenceRepo = "";
QString accessKey = "";
QString secretKey = "";
QString apiURL = "";
QString screenshotExec = "";
QString screenshotShortcutCombo = "";
QString captureWindowExec = "";
QString captureWindowShortcut = "";
QString errorText = "";
private:
AppConfig() noexcept {
try {
readConfig();
}
catch (std::exception &e) {
errorText = e.what();
}
}
QString saveLocation =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/ashirt/screenshot.json";
void readConfig() {
QFile configFile(saveLocation);
if (!configFile.open(QIODevice::ReadOnly)) {
if (configFile.exists()) {
throw FileError::mkError("Error reading config file", saveLocation.toStdString(),
configFile.error());
}
try {
writeDefaultConfig();
}
catch (...) {
// ignoring -- just trying to generate an empty config
}
return;
}
QByteArray data = configFile.readAll();
if (configFile.error() != QFile::NoError) {
throw FileError::mkError("Error reading config file", saveLocation.toStdString(),
configFile.error());
}
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
// ignoring specific type -- unlikely to occur in practice.
throw std::runtime_error("Unable to parse config file");
}
this->evidenceRepo = doc["evidenceRepo"].toString();
this->accessKey = doc["accessKey"].toString();
this->secretKey = doc["secretKey"].toString();
this->apiURL = doc["apiURL"].toString();
this->screenshotExec = doc["screenshotCommand"].toString();
this->screenshotShortcutCombo = doc["screenshotShortcut"].toString();
this->captureWindowExec = doc["captureWindowExec"].toString();
this->captureWindowShortcut = doc["captureWindowShortcut"].toString();
}
void writeDefaultConfig() {
evidenceRepo = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/ashirt";
#ifdef Q_OS_MACOS
screenshotExec = "screencapture -s %file";
captureWindowExec = "screencapture -w %file";
#endif
try {
writeConfig();
}
catch (...) {
// ignoring error -- best effort approach
}
}
public:
void writeConfig() {
QJsonObject root = QJsonObject(); // QFiles close automatically, so no need for close here.
root["evidenceRepo"] = evidenceRepo;
root["accessKey"] = accessKey;
root["secretKey"] = secretKey;
root["apiURL"] = apiURL;
root["screenshotCommand"] = screenshotExec;
root["screenshotShortcut"] = screenshotShortcutCombo;
root["captureWindowExec"] = captureWindowExec;
root["captureWindowShortcut"] = captureWindowShortcut;
auto saveRoot = saveLocation.left(saveLocation.lastIndexOf("/"));
QDir().mkpath(saveRoot);
QFile configFile(saveLocation);
if (!configFile.open(QIODevice::WriteOnly)) {
throw FileError::mkError("Error writing config file", saveLocation.toStdString(),
configFile.error());
}
QJsonDocument doc(root);
auto written = configFile.write(doc.toJson());
if (written == -1) {
throw FileError::mkError("Error writing config file", saveLocation.toStdString(),
configFile.error());
}
return;
}
};
#endif // DATA_H

67
src/appsettings.h Normal file
View File

@ -0,0 +1,67 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef APPSETTINGS_H
#define APPSETTINGS_H
#include <QSettings>
#include <QString>
// AppSettings is a singleton construct for accessing the application's settings. This is different
// from configuration, as it represents the application's state, rather than how the application
// communicates.
//
// singleton design borrowed from:
// https://stackoverflow.com/questions/1008019/c-singleton-design-pattern
class AppSettings : public QObject {
Q_OBJECT;
public:
static AppSettings &getInstance() {
static AppSettings instance;
return instance;
}
AppSettings(AppSettings const &) = delete;
void operator=(AppSettings const &) = delete;
private:
QSettings settings;
const char *opSlugSetting = "operation/slug";
const char *opNameSetting = "operation/name";
const char *opPausedSetting = "operation/paused";
AppSettings() : QObject(nullptr) {}
public:
signals:
void onOperationUpdated(QString operationSlug, QString operationName);
void onOperationStateChanged(bool isPaused);
void onSettingsSynced();
public:
void sync() {
settings.sync(); // ignoring the error
emit this->onSettingsSynced();
}
void setOperationDetails(QString operationSlug, QString operationName) {
settings.setValue(opSlugSetting, operationSlug);
settings.setValue(opNameSetting, operationName);
emit onOperationUpdated(operationSlug, operationName);
}
QString operationSlug() { return settings.value(opSlugSetting).toString(); }
QString operationName() { return settings.value(opNameSetting).toString(); }
void setOperationPaused(bool paused) {
settings.setValue(opPausedSetting, paused);
emit onOperationStateChanged(paused);
}
bool toggleOperationPaused() {
setOperationPaused(!isOperationPaused());
return isOperationPaused();
}
bool isOperationPaused() { return settings.value(opPausedSetting).toBool(); }
};
#endif // APPSETTINGS_H

View File

@ -0,0 +1,33 @@
// Copyright 22014-020, Phyatt, et al
// Licensed under the terms of CC BY-SA 3.0.
// Original Source: https://stackoverflow.com/a/22618496/4262552
#include "aspectratiopixmaplabel.h"
AspectRatioPixmapLabel::AspectRatioPixmapLabel(QWidget *parent) : QLabel(parent) {
setMinimumSize(1, 1);
setScaledContents(false);
}
void AspectRatioPixmapLabel::setPixmap(const QPixmap &p) {
pix = p;
QLabel::setPixmap(scaledPixmap());
}
int AspectRatioPixmapLabel::heightForWidth(int width) const {
return pix.isNull() ? this->height() : ((qreal)pix.height() * width) / pix.width();
}
QSize AspectRatioPixmapLabel::sizeHint() const {
int w = this->width();
return QSize(w, heightForWidth(w));
}
QPixmap AspectRatioPixmapLabel::scaledPixmap() const {
return pix.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
void AspectRatioPixmapLabel::resizeEvent(QResizeEvent *e) {
Q_UNUSED(e);
if (!pix.isNull()) QLabel::setPixmap(scaledPixmap());
}

View File

@ -0,0 +1,27 @@
// Copyright 22014-020, Phyatt, et al
// Licensed under the terms of CC BY-SA 3.0.
// Original Source: https://stackoverflow.com/a/22618496/4262552
#ifndef ASPECTRATIOPIXMAPLABEL_H
#define ASPECTRATIOPIXMAPLABEL_H
#include <QLabel>
#include <QPixmap>
#include <QResizeEvent>
class AspectRatioPixmapLabel : public QLabel {
Q_OBJECT
public:
explicit AspectRatioPixmapLabel(QWidget *parent = 0);
virtual int heightForWidth(int width) const;
virtual QSize sizeHint() const;
QPixmap scaledPixmap() const;
public slots:
void setPixmap(const QPixmap &);
void resizeEvent(QResizeEvent *);
private:
QPixmap pix;
};
#endif // ASPECTRATIOPIXMAPLABEL_H

View File

@ -0,0 +1,52 @@
#include "imageview.h"
#include <QImageReader>
#include <QPixmap>
ImageView::ImageView(QWidget* parent) : EvidencePreview(parent) {
buildUi();
wireUi();
}
ImageView::~ImageView() {
delete previewImage;
delete gridLayout;
}
void ImageView::buildUi() {
gridLayout = new QGridLayout(this);
gridLayout->setMargin(0);
previewImage = new AspectRatioPixmapLabel(this);
previewImage->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
previewImage->setAlignment(Qt::AlignCenter);
// Layout
/* 0
+------------+
0 | |
| Img Label |
| |
+------------+
*/
// row 0
gridLayout->addWidget(previewImage, 0, 0);
}
void ImageView::wireUi() {}
// ---- Parent Overrides
void ImageView::clearPreview() { previewImage->clear(); }
void ImageView::loadFromFile(QString filepath) {
QImageReader reader(filepath);
const QImage img = reader.read();
if (img.isNull()) {
previewImage->setText("Unable to load preview: " + reader.errorString());
}
else {
previewImage->setPixmap(QPixmap::fromImage(img));
}
}

View File

@ -0,0 +1,40 @@
#ifndef IMAGEVIEW_H
#define IMAGEVIEW_H
#include <QGridLayout>
#include <QWidget>
#include "aspectratiopixmaplabel.h"
#include "components/evidencepreview.h"
/**
* @brief The ImageView class is a thinly wrapped AspectRatioPixmapLabel to meet the EvidencePreview
* interface requirements.
*/
class ImageView : public EvidencePreview {
Q_OBJECT
public:
explicit ImageView(QWidget* parent = nullptr);
~ImageView();
private:
/// buildUi constructs the UI, without wiring any connections
void buildUi();
/// wireUi connects UI elements together (currently a no-op)
void wireUi();
public:
/// loadFromFile attempts to load the indicated image from disk.
/// If this process fails, renders a text message instead. Inherited from EvidencePreview
virtual void loadFromFile(QString filepath) override;
/// clearPreview clears the rendered image. Inherited from EvidencePreview.
virtual void clearPreview() override;
private:
QGridLayout* gridLayout;
AspectRatioPixmapLabel* previewImage;
};
#endif // IMAGEVIEW_H

View File

@ -0,0 +1,99 @@
#include "codeblockview.h"
#include "exceptions/fileerror.h"
#include "helpers/ui_helpers.h"
CodeBlockView::CodeBlockView(QWidget* parent) : EvidencePreview(parent) {
buildUi();
wireUi();
}
CodeBlockView::~CodeBlockView() {
delete _languageLabel;
delete _sourceLabel;
delete codeEditor;
delete sourceTextBox;
delete languageComboBox;
delete gridLayout;
}
void CodeBlockView::buildUi() {
gridLayout = new QGridLayout(this);
gridLayout->setMargin(0);
_languageLabel = new QLabel("Language", this);
_sourceLabel = new QLabel("Source", this);
sourceTextBox = new QLineEdit(this);
languageComboBox = new QComboBox(this);
codeEditor = new CodeEditor(this);
for (const std::pair<QString, QString>& lang : SUPPORTED_LANGUAGES) {
languageComboBox->addItem(lang.first, lang.second);
}
// Layout
/* 0 1 2 3
+------------+-------------+----------+----------------+
0 | Lang Lab | [ textbox ] | Src Lab | [src textbox] |
+------------+-------------+----------+----------------+
1 | |
| Code Block |
| |
+------------+-------------+----------+----------------+
*/
// row 0
gridLayout->addWidget(_languageLabel, 0, 0);
gridLayout->addWidget(languageComboBox, 0, 1);
gridLayout->addWidget(_sourceLabel, 0, 2);
gridLayout->addWidget(sourceTextBox, 0, 3);
// row 1
gridLayout->addWidget(codeEditor, 1, 0, 1, gridLayout->columnCount());
}
void CodeBlockView::wireUi() {}
// ------- Parent Overrides
void CodeBlockView::loadFromFile(QString filepath) {
try {
loadedCodeblock = Codeblock::readCodeblock(filepath);
codeEditor->setPlainText(loadedCodeblock.content);
sourceTextBox->setText(loadedCodeblock.source);
UiHelpers::setComboBoxValue(languageComboBox, loadedCodeblock.subtype);
}
catch (std::exception& e) {
std::string msg = "Unable to load codeblock. Error: ";
msg += e.what();
codeEditor->setPlainText(QString(msg.c_str()));
setReadonly(true);
}
}
void CodeBlockView::saveEvidence() {
loadedCodeblock.source = sourceTextBox->text();
loadedCodeblock.subtype = languageComboBox->currentData().toString();
if (loadedCodeblock.filePath() != "") {
try {
Codeblock::saveCodeblock(loadedCodeblock);
}
catch (FileError& e) {
// best effort here -- if we can't save, oh well, they can edit it on the server
}
}
}
void CodeBlockView::clearPreview() {
codeEditor->setPlainText("");
sourceTextBox->setText("");
languageComboBox->setCurrentIndex(0); // should be Plain Text
}
void CodeBlockView::setReadonly(bool readonly) {
EvidencePreview::setReadonly(readonly);
codeEditor->setReadOnly(readonly);
sourceTextBox->setReadOnly(readonly);
languageComboBox->setEnabled(!readonly);
}

View File

@ -0,0 +1,118 @@
#ifndef CODEBLOCKVIEW_H
#define CODEBLOCKVIEW_H
#include <QComboBox>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QString>
#include <QWidget>
#include <vector>
#include "codeeditor.h"
#include "components/evidencepreview.h"
#include "models/codeblock.h"
// matches supported languages on the front end
static std::vector<std::pair<QString, QString>> SUPPORTED_LANGUAGES = {
std::pair<QString, QString>("Plain Text", ""),
std::pair<QString, QString>("ABAP", "abap"),
std::pair<QString, QString>("ActionScript", "actionscript"),
std::pair<QString, QString>("Ada", "ada"),
std::pair<QString, QString>("C / C++", "c_cpp"),
std::pair<QString, QString>("C#", "csharp"),
std::pair<QString, QString>("COBOL", "cobol"),
std::pair<QString, QString>("D", "d"),
std::pair<QString, QString>("Dart", "dart"),
std::pair<QString, QString>("Delphi/Object Pascal", "pascal"),
std::pair<QString, QString>("Dockerfile", "dockerfile"),
std::pair<QString, QString>("Elixir", "elixir"),
std::pair<QString, QString>("Elm", "elm"),
std::pair<QString, QString>("Erlang", "erlang"),
std::pair<QString, QString>("F#", "fsharp"),
std::pair<QString, QString>("Fortran", "fortran"),
std::pair<QString, QString>("Go", "golang"),
std::pair<QString, QString>("Groovy", "groovy"),
std::pair<QString, QString>("Haskell", "haskell"),
std::pair<QString, QString>("Java", "java"),
std::pair<QString, QString>("JavaScript", "javascript"),
std::pair<QString, QString>("Julia", "julia"),
std::pair<QString, QString>("Kotlin", "kotlin"),
std::pair<QString, QString>("Lisp", "lisp"),
std::pair<QString, QString>("Lua", "lua"),
std::pair<QString, QString>("MATLAB", "matlab"),
std::pair<QString, QString>("Markdown", "markdown"),
std::pair<QString, QString>("Objective-C", "objectivec"),
std::pair<QString, QString>("PHP", "php"),
std::pair<QString, QString>("Perl", "perl"),
std::pair<QString, QString>("Prolog", "prolog"),
std::pair<QString, QString>("Properties", "properties"),
std::pair<QString, QString>("Python", "python"),
std::pair<QString, QString>("R", "r"),
std::pair<QString, QString>("Ruby", "ruby"),
std::pair<QString, QString>("Rust", "rust"),
std::pair<QString, QString>("SQL", "sql"),
std::pair<QString, QString>("Sass", "sass"),
std::pair<QString, QString>("Scala", "scala"),
std::pair<QString, QString>("Scheme", "scheme"),
std::pair<QString, QString>("Shell/Bash", "sh"),
std::pair<QString, QString>("Swift", "swift"),
std::pair<QString, QString>("Tcl", "tcl"),
std::pair<QString, QString>("Terraform", "terraform"),
std::pair<QString, QString>("Toml", "toml"),
std::pair<QString, QString>("TypeScript", "typescript"),
std::pair<QString, QString>("VBScript", "vbscript"),
std::pair<QString, QString>("XML", "xml"),
};
/**
* @brief The CodeBlockView class provides a wrapped code editor, along with editable
* areas for source and language. Note that even though this is a "view" it's fully editable.
* Set to readonly if you need a proper view.
*/
class CodeBlockView : public EvidencePreview {
Q_OBJECT
public:
explicit CodeBlockView(QWidget* parent = nullptr);
~CodeBlockView();
private:
/// buildUi constructs the UI, without wiring any connections
void buildUi();
/// wireUi connects UI elements together (currently a no-op)
void wireUi();
public:
/// loadFromFile attempts to load the indicated codeblock from disk.
/// If this process fails, renders a message instead of the codeblock. Inherited from
/// EvidencePreview
virtual void loadFromFile(QString filepath) override;
/// saveEvidence attempts to write the codeblock back to disk, where it was loaded from.
/// Inherited from EvidencePreview
/// Note: Will silently fail if an error occurs while saving. Implemented as best effort approach.
virtual void saveEvidence() override;
/// clearPreview removes the content, source, and sets the language to "Plain Text". Inherited
/// from EvidencePreview
virtual void clearPreview() override;
/// Sets the editable areas to readonly/writable (depending on value passed).
/// Inherited from EvidencePreview
virtual void setReadonly(bool readonly) override;
private:
Codeblock loadedCodeblock;
// UI components
QGridLayout* gridLayout;
QLabel* _languageLabel;
QLabel* _sourceLabel;
CodeEditor* codeEditor;
QLineEdit* sourceTextBox;
QComboBox* languageComboBox;
};
#endif // CODEBLOCKVIEW_H

View File

@ -0,0 +1,154 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
// Forked edits:
// Minimum column width extended to 2 characters
// Set tab changes focus to false initially
// set line wrap to no-wrap
#include "codeeditor.h"
#include <QPainter>
#include <QTextBlock>
QColor currentLineHighlightColor = QColor(115, 191, 255);
CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent) {
lineNumberArea = new LineNumberArea(this);
connect(this, &CodeEditor::blockCountChanged, this, &CodeEditor::updateLineNumberAreaWidth);
connect(this, &CodeEditor::updateRequest, this, &CodeEditor::updateLineNumberArea);
connect(this, &CodeEditor::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine);
// customize for text/code editing
setLineWrapMode(LineWrapMode::NoWrap);
setTabChangesFocus(false);
QFont font("source code pro");
font.setStyleHint(QFont::TypeWriter);
setFont(font);
updateLineNumberAreaWidth(0);
highlightCurrentLine();
}
void CodeEditor::keyReleaseEvent(QKeyEvent *e) { QPlainTextEdit::keyReleaseEvent(e); }
int CodeEditor::lineNumberAreaWidth() {
int digits = 1;
int max = qMax(1, blockCount());
while (max >= 10) {
max /= 10;
++digits;
}
int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * qMax(digits, 2);
return space;
}
void CodeEditor::updateLineNumberAreaWidth(int /* newBlockCount */) {
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
void CodeEditor::updateLineNumberArea(const QRect &rect, int dy) {
if (dy)
lineNumberArea->scroll(0, dy);
else
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
if (rect.contains(viewport()->rect())) updateLineNumberAreaWidth(0);
}
void CodeEditor::resizeEvent(QResizeEvent *e) {
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
void CodeEditor::highlightCurrentLine() {
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
selection.format.setBackground(currentLineHighlightColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) {
QPainter painter(lineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + qRound(blockBoundingRect(block).height());
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
QString number = QString::number(blockNumber + 1);
painter.setPen(Qt::black);
painter.drawText(0, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight,
number);
}
block = block.next();
top = bottom;
bottom = top + qRound(blockBoundingRect(block).height());
++blockNumber;
}
}

View File

@ -0,0 +1,100 @@
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef CODEEDITOR_H
#define CODEEDITOR_H
#include <QPlainTextEdit>
QT_BEGIN_NAMESPACE
class QPaintEvent;
class QResizeEvent;
class QSize;
class QWidget;
QT_END_NAMESPACE
class LineNumberArea;
class CodeEditor : public QPlainTextEdit {
Q_OBJECT
public:
CodeEditor(QWidget *parent = nullptr);
void lineNumberAreaPaintEvent(QPaintEvent *event);
int lineNumberAreaWidth();
protected:
void resizeEvent(QResizeEvent *event) override;
void keyReleaseEvent(QKeyEvent *e) override;
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
private:
QWidget *lineNumberArea;
};
class LineNumberArea : public QWidget {
public:
LineNumberArea(CodeEditor *editor) : QWidget(editor), codeEditor(editor) {}
QSize sizeHint() const override { return QSize(codeEditor->lineNumberAreaWidth(), 0); }
protected:
void paintEvent(QPaintEvent *event) override { codeEditor->lineNumberAreaPaintEvent(event); }
private:
CodeEditor *codeEditor;
};
#endif

View File

@ -0,0 +1,39 @@
#include "errorview.h"
#include <utility>
ErrorView::ErrorView(QString errorText, QWidget* parent) : EvidencePreview(parent) {
this->errorText = std::move(errorText);
buildUi();
wireUi();
}
ErrorView::~ErrorView() {
delete errorLabel;
delete gridLayout;
}
void ErrorView::buildUi() {
gridLayout = new QGridLayout(this);
gridLayout->setMargin(0);
errorLabel = new QLabel(errorText, this);
// Layout
/* 0
+------------+
0 | |
| Err Label |
| |
+------------+
*/
// row 0
gridLayout->addWidget(errorLabel, 0, 0);
}
void ErrorView::wireUi() {}
// ---- Parent Overrides
void ErrorView::clearPreview() {}
void ErrorView::loadFromFile(QString filepath) { Q_UNUSED(filepath); }

View File

@ -0,0 +1,43 @@
#ifndef ERRORVIEW_H
#define ERRORVIEW_H
#include <QGridLayout>
#include <QLabel>
#include <QString>
#include <QWidget>
#include "components/evidencepreview.h"
/**
* @brief The ErrorView class provides a default/error handler for situations when an an
* EvidencePreview is needed, but needs to be displayed with no chance to throw an error. Simply
* renders the provided error text in a label.
*/
class ErrorView : public EvidencePreview {
Q_OBJECT
public:
explicit ErrorView(QString errorText, QWidget* parent = nullptr);
~ErrorView();
private:
/// buildUi constructs the UI, without wiring any connections
void buildUi();
/// wireUi connects UI elements together (currently a no-op)
void wireUi();
public:
/// loadFromFile is a no-op. No files are loaded. Inherited from EvidencePreview
virtual void loadFromFile(QString filepath) override;
/// clearPreview is a no-op. The initial text is always displayed. Inherited from EvidencePreview.
virtual void clearPreview() override;
private:
QGridLayout* gridLayout;
QLabel* errorLabel;
QString errorText;
};
#endif // ERRORVIEW_H

View File

@ -0,0 +1,26 @@
#ifndef DELETEEVIDENCERESPONSE_H
#define DELETEEVIDENCERESPONSE_H
#include <QString>
#include "models/evidence.h"
struct DeleteEvidenceResponse {
DeleteEvidenceResponse(model::Evidence model) {
this->model = model;
this->errorText = "";
}
DeleteEvidenceResponse(bool fileDeleteSuccess, bool dbDeleteSuccess, QString err,
model::Evidence model)
: DeleteEvidenceResponse(model) {
this->fileDeleteSuccess = fileDeleteSuccess;
this->dbDeleteSuccess = dbDeleteSuccess;
this->errorText = err;
}
bool fileDeleteSuccess;
bool dbDeleteSuccess;
QString errorText;
model::Evidence model;
};
#endif // DELETEEVIDENCERESPONSE_H

View File

@ -0,0 +1,215 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "evidenceeditor.h"
#include <vector>
#include "components/aspectratio_pixmap_label/imageview.h"
#include "components/code_editor/codeblockview.h"
#include "components/error_view/errorview.h"
#include "components/evidence_editor/evidenceeditor.h"
#include "models/codeblock.h"
#include "models/evidence.h"
EvidenceEditor::EvidenceEditor(qint64 evidenceID, DatabaseConnection *db, QWidget *parent)
: EvidenceEditor(db, parent) {
this->evidenceID = evidenceID;
loadData();
}
EvidenceEditor::EvidenceEditor(DatabaseConnection *db, QWidget *parent) : QWidget(parent) {
buildUi();
this->db = db;
wireUi();
setEnabled(false);
}
EvidenceEditor::~EvidenceEditor() {
delete _descriptionLabel;
delete tagEditor;
delete descriptionTextBox;
delete loadedPreview;
delete splitter;
delete gridLayout;
}
void EvidenceEditor::buildUi() {
gridLayout = new QGridLayout(this);
gridLayout->setMargin(0);
splitter = new QSplitter(this);
splitter->setOrientation(Qt::Vertical);
_descriptionLabel = new QLabel("Description", this);
tagEditor = new TagEditor(this);
descriptionTextBox = new QTextEdit(this);
// Layout
/* 0
+----------------------------------------+
0 | Desc Label |
+----------------------------------------+
1 | +------------Vert. Splitter---------+ |
| | Desc Text box | |
| | | |
| >===================================< |
| | Preview Area | |
| | (reserved) | |
| +-----------------------------------+ |
+----------------------------------------+
2 | |
| tagEditor |
| |
+----------------------------------------+
*/
// row 0
gridLayout->addWidget(_descriptionLabel, 0, 0);
// row 1
gridLayout->addWidget(splitter, 1, 0);
splitter->addWidget(descriptionTextBox);
// row 2
gridLayout->addWidget(tagEditor, 2, 0);
this->setLayout(gridLayout);
}
model::Evidence EvidenceEditor::encodeEvidence() {
model::Evidence copy = model::Evidence(originalEvidenceData);
copy.description = descriptionTextBox->toPlainText();
copy.tags.clear();
copy.tags = tagEditor->getIncludedTags();
return copy;
}
void EvidenceEditor::setEnabled(bool enable) {
// if the product is enabled, then we can edit, hence it's not readonly
descriptionTextBox->setReadOnly(!enable);
tagEditor->setEnabled(enable);
if (loadedPreview != nullptr) {
loadedPreview->setReadonly(!enable);
}
}
void EvidenceEditor::wireUi() {
connect(tagEditor, &TagEditor::tagsLoaded, this, &EvidenceEditor::onTagsLoaded);
}
void EvidenceEditor::loadData() {
// get local db evidence data
clearEditor();
delete loadedPreview;
try {
originalEvidenceData = db->getEvidenceDetails(evidenceID);
descriptionTextBox->setText(originalEvidenceData.description);
operationSlug = originalEvidenceData.operationSlug;
if (originalEvidenceData.contentType == "image") {
loadedPreview = new ImageView(this);
}
else if (originalEvidenceData.contentType == "codeblock") {
loadedPreview = new CodeBlockView(this);
}
else {
loadedPreview =
new ErrorView("Unsupported evidence type: " + originalEvidenceData.contentType, this);
}
loadedPreview->loadFromFile(originalEvidenceData.path);
loadedPreview->setReadonly(readonly);
// get all remote tags (for op)
std::vector<qint64> initialTagIDs;
initialTagIDs.reserve(originalEvidenceData.tags.size());
for (const model::Tag &tag : originalEvidenceData.tags) {
initialTagIDs.push_back(tag.serverTagId);
}
tagEditor->loadTags(operationSlug, initialTagIDs);
}
catch (QSqlError &e) {
loadedPreview = new ErrorView("Unable to load evidence: " + e.text(), this);
}
splitter->addWidget(loadedPreview);
}
void EvidenceEditor::updateEvidence(qint64 evidenceID, bool readonly) {
clearEditor();
setEnabled(false);
this->readonly = readonly;
this->evidenceID = evidenceID;
if (evidenceID > 0) {
loadData();
}
}
void EvidenceEditor::clearEditor() {
tagEditor->clear();
this->descriptionTextBox->setText("");
if (loadedPreview != nullptr) {
loadedPreview->clearPreview();
}
}
void EvidenceEditor::onTagsLoaded(bool success) {
if (!success) {
tagEditor->setEnabled(false);
}
else {
tagEditor->setEnabled(!readonly);
}
emit onWidgetReady();
}
// saveEvidence is a helper method to save (to the database) the currently
// loaded evidence, using the editor changes.
SaveEvidenceResponse EvidenceEditor::saveEvidence() {
if (loadedPreview != nullptr) {
loadedPreview->saveEvidence();
}
auto evi = encodeEvidence();
auto resp = SaveEvidenceResponse(evi);
try {
db->updateEvidenceDescription(evi.description, evi.id);
db->setEvidenceTags(evi.tags, evi.id);
resp.actionSucceeded = true;
}
catch (QSqlError &e) {
resp.actionSucceeded = false;
resp.errorText = e.text();
}
return resp;
}
// deleteEvidence is a helper method to delete both the database record and
// file location of the currently loaded evidence.
DeleteEvidenceResponse EvidenceEditor::deleteEvidence() {
auto evi = encodeEvidence();
auto resp = DeleteEvidenceResponse(evi);
try {
db->deleteEvidence(evi.id);
resp.dbDeleteSuccess = true;
}
catch (QSqlError &e) {
resp.dbDeleteSuccess = false;
resp.errorText = e.text();
}
auto localFile = new QFile(evi.path);
if (!localFile->remove()) {
resp.fileDeleteSuccess = false;
resp.errorText += "\n" + localFile->errorString();
}
else {
resp.fileDeleteSuccess = true;
}
localFile->deleteLater(); // deletes the pointer, not the file
resp.errorText = resp.errorText.trimmed();
return resp;
}

View File

@ -0,0 +1,67 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef EVIDENCEEDITOR_H
#define EVIDENCEEDITOR_H
#include <QGridLayout>
#include <QLabel>
#include <QString>
#include <QTextEdit>
#include <QWidget>
#include "components/evidencepreview.h"
#include "components/tag_editor/tageditor.h"
#include "db/databaseconnection.h"
#include "deleteevidenceresponse.h"
#include "saveevidenceresponse.h"
#include <QSplitter>
class EvidenceEditor : public QWidget {
Q_OBJECT
public:
explicit EvidenceEditor(qint64 evidenceID, DatabaseConnection* db, QWidget* parent = nullptr);
explicit EvidenceEditor(DatabaseConnection* db, QWidget* parent = nullptr);
~EvidenceEditor();
private:
void buildUi();
void wireUi();
void loadData();
void clearEditor();
public:
model::Evidence encodeEvidence();
void setEnabled(bool enable);
SaveEvidenceResponse saveEvidence();
DeleteEvidenceResponse deleteEvidence();
signals:
void onWidgetReady();
public slots:
void updateEvidence(qint64 evidenceID, bool readonly);
private slots:
void onTagsLoaded(bool success);
private:
DatabaseConnection* db;
qint64 evidenceID = 0;
QString operationSlug;
bool readonly = false;
model::Evidence originalEvidenceData;
// UI components
QGridLayout* gridLayout;
QSplitter* splitter;
QLabel* _descriptionLabel;
QTextEdit* descriptionTextBox;
TagEditor* tagEditor;
EvidencePreview* loadedPreview = nullptr;
};
#endif // EVIDENCEEDITOR_H

View File

@ -0,0 +1,23 @@
#ifndef SAVEEVIDENCERESPONSE_H
#define SAVEEVIDENCERESPONSE_H
#include <QString>
#include "models/evidence.h"
struct SaveEvidenceResponse {
SaveEvidenceResponse(model::Evidence model) {
this->model = model;
this->errorText = "";
}
SaveEvidenceResponse(bool success, QString err, model::Evidence model)
: SaveEvidenceResponse(model) {
this->actionSucceeded = success;
this->errorText = err;
}
bool actionSucceeded;
QString errorText;
model::Evidence model;
};
#endif // SAVEEVIDENCERESPONSE_H

View File

@ -0,0 +1,9 @@
#include "evidencepreview.h"
EvidencePreview::EvidencePreview(QWidget *parent) : QWidget(parent) {}
void EvidencePreview::saveEvidence() {
// no-op expected for view-only evidence
}
void EvidencePreview::setReadonly(bool readonly) { this->readonly = readonly; }

View File

@ -0,0 +1,44 @@
#ifndef EVIDENCEPREVIEW_H
#define EVIDENCEPREVIEW_H
#include <QWidget>
/**
* @brief The EvidencePreview class is a (non-pure) virtual class that provides a thin wrapper
* around individual evidence previews. This ensures some common, basic functionality across
* evidence types.
*/
class EvidencePreview : public QWidget {
Q_OBJECT
public:
explicit EvidencePreview(QWidget *parent = nullptr);
public:
/**
* @brief loadFromFile is a pure virtual method allowing each preview to load its data from disk
* (where all evidence types live)
* @param filepath is the full path to the evidence file
*/
virtual void loadFromFile(QString filepath) = 0;
/// clearPreview is a pure virtual method that requests the preview content to render a
/// default/plain view. No content should be displayed.
virtual void clearPreview() = 0;
/// saveEvidence allows the underlying evidence to be re-written to disk. The default
/// implementation is a no-op.
virtual void saveEvidence();
/// setReadonly marks the evidence preview as read-only, disallowing editing. The default
/// implementation sets the internal flag -- it is the responsibilty of the underlying evidence to
/// act on this data.
virtual void setReadonly(bool readonly);
/// isReadOnly returns whether the current preview has been marked as readonly.
inline bool isReadOnly() { return readonly; };
private:
bool readonly = false;
};
#endif // EVIDENCEPREVIEW_H

View File

@ -0,0 +1,153 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Morgan Leborgne
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// clang-format off
#include "qprogressindicator.h"
#include <QPainter>
QProgressIndicator::QProgressIndicator(QWidget* parent)
: QWidget(parent)
, m_angle(0)
, m_timerId(-1)
, m_delay(40)
, m_displayedWhenStopped(false)
, m_color(Qt::black)
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFocusPolicy(Qt::NoFocus);
}
bool
QProgressIndicator::isAnimated() const
{
return (m_timerId != -1);
}
void
QProgressIndicator::setDisplayedWhenStopped(bool state)
{
m_displayedWhenStopped = state;
update();
}
bool
QProgressIndicator::isDisplayedWhenStopped() const
{
return m_displayedWhenStopped;
}
void
QProgressIndicator::startAnimation()
{
m_angle = 0;
if (m_timerId == -1)
m_timerId = startTimer(m_delay);
}
void
QProgressIndicator::stopAnimation()
{
if (m_timerId != -1)
killTimer(m_timerId);
m_timerId = -1;
update();
}
void
QProgressIndicator::setAnimationDelay(int delay)
{
if (m_timerId != -1)
killTimer(m_timerId);
m_delay = delay;
if (m_timerId != -1)
m_timerId = startTimer(m_delay);
}
void
QProgressIndicator::setColor(const QColor& color)
{
m_color = color;
update();
}
QSize
QProgressIndicator::sizeHint() const
{
return QSize(20, 20);
}
int
QProgressIndicator::heightForWidth(int w) const
{
return w;
}
void
QProgressIndicator::timerEvent(QTimerEvent* /*event*/)
{
m_angle = (m_angle + 30) % 360;
update();
}
void
QProgressIndicator::paintEvent(QPaintEvent* /*event*/)
{
if (!m_displayedWhenStopped && !isAnimated())
return;
int width = qMin(this->width(), this->height());
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
int outerRadius = (width - 1) * 0.5;
int innerRadius = (width - 1) * 0.5 * 0.38;
int capsuleHeight = outerRadius - innerRadius;
int capsuleWidth = (width > 32) ? capsuleHeight * .23 : capsuleHeight * .35;
int capsuleRadius = capsuleWidth / 2;
for (int i = 0; i < 12; i++) {
QColor color = m_color;
color.setAlphaF(1.0f - (i / 12.0f));
p.setPen(Qt::NoPen);
p.setBrush(color);
p.save();
p.translate(rect().center());
p.rotate(m_angle - i * 30.0f);
p.drawRoundedRect(-capsuleWidth * 0.5, -(innerRadius + capsuleHeight), capsuleWidth,
capsuleHeight, capsuleRadius, capsuleRadius);
p.restore();
}
}
// clang-format on

View File

@ -0,0 +1,122 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2011 Morgan Leborgne
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// clang-format off
#ifndef QPROGRESSINDICATOR_H
#define QPROGRESSINDICATOR_H
#include <QColor>
#include <QWidget>
/*!
\class QProgressIndicator
\brief The QProgressIndicator class lets an application display a progress indicator to show
that a lengthy task is under way.
Progress indicators are indeterminate and do nothing more than spin to show that the application
is busy.
\sa QProgressBar
*/
class QProgressIndicator : public QWidget
{
Q_OBJECT
Q_PROPERTY(int delay READ animationDelay WRITE setAnimationDelay)
Q_PROPERTY(bool displayedWhenStopped READ isDisplayedWhenStopped WRITE setDisplayedWhenStopped)
Q_PROPERTY(QColor color READ color WRITE setColor)
public:
QProgressIndicator(QWidget* parent = 0);
/*! Returns the delay between animation steps.
\return The number of milliseconds between animation steps. By default, the animation delay
is set to 40 milliseconds.
\sa setAnimationDelay
*/
int animationDelay() const { return m_delay; }
/*! Returns a Boolean value indicating whether the component is currently animated.
\return Animation state.
\sa startAnimation stopAnimation
*/
bool isAnimated() const;
/*! Returns a Boolean value indicating whether the receiver shows itself even when it is not
animating.
\return Return true if the progress indicator shows itself even when it is not animating. By
default, it returns false.
\sa setDisplayedWhenStopped
*/
bool isDisplayedWhenStopped() const;
/*! Returns the color of the component.
\sa setColor
*/
const QColor& color() const { return m_color; }
virtual QSize sizeHint() const;
int heightForWidth(int w) const;
public slots:
/*! Starts the spin animation.
\sa stopAnimation isAnimated
*/
void startAnimation();
/*! Stops the spin animation.
\sa startAnimation isAnimated
*/
void stopAnimation();
/*! Sets the delay between animation steps.
Setting the \a delay to a value larger than 40 slows the animation, while setting the \a
delay to a smaller value speeds it up.
\param delay The delay, in milliseconds.
\sa animationDelay
*/
void setAnimationDelay(int delay);
/*! Sets whether the component hides itself when it is not animating.
\param state The animation state. Set false to hide the progress indicator when it is not
animating; otherwise true.
\sa isDisplayedWhenStopped
*/
void setDisplayedWhenStopped(bool state);
/*! Sets the color of the components to the given color.
\sa color
*/
void setColor(const QColor& color);
protected:
virtual void timerEvent(QTimerEvent* event);
virtual void paintEvent(QPaintEvent* event);
private:
int m_angle;
int m_timerId;
int m_delay;
bool m_displayedWhenStopped;
QColor m_color;
};
#endif // QPROGRESSINDICATOR_H
// clang-format on

View File

@ -0,0 +1,48 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "loadingbutton.h"
#include <QResizeEvent>
LoadingButton::LoadingButton(QWidget* parent, QPushButton* model)
: LoadingButton("", parent, model) {}
LoadingButton::LoadingButton(const QString& text, QWidget* parent, QPushButton* model)
: QPushButton(text, parent) {
if (model == nullptr) {
model = this;
}
this->setMinimumSize(model->width(), model->minimumHeight());
this->resize(model->width(), model->height());
this->showingLabel = true;
loading = new QProgressIndicator(this);
loading->setMinimumSize(this->minimumSize());
loading->resize(this->width(), this->height());
}
LoadingButton::~LoadingButton() { delete loading; }
void LoadingButton::startAnimation() {
label = this->text();
showLabel(false);
}
void LoadingButton::stopAnimation() { showLabel(true); }
void LoadingButton::showLabel(bool show) {
this->showingLabel = show;
if (show) {
loading->stopAnimation();
this->setText(this->label);
}
else {
this->setText("");
loading->startAnimation();
}
}
void LoadingButton::resizeEvent(QResizeEvent* evt) {
QPushButton::resizeEvent(evt);
loading->resize(evt->size());
}

View File

@ -0,0 +1,38 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef LOADINGBUTTON_H
#define LOADINGBUTTON_H
#include <QPushButton>
#include "components/loading/qprogressindicator.h"
class LoadingButton : public QPushButton {
Q_OBJECT
public:
explicit LoadingButton(QWidget* parent = nullptr, QPushButton* model = nullptr);
explicit LoadingButton(const QString& text, QWidget* parent = nullptr,
QPushButton* model = nullptr);
~LoadingButton();
void startAnimation();
void stopAnimation();
private:
void showLabel(bool show);
void init();
protected:
void resizeEvent(QResizeEvent* evt) override;
private:
QProgressIndicator* loading;
QString label;
bool showingLabel;
};
#endif // LOADINGBUTTON_H

View File

@ -0,0 +1,240 @@
#include "tageditor.h"
#include <QRandomGenerator>
#include "dtos/tag.h"
#include "helpers/netman.h"
#include "helpers/stopreply.h"
TagEditor::TagEditor(QWidget *parent) : QWidget(parent) {
buildUi();
couldNotCreateTagMsg = new QErrorMessage(this);
wireUi();
}
TagEditor::~TagEditor() {
delete _withTagsLabel;
delete createTagTextBox;
delete includedTagsListBox;
delete allTagsListBox;
delete includeTagButton;
delete excludeTagButton;
delete couldNotCreateTagMsg;
delete createTagButton;
delete gridLayout;
stopReply(&getTagsReply);
stopReply(&createTagReply);
}
void TagEditor::buildUi() {
gridLayout = new QGridLayout(this);
gridLayout->setMargin(0);
_withTagsLabel = new QLabel("With Tags...", this);
includedTagsListBox = new QListWidget(this);
allTagsListBox = new QListWidget(this);
includeTagButton = new QPushButton("<<", this);
excludeTagButton = new QPushButton(">>", this);
createTagTextBox = new QLineEdit();
createTagButton = new LoadingButton("Create Tag", this);
minorErrorLabel = new QLabel(this);
// Layout
/* 0 1 2 3
+----------+-----------+----------+---------+
0 | With Tags label |
+----------+-----------+----------+---------+
1 | Include | << btn | All |
+ +-----------+ + +
2 | Tag List | >> Btn | Tags List |
+----------+-----------+----------+---------+
3 | err Lab | <empty> | Add TB | add btn |
+----------+-----------+----------+---------+
*/
// row 0
gridLayout->addWidget(_withTagsLabel, 0, 0, 1, gridLayout->columnCount());
// row 1
gridLayout->addWidget(includedTagsListBox, 1, 0, 2, 1);
gridLayout->addWidget(includeTagButton, 1, 1);
gridLayout->addWidget(allTagsListBox, 1, 2, 2, 2);
// row 2
gridLayout->addWidget(excludeTagButton, 2, 1);
// row 3
gridLayout->addWidget(minorErrorLabel, 3, 0);
gridLayout->addWidget(createTagTextBox, 3, 2);
gridLayout->addWidget(createTagButton, 3, 3);
this->setLayout(gridLayout);
}
void TagEditor::wireUi() {
auto btnClicked = &QPushButton::clicked;
connect(createTagButton, btnClicked, this, &TagEditor::createTagButtonClicked);
connect(includeTagButton, btnClicked, this, &TagEditor::includeTagButtonClicked);
connect(excludeTagButton, btnClicked, this, &TagEditor::excludeTagButtonClicked);
connect(createTagTextBox, &QLineEdit::returnPressed, this, &TagEditor::createTagButtonClicked);
allTagsListBox->setSortingEnabled(true);
}
void TagEditor::clear() {
stopReply(&getTagsReply);
stopReply(&createTagReply);
createTagTextBox->setText("");
allTagsListBox->clear();
includedTagsListBox->clear();
minorErrorLabel->setText("");
}
void TagEditor::setEnabled(bool enable) {
createTagButton->setEnabled(enable);
includeTagButton->setEnabled(enable);
excludeTagButton->setEnabled(enable);
createTagTextBox->setEnabled(enable);
}
void TagEditor::loadTags(const QString& operationSlug, std::vector<qint64> initialTagIDs) {
this->operationSlug = operationSlug;
includedTagIds.clear();
for (auto tagID : initialTagIDs) {
includedTagIds.insert(tagID);
}
getTagsReply = NetMan::getInstance().getOperationTags(operationSlug);
connect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
}
void TagEditor::refreshTagBoxes() {
allTagsListBox->clear();
includedTagsListBox->clear();
for (const auto &itr : knownTags) {
QString tagText = itr.first;
qint64 tagId = itr.second;
int itemCount = includedTagIds.count(tagId);
if (itemCount == 1) {
includedTagsListBox->addItem(tagText);
}
else {
allTagsListBox->addItem(tagText);
}
}
}
std::vector<model::Tag> TagEditor::getIncludedTags() {
std::vector<model::Tag> rtn;
rtn.reserve(includedTagIds.size());
// Construct a reverse map to find tag names.
// slightly inefficient way to do this, but much easier to code against.
std::unordered_map<qint64, QString> reversedMap;
for (const auto &entry : knownTags) {
reversedMap.insert(std::pair<qint64, QString>(entry.second, entry.first));
}
for (const qint64 &tagID : includedTagIds) {
try {
auto tagName = reversedMap.at(tagID);
model::Tag tag(tagID, tagName);
rtn.push_back(tag);
}
catch (std::out_of_range &e) {
} // drop any tag ids we can't find (doesn't exist on the server, and will fail anyway)
}
return rtn;
}
void TagEditor::createTagButtonClicked() {
auto newText = createTagTextBox->text().trimmed();
if (newText == "") {
return;
}
createTagButton->startAnimation();
createTagButton->setEnabled(false);
dto::Tag newTag(newText, randomColor());
createTagReply = NetMan::getInstance().createTag(newTag, operationSlug);
connect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
}
void TagEditor::includeTagButtonClicked() {
auto items = allTagsListBox->selectedItems();
for (auto item : items) {
includedTagsListBox->addItem(item->text());
allTagsListBox->takeItem(allTagsListBox->row(item));
includedTagIds.insert(knownTags[item->text()]);
}
}
void TagEditor::excludeTagButtonClicked() {
auto items = includedTagsListBox->selectedItems();
for (auto item : items) {
allTagsListBox->addItem(item->text());
includedTagsListBox->takeItem(includedTagsListBox->row(item));
includedTagIds.erase(knownTags[item->text()]);
}
}
void TagEditor::onGetTagsComplete() {
bool isValid;
auto data = NetMan::extractResponse(getTagsReply, isValid);
if (isValid) {
std::vector<dto::Tag> tags = dto::Tag::parseDataAsList(data);
knownTags.clear();
for (const dto::Tag &tag : tags) {
knownTags.insert(std::pair<QString, qint64>(tag.name, tag.id));
}
refreshTagBoxes();
}
else {
minorErrorLabel->setText(tr("Unable to fetch tags. Please check your connection."));
includeTagButton->setEnabled(false);
}
disconnect(getTagsReply, &QNetworkReply::finished, this, &TagEditor::onGetTagsComplete);
tidyReply(&getTagsReply);
emit tagsLoaded(isValid);
}
void TagEditor::onCreateTagComplete() {
bool isValid;
auto data = NetMan::extractResponse(createTagReply, isValid);
if (isValid) {
auto newTag = dto::Tag::parseData(data);
knownTags.insert(std::pair<QString, qint64>(newTag.name, newTag.id));
includedTagIds.insert(newTag.id);
createTagTextBox->setText("");
refreshTagBoxes();
}
else {
couldNotCreateTagMsg->showMessage(
"Could not create tag. Please check your connection and try again.");
}
disconnect(createTagReply, &QNetworkReply::finished, this, &TagEditor::onCreateTagComplete);
tidyReply(&createTagReply);
createTagButton->stopAnimation();
createTagButton->setEnabled(true);
}
QString TagEditor::randomColor() {
// Note: this should match the frontend's color palette (naming)
static std::vector<QString> colors = {
"blue", "yellow", "green", "indigo", "orange",
"lightBlue", "lightYellow", "lightGreen", "lightIndigo", "lightOrange",
"pink", "red", "teal", "vermilion", "violet",
"lightPink", "lightRed", "lightTeal", "lightVermilion", "lightViolet"};
auto index = QRandomGenerator::global()->bounded(int(colors.size()));
return colors.at(index);
}

View File

@ -0,0 +1,70 @@
#ifndef TAGEDITOR_H
#define TAGEDITOR_H
#include <QErrorMessage>
#include <QGridLayout>
#include <QLabel>
#include <QLineEdit>
#include <QListWidget>
#include <QNetworkReply>
#include <QPushButton>
#include <QWidget>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "components/loading_button/loadingbutton.h"
#include "models/tag.h"
class TagEditor : public QWidget {
Q_OBJECT
public:
explicit TagEditor(QWidget* parent = nullptr);
~TagEditor();
public:
void setEnabled(bool enable);
void loadTags(const QString& operationSlug, std::vector<qint64> initialTagIDs);
void clear();
std::vector<model::Tag> getIncludedTags();
signals:
void tagsLoaded(bool success);
private:
void buildUi();
void wireUi();
void refreshTagBoxes();
QString randomColor();
private slots:
void createTagButtonClicked();
void includeTagButtonClicked();
void excludeTagButtonClicked();
void onGetTagsComplete();
void onCreateTagComplete();
private:
QString operationSlug;
std::unordered_map<QString, qint64> knownTags;
std::unordered_set<qint64> includedTagIds;
// ui Components
QGridLayout* gridLayout;
QLabel* _withTagsLabel;
LoadingButton* createTagButton;
QLineEdit* createTagTextBox;
QListWidget* includedTagsListBox;
QListWidget* allTagsListBox;
QPushButton* includeTagButton;
QPushButton* excludeTagButton;
QLabel* minorErrorLabel;
QNetworkReply* getTagsReply = nullptr;
QNetworkReply* createTagReply = nullptr;
QErrorMessage* couldNotCreateTagMsg = nullptr;
};
#endif // TAGEDITOR_H

View File

@ -0,0 +1,343 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "databaseconnection.h"
#include <QDir>
#include <QVariant>
#include <iostream>
#include <vector>
#include "exceptions/databaseerr.h"
#include "exceptions/fileerror.h"
// DatabaseConnection constructs a connection to the database, unsurpringly. Note that the
// constructor can throw a error (see below). Additionally, many methods can throw a QSqlError,
// though are not marked as such in their comments. Other errors are listed in throw comments, or
// are marked as noexcept if no error is possible.
//
// Throws: DBDriverUnavailable if the required database driver does not exist
DatabaseConnection::DatabaseConnection() {
const QString DRIVER("QSQLITE");
if (QSqlDatabase::isDriverAvailable(DRIVER)) {
db = QSqlDatabase::addDatabase(DRIVER);
db.setDatabaseName(dbPath);
auto dbFileRoot = dbPath.left(dbPath.lastIndexOf("/"));
QDir().mkpath(dbFileRoot);
}
else {
throw DBDriverUnavailableError("SQLite");
}
}
void DatabaseConnection::connect() {
if (!db.open()) {
throw db.lastError();
}
migrateDB();
}
void DatabaseConnection::close() noexcept { db.close(); }
qint64 DatabaseConnection::createEvidence(const QString &filepath, const QString &operationSlug,
const QString &contentType) {
return doInsert(&db,
"INSERT INTO evidence"
" (path, operation_slug, content_type, recorded_date)"
" VALUES"
" (?, ?, ?, datetime('now'))",
{filepath, operationSlug, contentType});
}
model::Evidence DatabaseConnection::getEvidenceDetails(qint64 evidenceID) {
model::Evidence rtn;
auto query = executeQuery(
&db,
"SELECT"
" id, path, operation_slug, content_type, description, error, recorded_date, upload_date"
" FROM evidence"
" WHERE id=? LIMIT 1",
{evidenceID});
if (query.first()) {
rtn.id = query.value("id").toLongLong();
rtn.path = query.value("path").toString();
rtn.operationSlug = query.value("operation_slug").toString();
rtn.contentType = query.value("content_type").toString();
rtn.description = query.value("description").toString();
rtn.errorText = query.value("error").toString();
rtn.recordedDate = query.value("recorded_date").toDateTime();
rtn.uploadDate = query.value("upload_date").toDateTime();
auto getTagQuery = executeQuery(&db,
"SELECT"
" id, tag_id, name"
" FROM tags"
" WHERE evidence_id=?",
{evidenceID});
while (getTagQuery.next()) {
rtn.tags.emplace_back(model::Tag(getTagQuery.value("id").toLongLong(),
getTagQuery.value("tag_id").toLongLong(),
getTagQuery.value("name").toString()));
}
}
else {
std::cout << "Could not find evidence with id: " << evidenceID << std::endl;
}
return rtn;
}
void DatabaseConnection::updateEvidenceDescription(const QString &newDescription,
qint64 evidenceID) {
executeQuery(&db, "UPDATE evidence SET description=? WHERE id=?", {newDescription, evidenceID});
}
void DatabaseConnection::deleteEvidence(qint64 evidenceID) {
executeQuery(&db, "DELETE FROM evidence WHERE id=?", {evidenceID});
}
void DatabaseConnection::updateEvidenceError(const QString &errorText, qint64 evidenceID) {
executeQuery(&db, "UPDATE evidence SET error=? WHERE id=?", {errorText, evidenceID});
}
void DatabaseConnection::updateEvidenceSubmitted(qint64 evidenceID) {
executeQuery(&db, "UPDATE evidence SET upload_date=datetime('now') WHERE id=?", {evidenceID});
}
void DatabaseConnection::setEvidenceTags(const std::vector<model::Tag> &newTags,
qint64 evidenceID) {
QList<QVariant> newTagIds;
for (const auto &tag : newTags) {
newTagIds.push_back(tag.serverTagId);
}
executeQuery(&db, "DELETE FROM tags WHERE tag_id NOT IN (?) AND evidence_id = ?",
{newTagIds, evidenceID});
auto currentTagsResult =
executeQuery(&db, "SELECT tag_id FROM tags WHERE evidence_id = ?", {evidenceID});
QList<qint64> currentTags;
while (currentTagsResult.next()) {
currentTags.push_back(currentTagsResult.value("tag_id").toLongLong());
}
struct dataset {
qint64 evidenceID = 0;
qint64 tagID = 0;
QString name;
};
std::vector<dataset> tagDataToInsert;
QString baseQuery = "INSERT INTO tags (evidence_id, tag_id, name) VALUES ";
for (const auto &newTag : newTags) {
if (currentTags.count(newTag.serverTagId) == 0) {
dataset item;
item.evidenceID = evidenceID;
item.tagID = newTag.serverTagId;
item.name = newTag.tagName;
tagDataToInsert.push_back(item);
}
}
// one possible concern: we are going to be passing a lot of parameters
// sqlite indicates it's default is 100 passed parameter, but it can "handle thousands"
if (!tagDataToInsert.empty()) {
std::vector<QVariant> args;
baseQuery += "(?,?,?)";
baseQuery += QString(", (?,?,?)").repeated(int(tagDataToInsert.size() - 1));
for (const auto &item : tagDataToInsert) {
args.emplace_back(item.evidenceID);
args.emplace_back(item.tagID);
args.emplace_back(item.name);
}
executeQuery(&db, baseQuery, args);
}
}
DBQuery DatabaseConnection::buildGetEvidenceWithFiltersQuery(const EvidenceFilters &filters) {
QString query =
"SELECT id, path, operation_slug, content_type, description, error, recorded_date, "
"upload_date"
" FROM evidence";
std::vector<QVariant> values;
std::vector<QString> parts;
if (filters.hasError != Tri::Any) {
parts.emplace_back(" error LIKE ? ");
// _% will ensure at least one character exists in the error column, ensuring it's populated
values.emplace_back(filters.hasError == Tri::Yes ? "_%" : "");
}
if (filters.submitted != Tri::Any) {
parts.emplace_back((filters.submitted == Tri::Yes) ? " upload_date IS NOT NULL "
: " upload_date IS NULL ");
}
if (!filters.operationSlug.isEmpty()) {
parts.emplace_back(" operation_slug = ? ");
values.emplace_back(filters.operationSlug);
}
if (!filters.contentType.isEmpty()) {
parts.emplace_back(" content_type = ? ");
values.emplace_back(filters.contentType);
}
if (filters.startDate.isValid()) {
parts.emplace_back(" recorded_date >= ? ");
values.emplace_back(filters.startDate);
}
if (filters.endDate.isValid()) {
auto realEndDate = filters.endDate.addDays(1);
parts.emplace_back(" recorded_date < ? ");
values.emplace_back(realEndDate);
}
if (!parts.empty()) {
query += " WHERE " + parts.at(0);
for (size_t i = 1; i < parts.size(); i++) {
query += " AND " + parts.at(i);
}
}
return DBQuery(query, values);
}
std::vector<model::Evidence> DatabaseConnection::getEvidenceWithFilters(
const EvidenceFilters &filters) {
auto dbQuery = buildGetEvidenceWithFiltersQuery(filters);
auto resultSet = executeQuery(&db, dbQuery.query(), dbQuery.values());
std::vector<model::Evidence> allEvidence;
while (resultSet.next()) {
model::Evidence evi;
evi.id = resultSet.value("id").toLongLong();
evi.path = resultSet.value("path").toString();
evi.operationSlug = resultSet.value("operation_slug").toString();
evi.contentType = resultSet.value("content_type").toString();
evi.description = resultSet.value("description").toString();
evi.errorText = resultSet.value("error").toString();
evi.recordedDate = resultSet.value("recorded_date").toDateTime();
evi.uploadDate = resultSet.value("upload_date").toDateTime();
allEvidence.push_back(evi);
}
return allEvidence;
}
// migrateDB checks the migration status and then performs the full migration for any
// lacking update.
//
// Throws exceptions/FileError if a migration file cannot be found.
void DatabaseConnection::migrateDB() {
std::cout << "Checking database state" << std::endl;
auto migrationsToApply = DatabaseConnection::getUnappliedMigrations();
for (const QString &newMigration : migrationsToApply) {
QFile migrationFile(":/migrations/" + newMigration);
auto ok = migrationFile.open(QFile::ReadOnly);
if (!ok) {
throw FileError::mkError("Error reading migration file",
migrationFile.fileName().toStdString(), migrationFile.error());
}
auto content = QString(migrationFile.readAll());
migrationFile.close();
std::cout << "Applying Migration: " << newMigration.toStdString() << std::endl;
auto upScript = extractMigrateUpContent(content);
executeQuery(&db, upScript);
executeQuery(&db,
"INSERT INTO migrations (migration_name, applied_at) VALUES (?, datetime('now'))",
{newMigration});
}
std::cout << "All migrations applied" << std::endl;
}
// getUnappliedMigrations retrieves a list of all of the migrations that have not been applied
// to the local database.
//
// Note: All sql files must end in ".sql" to be picked up
//
// Throws:
// * BadDatabaseStateError if some migrations have been applied that are not known
// * QSqlError if database queries fail
QStringList DatabaseConnection::getUnappliedMigrations() {
QDir migrationsDir(":/migrations");
auto allMigrations = migrationsDir.entryList(QDir::Files, QDir::Name);
QStringList appliedMigrations;
QStringList migrationsToApply;
QSqlQuery dbMigrations("SELECT migration_name FROM migrations");
if (dbMigrations.exec()) {
while (dbMigrations.next()) {
appliedMigrations << dbMigrations.value("migration_name").toString();
}
}
// compare the two list to find gaps
for (const QString &possibleMigration : allMigrations) {
if (possibleMigration.right(4) != ".sql") {
continue; // assume non-sql files aren't actual migrations.
}
auto foundIndex = appliedMigrations.indexOf(possibleMigration);
if (foundIndex == -1) {
migrationsToApply << possibleMigration;
}
else {
appliedMigrations.removeAt(foundIndex);
}
}
if (!appliedMigrations.empty()) {
throw BadDatabaseStateError();
}
return migrationsToApply;
}
// extractMigrateUpContent parses the given migration content and retrieves only
// the portion that applies to the "up" / apply logic. The "down" section is ignored.
QString DatabaseConnection::extractMigrateUpContent(const QString &allContent) noexcept {
auto copying = false;
QString upContent;
for (const QString &line : allContent.split("\n")) {
if (line.trimmed().toLower() == "-- +migrate up") {
copying = true;
}
else if (line.trimmed().toLower() == "-- +migrate down") {
if (copying) {
break;
}
copying = false;
}
else if (copying) {
upContent.append(line + "\n");
}
}
return upContent;
}
// executeQuery simply attempts to execute the given stmt with the passed args. The statement is
// first prepared, and arg placements can be specified with "?"
//
// Throws: QSqlError when a query error occurs
QSqlQuery DatabaseConnection::executeQuery(QSqlDatabase *db, const QString &stmt,
const std::vector<QVariant> &args) {
QSqlQuery query(*db);
if (!query.prepare(stmt)) {
throw query.lastError();
}
for (const auto &arg : args) {
query.addBindValue(arg);
}
if (!query.exec()) {
throw query.lastError();
}
return query;
}
// doInsert is a version of executeQuery that returns the last inserted id, rather than the
// underlying query/response
//
// Throws: QSqlError when a query error occurs
qint64 DatabaseConnection::doInsert(QSqlDatabase *db, const QString &stmt,
const std::vector<QVariant> &args) {
auto query = executeQuery(db, stmt, args);
return query.lastInsertId().toLongLong();
}

View File

@ -0,0 +1,70 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef DATABASECONNECTION_H
#define DATABASECONNECTION_H
#include <QSqlDatabase>
#include <QSqlDriver>
#include <QSqlError>
#include <QSqlQuery>
#include <QStandardPaths>
#include <QString>
#include <QVariant>
#include "forms/evidence_filter/evidencefilter.h"
#include "models/evidence.h"
static QString dbPath =
QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/ashirt/screenshot.sqlite";
class DBQuery {
private:
QString _query;
std::vector<QVariant> _values;
public:
DBQuery(QString query) : DBQuery(query, {}) {}
DBQuery(QString query, std::vector<QVariant> values) {
this->_query = query;
this->_values = values;
}
inline QString query() { return _query; }
inline std::vector<QVariant> values() { return _values; }
};
class DatabaseConnection {
public:
DatabaseConnection();
void connect();
void close() noexcept;
DBQuery buildGetEvidenceWithFiltersQuery(const EvidenceFilters &filters);
model::Evidence getEvidenceDetails(qint64 evidenceID);
std::vector<model::Evidence> getEvidenceWithFilters(const EvidenceFilters &filters);
qint64 createEvidence(const QString &filepath, const QString &operationSlug,
const QString &contentType);
void updateEvidenceDescription(const QString &newDescription, qint64 evidenceID);
void updateEvidenceError(const QString &errorText, qint64 evidenceID);
void updateEvidenceSubmitted(qint64 evidenceID);
void setEvidenceTags(const std::vector<model::Tag> &newTags, qint64 evidenceID);
void deleteEvidence(qint64 evidenceID);
private:
QSqlDatabase db;
void migrateDB();
QStringList getUnappliedMigrations();
static QString extractMigrateUpContent(const QString &allContent) noexcept;
static QSqlQuery executeQuery(QSqlDatabase *db, const QString &stmt,
const std::vector<QVariant> &args = {});
static qint64 doInsert(QSqlDatabase *db, const QString &stmt, const std::vector<QVariant> &args);
};
#endif // DATABASECONNECTION_H

52
src/dtos/operation.h Normal file
View File

@ -0,0 +1,52 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef DTO_OPERATION_H
#define DTO_OPERATION_H
#include <QVariant>
#include <vector>
#include "helpers/jsonhelpers.h"
namespace dto {
class Operation {
public:
Operation() {}
enum OperationStatus {
OperationStatusPlanning = 0,
OperationStatusAcitve = 1,
OperationStatusComplete = 2,
};
QString slug;
QString name;
int numUsers;
OperationStatus status;
qint64 id;
static Operation parseData(QByteArray data) {
return parseJSONItem<Operation>(data, Operation::fromJson);
}
static std::vector<Operation> parseDataAsList(QByteArray data) {
return parseJSONList<Operation>(data, Operation::fromJson);
}
private:
// provides a Operation from a given QJsonObject
static Operation fromJson(QJsonObject obj) {
Operation o;
o.slug = obj["slug"].toString();
o.name = obj["name"].toString();
o.numUsers = obj["numUsers"].toInt();
o.status = static_cast<OperationStatus>(obj["status"].toInt());
o.id = obj["id"].toVariant().toLongLong();
return o;
}
};
} // namespace dto
#endif // DTO_OPERATION_H

51
src/dtos/tag.h Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef DTO_TAG_H
#define DTO_TAG_H
#include <QVariant>
#include <vector>
#include "helpers/jsonhelpers.h"
namespace dto {
class Tag {
public:
Tag() {}
Tag(QString name, QString colorName) {
this->name = name;
this->colorName = colorName;
}
qint64 id;
QString colorName;
QString name;
static Tag parseData(QByteArray data) { return parseJSONItem<Tag>(data, Tag::fromJson); }
static std::vector<Tag> parseDataAsList(QByteArray data) {
return parseJSONList<Tag>(data, Tag::fromJson);
}
static QByteArray toJson(Tag t) {
QJsonObject obj;
obj.insert("colorName", t.colorName);
obj.insert("name", t.name);
return QJsonDocument(obj).toJson();
}
private:
// provides a Tag from a given QJsonObject
static Tag fromJson(QJsonObject obj) {
Tag t;
t.id = obj["id"].toVariant().toLongLong();
t.colorName = obj["colorName"].toString();
t.name = obj["name"].toString();
return t;
}
};
} // namespace dto
#endif // DTO_TAG_H

View File

@ -0,0 +1,20 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef DATABASEERR_H
#define DATABASEERR_H
#include <stdexcept>
class BadDatabaseStateError : public std::runtime_error {
public:
BadDatabaseStateError() : std::runtime_error("Database is in an inconsistent state") {}
};
class DBDriverUnavailableError : public std::runtime_error {
public:
DBDriverUnavailableError(std::string friendlyDriverName)
: std::runtime_error(friendlyDriverName + " driver is unavailable") {}
};
#endif // DATABASEERR_H

View File

@ -0,0 +1,69 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef FILEERROR_H
#define FILEERROR_H
#include <QFileDevice>
#include <stdexcept>
#include <string>
class FileError : public std::runtime_error {
public:
static FileError mkError(std::string msg, std::string path, QFileDevice::FileError err) {
std::string suberror;
switch (err) {
case QFileDevice::ReadError:
suberror = "Error reading file";
break;
case QFileDevice::WriteError:
suberror = "Error writing file";
break;
case QFileDevice::FatalError:
suberror = "Fatal error occurred";
break;
case QFileDevice::ResourceError:
suberror = "Insufficient resources available";
break;
case QFileDevice::OpenError:
suberror = "Could not open file";
break;
case QFileDevice::AbortError:
suberror = "Operation was aborted";
break;
case QFileDevice::TimeOutError:
suberror = "Operation timed out";
break;
case QFileDevice::UnspecifiedError:
suberror = "Unknown Error";
break;
case QFileDevice::RemoveError:
suberror = "Unable to remove file";
break;
case QFileDevice::RenameError:
suberror = "Unable to rename/move file";
break;
case QFileDevice::PositionError:
suberror = "Position error"; // I don't think we'll ever enounter this error
break;
case QFileDevice::ResizeError:
suberror = "Unable to resize file";
break;
case QFileDevice::PermissionsError:
suberror = "Unable to access file";
break;
case QFileDevice::CopyError:
suberror = "Unable to copy file";
break;
case QFileDevice::NoError:
suberror = "Actually, no error occurred -- just bad programming.";
break;
}
return msg + " (path: " + path + "): " + suberror;
}
private:
FileError(std::string msg) : std::runtime_error(msg) {}
};
#endif // FILEERROR_H

View File

@ -0,0 +1,38 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "buttonboxform.h"
#include <iostream>
ButtonBoxForm::ButtonBoxForm(QWidget *parent) : QDialog(parent) {}
void ButtonBoxForm::setButtonBox(QDialogButtonBox *buttonBox) { this->buttonBox = buttonBox; }
void ButtonBoxForm::routeButtonPress(QAbstractButton *btn) {
if (buttonBox == nullptr) {
return;
}
if (buttonBox->button(QDialogButtonBox::Ok) == btn) {
onOkClicked();
}
if (buttonBox->button(QDialogButtonBox::Save) == btn) {
onSaveClicked();
}
else if (buttonBox->button(QDialogButtonBox::Apply) == btn) {
onApplyClicked();
}
else if (buttonBox->button(QDialogButtonBox::Close) == btn) {
onCloseClicked();
}
else if (buttonBox->button(QDialogButtonBox::Cancel) == btn) {
onCancelClicked();
}
}
// default functions that just ignore the result, for easier use.
void ButtonBoxForm::onOkClicked() {}
void ButtonBoxForm::onSaveClicked() {}
void ButtonBoxForm::onApplyClicked() {}
void ButtonBoxForm::onCloseClicked() {}
void ButtonBoxForm::onCancelClicked() {}

31
src/forms/buttonboxform.h Normal file
View File

@ -0,0 +1,31 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef BUTTONBOXFORM_H
#define BUTTONBOXFORM_H
#include <QDialog>
#include <QDialogButtonBox>
#include <QPushButton>
class ButtonBoxForm : public QDialog {
Q_OBJECT
public:
explicit ButtonBoxForm(QWidget* parent = nullptr);
~ButtonBoxForm() = default;
void setButtonBox(QDialogButtonBox* buttonBox);
virtual void onOkClicked();
virtual void onSaveClicked();
virtual void onApplyClicked();
virtual void onCloseClicked();
virtual void onCancelClicked();
public slots:
void routeButtonPress(QAbstractButton* btn);
private:
QDialogButtonBox* buttonBox = nullptr;
};
#endif // BUTTONBOXFORM_H

View File

@ -0,0 +1,119 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "credits.h"
#include "ui_credits.h"
#include <QDateTime>
struct Attribution {
std::string library;
std::string libraryUrl;
std::string authors;
std::string license;
std::string licenseUrl;
Attribution() = default;
Attribution(std::string library, std::string libraryUrl, std::string authors, std::string license,
std::string licenseUrl) {
this->library = library;
this->libraryUrl = libraryUrl;
this->authors = authors;
this->license = license;
this->licenseUrl = licenseUrl;
}
};
static std::string hyperlinkMd(std::string label, std::string url) {
return "[" + label + "](" + url + ")";
}
static std::string attributionMarkdown() {
std::vector<Attribution> attribs = {
Attribution("Qt", "http://qt.io", "The Qt Company", "GPL v3",
"https://www.gnu.org/licenses/gpl-3.0.html"),
Attribution("QProgressIndicator", "https://github.com/mojocorp/QProgressIndicator",
"Mojocorp, et al", "MIT License",
"https://github.com/mojocorp/QProgressIndicator/blob/master/LICENSE"),
Attribution("MultipartEncoder", "https://github.com/AndsonYe/MultipartEncoder", "Ye Yangang",
"MIT License",
"https://github.com/AndsonYe/MultipartEncoder/blob/master/LICENSE"),
Attribution("UGlobalHotkey", "https://github.com/joelatdeluxe/UGlobalHotkey",
"Anton Konstantinov, et al", "Public Domain", ""),
Attribution("AspectRatioPixmapLabel", "https://stackoverflow.com/a/22618496/4262552",
"phyatt, et al", "CC BY-SA 3.0",
"https://creativecommons.org/licenses/by-sa/3.0/"),
};
std::string msg =
"This application uses the following open source software:\n\n"
"| Project/Library | Authors | License |\n"
"| --------------- | ------- | ------- |\n";
for (const auto& attrib : attribs) {
std::string license;
std::string suffix;
if (!attrib.licenseUrl.empty()) {
license += "[";
suffix = "](" + attrib.licenseUrl + ")";
}
license += attrib.license + suffix;
// clang-format off
msg += "| " + hyperlinkMd(attrib.library, attrib.libraryUrl) +
" | " + attrib.authors +
" | " + license +
" |\n";
// clang-format on
}
return msg;
}
static std::string copyrightDate() {
int initialYear = 2020;
int currentYear = QDateTime::currentDateTime().date().year();
auto rtn = std::to_string(initialYear);
if (currentYear != initialYear) {
rtn += "-" + std::to_string(currentYear);
}
return rtn;
}
static std::string versionData() { return "Beta 1"; }
static std::string userGuideUrl = "https://www.github.com/ascreen/blob/master/README.md";
static std::string reportAnIssueUrl = "https://www.github.com/ascreen/issues";
static std::string preambleMarkdown() {
const std::string lf = "\n\n"; // double linefeed to add in linebreaks in markdown
// clang-format off
return "Version: " + versionData() +
lf + "Copyright " + copyrightDate() + ", Verizon Media" +
lf + "Licensed under the terms of [GPLv3](https://www.gnu.org/licenses/gpl-3.0.html)" +
lf + "A short user guide can be found " + hyperlinkMd("here", userGuideUrl) +
lf + "Report issues " + hyperlinkMd("here", reportAnIssueUrl) +
lf;
// clang-format on
}
static std::string bodyMarkdown() {
// clang-format off
return "# AScreen\n\n"
+ preambleMarkdown()
+ "## Credits\n"
+ attributionMarkdown();
// clang-format on
}
Credits::Credits(QWidget* parent) : QDialog(parent), ui(new Ui::Credits) {
ui->setupUi(this);
ui->creditsArea->setMarkdown(bodyMarkdown().c_str());
// Make the dialog pop up above any other windows but retain title bar and buttons
Qt::WindowFlags flags = this->windowFlags();
flags |= Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;
this->setWindowFlags(flags);
}
Credits::~Credits() { delete ui; }

View File

@ -0,0 +1,24 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef CREDITS_H
#define CREDITS_H
#include <QDialog>
namespace Ui {
class Credits;
}
class Credits : public QDialog {
Q_OBJECT
public:
explicit Credits(QWidget *parent = nullptr);
~Credits();
private:
Ui::Credits *ui;
};
#endif // CREDITS_H

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Credits</class>
<widget class="QDialog" name="Credits">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>582</width>
<height>376</height>
</rect>
</property>
<property name="windowTitle">
<string>About</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QTextBrowser" name="creditsArea">
<property name="readOnly">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Credits</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Credits</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,333 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "evidencemanager.h"
#include <QCheckBox>
#include <QMessageBox>
#include <QRandomGenerator>
#include <QStandardPaths>
#include <QTableWidgetItem>
#include <iostream>
#include "appsettings.h"
#include "dtos/tag.h"
#include "forms/evidence_filter/evidencefilter.h"
#include "forms/evidence_filter/evidencefilterform.h"
#include "helpers/netman.h"
#include "helpers/stopreply.h"
#include "helpers/ui_helpers.h"
#include "ui_evidencemanager.h"
enum ColumnIndexes {
COL_DATE_CAPTURED = 0,
COL_OPERATION,
COL_PATH,
COL_CONTENT_TYPE,
COL_DESCRIPTION,
COL_SUBMITTED,
COL_DATE_SUBMITTED,
COL_FAILED,
COL_ERROR_MSG
};
EvidenceManager::EvidenceManager(DatabaseConnection* db, QWidget* parent)
: QDialog(parent), ui(new Ui::EvidenceManager) {
ui->setupUi(this);
this->db = db;
this->evidenceIDForRequest = 0; // initializing to remove clang-tidy warning
evidenceEditor = new EvidenceEditor(db, this);
filterForm = new EvidenceFilterForm(this);
submitButton =
new LoadingButton(ui->submitEvidenceButton->text(), this, ui->submitEvidenceButton);
// Replace the _evidenceEditorPlaceholder with a proper editor
UiHelpers::replacePlaceholder(ui->_evidenceEditorPlaceholder, evidenceEditor, ui->gridLayout);
evidenceEditor->setSizePolicy(QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding));
UiHelpers::replacePlaceholder(ui->submitEvidenceButton, submitButton, ui->gridLayout);
ui->submitEvidenceButton->setVisible(false);
ui->gridLayout->removeWidget(ui->submitEvidenceButton);
wireUi();
}
EvidenceManager::~EvidenceManager() {
delete ui;
delete evidenceEditor;
delete filterForm;
delete submitButton;
stopReply(&uploadAssetReply);
}
void EvidenceManager::closeEvent(QCloseEvent* event) {
QDialog::closeEvent(event);
evidenceEditor->updateEvidence(-1, true);
}
void EvidenceManager::showEvent(QShowEvent* evt) {
QDialog::showEvent(evt);
resetFilterButtonClicked();
}
void EvidenceManager::wireUi() {
auto btnClicked = &QPushButton::clicked;
connect(submitButton, btnClicked, this, &EvidenceManager::submitEvidenceButtonClicked);
connect(ui->deleteEvidenceButton, btnClicked, this,
&EvidenceManager::deleteEvidenceButtonClicked);
connect(ui->applyFilterButton, btnClicked, this, &EvidenceManager::applyFilterButtonClicked);
connect(ui->resetFilterButton, btnClicked, this, &EvidenceManager::resetFilterButtonClicked);
connect(ui->editFiltersButton, btnClicked, this, &EvidenceManager::openFiltersMenu);
connect(ui->filterTextBox, &QLineEdit::returnPressed, this,
&EvidenceManager::applyFilterButtonClicked);
connect(ui->closeFormButton, btnClicked, this, &EvidenceManager::close);
connect(filterForm, &EvidenceFilterForm::evidenceSet, this, &EvidenceManager::applyFilterForm);
connect(ui->evidenceTable, &QTableWidget::currentCellChanged, this,
&EvidenceManager::onRowChanged);
connect(this, &EvidenceManager::evidenceChanged, evidenceEditor, &EvidenceEditor::updateEvidence);
}
void EvidenceManager::submitEvidenceButtonClicked() {
submitButton->startAnimation();
setActionButtonsEnabled(false);
if (saveData()) {
evidenceIDForRequest = selectedRowEvidenceID();
try {
model::Evidence evi = db->getEvidenceDetails(evidenceIDForRequest);
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
connect(uploadAssetReply, &QNetworkReply::finished, this, &EvidenceManager::onUploadComplete);
}
catch (QSqlError& e) {
QMessageBox::warning(this, "Cannot submit evidence",
"Could not retrieve data. Please try again.");
}
}
}
void EvidenceManager::deleteEvidenceButtonClicked() {
auto reply = QMessageBox::question(this, "Discard Evidence",
"Are you sure you want to discard this evidence? This will "
"only delete this evidence on your computer.",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply == QMessageBox::Yes) {
auto deleteResp = evidenceEditor->deleteEvidence();
auto evi = deleteResp.model;
if (!deleteResp.dbDeleteSuccess) {
std::cout << "Could not delete evidence from internal database. Error: "
<< deleteResp.errorText.toStdString() << std::endl;
}
else if (!deleteResp.fileDeleteSuccess) {
QMessageBox::warning(this, "Could not delete",
"Unable to delete evidence file.\n"
"You can try deleting the file directly. File Location:\n" +
evi.path);
}
else {
loadEvidence();
}
ui->closeFormButton->setEnabled(true);
}
}
void EvidenceManager::applyFilterButtonClicked() { loadEvidence(); }
void EvidenceManager::resetFilterButtonClicked() {
EvidenceFilters filter;
filter.operationSlug = AppSettings::getInstance().operationSlug();
ui->filterTextBox->setText(filter.toString());
loadEvidence();
}
void EvidenceManager::applyFilterForm(const EvidenceFilters& filter) {
ui->filterTextBox->setText(filter.toString());
applyFilterButtonClicked();
}
void EvidenceManager::loadEvidence() {
enableEvidenceButtons(false);
ui->evidenceTable->clearContents();
try {
auto filter = EvidenceFilters::parseFilter(ui->filterTextBox->text());
std::vector<model::Evidence> operationEvidence = db->getEvidenceWithFilters(filter);
ui->evidenceTable->setRowCount(operationEvidence.size());
// removing sorting temporarily to solve a bug (per qt: not a bug)
// Essentially, _not_ doing this breaks reloading the table. Mostly empty cells appear.
// from: https://stackoverflow.com/a/8904287/4262552
// see also: https://bugreports.qt.io/browse/QTBUG-75479
ui->evidenceTable->setSortingEnabled(false);
for (size_t row = 0; row < operationEvidence.size(); row++) {
auto evi = operationEvidence.at(row);
auto rowData = buildBaseEvidenceRow(evi.id);
ui->evidenceTable->setItem(row, COL_OPERATION, rowData.operation);
ui->evidenceTable->setItem(row, COL_DESCRIPTION, rowData.description);
ui->evidenceTable->setItem(row, COL_CONTENT_TYPE, rowData.contentType);
ui->evidenceTable->setItem(row, COL_DATE_CAPTURED, rowData.dateCaptured);
ui->evidenceTable->setItem(row, COL_PATH, rowData.path);
ui->evidenceTable->setItem(row, COL_FAILED, rowData.failed);
ui->evidenceTable->setItem(row, COL_ERROR_MSG, rowData.errorText);
ui->evidenceTable->setItem(row, COL_SUBMITTED, rowData.submitted);
ui->evidenceTable->setItem(row, COL_DATE_SUBMITTED, rowData.dateSubmitted);
setRowText(row, evi);
}
ui->evidenceTable->setSortingEnabled(true);
}
catch (QSqlError& e) {
std::cout << "Could not retrieve evidence for operation. Error: " << e.text().toStdString()
<< std::endl;
}
}
// buildBaseEvidenceRow constructs a container for a row of data.
// Note: the row (container) is on the stack, but items in the container
// are on the heap, and must be deleted.
EvidenceRow EvidenceManager::buildBaseEvidenceRow(qint64 evidenceID) {
EvidenceRow row{};
auto basicItem = [evidenceID]() -> QTableWidgetItem* {
auto item = new QTableWidgetItem();
item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
item->setData(Qt::UserRole, evidenceID);
return item;
};
row.dateCaptured = basicItem();
row.description = basicItem();
row.operation = basicItem();
row.contentType = basicItem();
row.path = basicItem();
row.errorText = basicItem();
row.dateSubmitted = basicItem();
row.submitted = basicItem();
row.submitted->setTextAlignment(Qt::AlignCenter);
row.failed = basicItem();
row.failed->setTextAlignment(Qt::AlignCenter);
return row;
}
void EvidenceManager::setRowText(int row, const model::Evidence& model) {
static QString dateFormat = "MMM dd, yyyy hh:mm";
ui->evidenceTable->item(row, COL_DATE_CAPTURED)->setText(model.recordedDate.toString(dateFormat));
ui->evidenceTable->item(row, COL_DESCRIPTION)->setText(model.description);
ui->evidenceTable->item(row, COL_OPERATION)->setText(model.operationSlug);
ui->evidenceTable->item(row, COL_CONTENT_TYPE)->setText(model.contentType);
ui->evidenceTable->item(row, COL_SUBMITTED)->setText(model.uploadDate.isNull() ? "No" : "Yes");
ui->evidenceTable->item(row, COL_FAILED)->setText((model.errorText == "") ? "" : "Yes");
ui->evidenceTable->item(row, COL_PATH)->setText(model.path);
ui->evidenceTable->item(row, COL_ERROR_MSG)->setText(model.errorText);
auto uploadDateText = model.uploadDate.isNull() ? "Never" : model.uploadDate.toString(dateFormat);
ui->evidenceTable->item(row, COL_DATE_SUBMITTED)->setText(uploadDateText);
}
void EvidenceManager::refreshRow(int row) {
auto evidenceID = selectedRowEvidenceID();
try {
auto updatedData = db->getEvidenceDetails(evidenceID);
setRowText(row, updatedData);
}
catch (QSqlError& e) {
std::cout << "Could not refresh table row: " << e.text().toStdString() << std::endl;
}
}
void EvidenceManager::setActionButtonsEnabled(bool enabled) {
enableEvidenceButtons(enabled);
ui->closeFormButton->setEnabled(enabled);
}
void EvidenceManager::enableEvidenceButtons(bool enabled) {
submitButton->setEnabled(enabled);
ui->deleteEvidenceButton->setEnabled(enabled);
}
bool EvidenceManager::saveData() {
auto saveResponse = evidenceEditor->saveEvidence();
if (saveResponse.actionSucceeded) {
refreshRow(ui->evidenceTable->currentRow());
return true;
}
QMessageBox::warning(this, "Cannot Save",
"Unable to save evidence data.\n"
"You can try uploading directly to the website. File Location:\n" +
saveResponse.model.path);
return false;
}
void EvidenceManager::openFiltersMenu() {
filterForm->setForm(EvidenceFilters::parseFilter(ui->filterTextBox->text()));
filterForm->open();
}
void EvidenceManager::onRowChanged(int currentRow, int _currentColumn, int _previousRow,
int _previousColumn) {
Q_UNUSED(_currentColumn);
Q_UNUSED(_previousRow);
Q_UNUSED(_previousColumn);
if (currentRow == -1) {
enableEvidenceButtons(false);
emit evidenceChanged(-1, true);
return;
}
auto evidence = db->getEvidenceDetails(selectedRowEvidenceID());
enableEvidenceButtons(true);
auto readonly = evidence.uploadDate.isValid();
submitButton->setEnabled(!readonly);
emit evidenceChanged(evidence.id, true);
}
void EvidenceManager::onUploadComplete() {
bool isValid;
NetMan::extractResponse(uploadAssetReply, isValid);
if (!isValid) {
auto errMessage =
"Unable to upload evidence: Network error (" + uploadAssetReply->errorString() + ")";
try {
db->updateEvidenceError(errMessage, evidenceIDForRequest);
}
catch (QSqlError& e) {
std::cout << "Upload failed. Could not update internal database. Error: "
<< e.text().toStdString() << std::endl;
}
QMessageBox::warning(this, "Cannot Submit Evidence",
"Upload failed: Network error. Check your connection and try again.\n"
"(Error: " + uploadAssetReply->errorString() + ")");
}
else {
try {
db->updateEvidenceSubmitted(evidenceIDForRequest);
}
catch (QSqlError& e) {
std::cout << "Upload successful. Could not update internal database. Error: "
<< e.text().toStdString() << std::endl;
}
emit evidenceChanged(evidenceIDForRequest, true); // lock the editing form
}
refreshRow(ui->evidenceTable->currentRow());
// we don't actually need anything from the uploadAssets reply, so just clean it up.
// one thing we might want to record: evidence uuid... not sure why we'd need it though.
submitButton->stopAnimation();
ui->closeFormButton->setEnabled(true);
tidyReply(&uploadAssetReply);
}
qint64 EvidenceManager::selectedRowEvidenceID() {
return ui->evidenceTable->currentItem()->data(Qt::UserRole).toLongLong();
}

View File

@ -0,0 +1,83 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef EVIDENCEMANAGER_H
#define EVIDENCEMANAGER_H
#include <QDialog>
#include <QNetworkReply>
#include <QTableWidgetItem>
#include "components/evidence_editor/evidenceeditor.h"
#include "components/loading_button/loadingbutton.h"
#include "db/databaseconnection.h"
#include "forms/evidence_filter/evidencefilterform.h"
namespace Ui {
class EvidenceManager;
}
// QTableWidget should memory-manage this data.
struct EvidenceRow {
QTableWidgetItem* dateCaptured;
QTableWidgetItem* description;
QTableWidgetItem* contentType;
QTableWidgetItem* operation;
QTableWidgetItem* submitted;
QTableWidgetItem* failed;
QTableWidgetItem* path;
QTableWidgetItem* errorText;
QTableWidgetItem* dateSubmitted;
};
class EvidenceManager : public QDialog {
Q_OBJECT
public:
explicit EvidenceManager(DatabaseConnection* db, QWidget* parent = nullptr);
~EvidenceManager();
protected:
void closeEvent(QCloseEvent* event) override;
private:
void wireUi();
bool saveData();
void loadEvidence();
void setActionButtonsEnabled(bool enabled);
EvidenceRow buildBaseEvidenceRow(qint64 evidenceID);
void refreshRow(int row);
void setRowText(int row, const model::Evidence& model);
void enableEvidenceButtons(bool enable);
void showEvent(QShowEvent* evt) override;
qint64 selectedRowEvidenceID();
signals:
void evidenceChanged(quint64 evidenceID, bool readonly);
private slots:
void submitEvidenceButtonClicked();
void deleteEvidenceButtonClicked();
void applyFilterButtonClicked();
void resetFilterButtonClicked();
void applyFilterForm(const EvidenceFilters& filter);
void openFiltersMenu();
void onRowChanged(int currentRow, int currentColumn, int previousRow, int previousColumn);
void onUploadComplete();
private:
Ui::EvidenceManager* ui;
EvidenceEditor* evidenceEditor;
EvidenceFilterForm* filterForm;
LoadingButton* submitButton;
DatabaseConnection* db;
QNetworkReply* uploadAssetReply = nullptr;
qint64 evidenceIDForRequest;
};
#endif // EVIDENCEMANAGER_H

View File

@ -0,0 +1,209 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EvidenceManager</class>
<widget class="QDialog" name="EvidenceManager">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>8</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>Evidence Manager</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="5" column="1">
<widget class="QPushButton" name="deleteEvidenceButton">
<property name="text">
<string>Delete</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2" colspan="2">
<widget class="QLineEdit" name="filterTextBox"/>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="applyFilterButton">
<property name="text">
<string>Apply</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="6" column="5">
<widget class="QPushButton" name="closeFormButton">
<property name="text">
<string>Close</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="5">
<widget class="QPushButton" name="submitEvidenceButton">
<property name="text">
<string>Submit</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QPushButton" name="resetFilterButton">
<property name="text">
<string>Reset</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="editFiltersButton">
<property name="text">
<string>Edit Filters</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="1" colspan="5">
<widget class="QLabel" name="_evidenceEditorPlaceholder">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>_evidenceEditorPlaceholder</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="5">
<widget class="QTableWidget" name="evidenceTable">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string>Date Captured</string>
</property>
</column>
<column>
<property name="text">
<string>Operation</string>
</property>
</column>
<column>
<property name="text">
<string>Path</string>
</property>
</column>
<column>
<property name="text">
<string>Content Type</string>
</property>
</column>
<column>
<property name="text">
<string>Description</string>
</property>
</column>
<column>
<property name="text">
<string>Submitted</string>
</property>
</column>
<column>
<property name="text">
<string>Date Submitted</string>
</property>
</column>
<column>
<property name="text">
<string>Failed</string>
</property>
</column>
<column>
<property name="text">
<string>Error</string>
</property>
</column>
</widget>
</item>
<item row="5" column="2">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>
<tabstop>editFiltersButton</tabstop>
<tabstop>filterTextBox</tabstop>
<tabstop>evidenceTable</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,198 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "evidencefilter.h"
EvidenceFilters::EvidenceFilters() = default;
QString EvidenceFilters::standardizeFilterKey(QString key) {
if (FILTER_KEYS_ERROR.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_ERROR;
}
if (FILTER_KEYS_SUBMITTED.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_SUBMITTED;
}
if (FILTER_KEYS_TO.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_TO;
}
if (FILTER_KEYS_FROM.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_FROM;
}
if (FILTER_KEYS_ON.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_ON;
}
if (FILTER_KEYS_OPERATION.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_OPERATION;
}
if (FILTER_KEYS_CONTENT_TYPE.contains(key, Qt::CaseInsensitive)) {
return FILTER_KEY_CONTENT_TYPE;
}
return key;
}
QString EvidenceFilters::toString() const {
// helper functions
static auto toCommonDate = [](QDate d) -> QString {
auto today = QDateTime::currentDateTime().date();
if (d == today) {
return "Today";
}
if (d == today.addDays(-1)) {
return "Yesterday";
}
return d.toString("yyyy-MM-dd");
};
static auto triToText = [](Tri t) -> QString { return t == Yes ? "yes" : "no"; };
QString rtn = "";
if (!operationSlug.isEmpty()) {
rtn.append(" " + FILTER_KEY_OPERATION + ": " + operationSlug);
}
if (!contentType.isEmpty()) {
rtn.append(" " + FILTER_KEY_CONTENT_TYPE + ": " + contentType);
}
if (hasError != Any) {
rtn.append(" " + FILTER_KEY_ERROR + ": " + triToText(hasError));
}
if (startDate.isValid() || endDate.isValid()) {
if (startDate == endDate) {
rtn.append(" " + FILTER_KEY_ON + ": " + toCommonDate(startDate));
}
else {
if (startDate.isValid()) {
rtn.append(" " + FILTER_KEY_FROM + ": " + toCommonDate(startDate));
}
if (endDate.isValid()) {
rtn.append(" " + FILTER_KEY_TO + ": " + toCommonDate(endDate));
}
}
}
if (submitted != Any) {
rtn.append(" " + FILTER_KEY_SUBMITTED + ": " + triToText(submitted));
}
return rtn.trimmed();
}
EvidenceFilters EvidenceFilters::parseFilter(const QString& text) {
EvidenceFilters filter;
if (text.trimmed().isEmpty()) {
return filter;
}
auto tokenizedFilter = tokenizeFilterText(text);
for (const auto& item : tokenizedFilter) {
QString key = EvidenceFilters::standardizeFilterKey(item.first.toLower().trimmed());
QString value = item.second.trimmed();
if (key == FILTER_KEY_ERROR) {
auto val = value.toLower();
filter.hasError = parseTriFilterValue(val);
}
else if (key == FILTER_KEY_SUBMITTED) {
auto val = value.toLower();
filter.submitted = parseTriFilterValue(val);
}
else if (key == FILTER_KEY_OPERATION) {
filter.operationSlug = value;
}
else if (key == FILTER_KEY_TO) {
filter.endDate = parseDateString(value);
}
else if (key == FILTER_KEY_FROM) {
filter.startDate = parseDateString(value);
}
else if (key == FILTER_KEY_ON) {
auto formattedValue = parseDateString(value);
filter.startDate = formattedValue;
filter.endDate = formattedValue;
}
else if (key == FILTER_KEY_CONTENT_TYPE) {
filter.contentType = value;
}
}
return filter;
}
// parseTriFilterValue returns a Tri object given a string. If the given string is "t" or "y"
// then Tri::Yes will be returned. Otherwise, in non-strict mode, Tri::No will be returned.
// In strict mode, Tri::No will be returned only if it starts with "f" or "n", otherwise Tri::Any
// is returned.
Tri EvidenceFilters::parseTriFilterValue(const QString& text, bool strict) {
auto val = text.toLower().trimmed();
if (val.startsWith("t") || val.startsWith("y")) {
return Tri::Yes;
}
if (strict) {
return (val.startsWith("f") || val.startsWith("n")) ? Tri::No : Tri::Any;
}
return Tri::No;
}
std::vector<std::pair<QString, QString>> EvidenceFilters::tokenizeFilterText(const QString& text) {
QStringList list = text.split(":", QString::SplitBehavior::SkipEmptyParts);
// now in: [Key][value key]...[value] format
QStringList keys;
QStringList values;
keys.append(list.first());
for (int i = 1; i < list.size() - 1; i++) {
auto valueKeyPair = list.at(i).split(" ", QString::SplitBehavior::SkipEmptyParts);
keys.append(valueKeyPair.last());
valueKeyPair.removeLast();
values.append(valueKeyPair.join(" "));
}
values.append(list.last());
std::vector<std::pair<QString, QString>> rtn;
for (int i = 0; i < keys.length(); i++) {
auto keyvalue = std::pair<QString, QString>(keys.at(i), values.at(i));
rtn.emplace_back(keyvalue);
}
return rtn;
}
QDate EvidenceFilters::parseDateString(QString text) {
static QString DATE_FORMAT = "yyyy-MM-dd";
text = text.toLower();
if (text == "today") {
text = QDateTime::currentDateTime().toString(DATE_FORMAT);
}
else if (text == "yesterday") {
text = QDateTime::currentDateTime().addDays(-1).toString(DATE_FORMAT);
}
// other thoughts: this week (hard -- spans years), this month, this year, last ${period}
return QDate::fromString(text, DATE_FORMAT);
}
// parseTri returns Tri::Yes if the given text is exactly "Yes", Tri::No if the text is exactly "No"
// otherwise Tri::Any.
// This is the inverse of triToString
Tri EvidenceFilters::parseTri(const QString& text) {
if (text == "Yes") {
return Yes;
}
if (text == "No") {
return No;
}
return Any;
}
// triToString returns "Yes" for Tri::Yes, "No" for Tri::No, otherwise "Any"
// This is the inverse to parseTri
QString EvidenceFilters::triToString(const Tri& tri) {
switch (tri) {
case Yes:
return "Yes";
case No:
return "No";
default:
return "Any";
}
}

View File

@ -0,0 +1,58 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef EVIDENCEFILTER_H
#define EVIDENCEFILTER_H
#include <QDate>
#include <QString>
#include <utility>
#include <vector>
enum Tri { Any, Yes, No };
// These represent the standard key for a filter
const QString FILTER_KEY_ERROR = "err";
const QString FILTER_KEY_SUBMITTED = "submitted";
const QString FILTER_KEY_TO = "to";
const QString FILTER_KEY_FROM = "from";
const QString FILTER_KEY_ON = "on";
const QString FILTER_KEY_OPERATION = "op";
const QString FILTER_KEY_CONTENT_TYPE = "type";
// These represent aliases for standard key for a filter
const QStringList FILTER_KEYS_ERROR = {FILTER_KEY_ERROR, "error", "failed", "fail"};
const QStringList FILTER_KEYS_SUBMITTED = {FILTER_KEY_SUBMITTED};
const QStringList FILTER_KEYS_TO = {FILTER_KEY_TO, "before", "til", "until"};
const QStringList FILTER_KEYS_FROM = {FILTER_KEY_FROM, "after"};
const QStringList FILTER_KEYS_ON = {FILTER_KEY_ON};
const QStringList FILTER_KEYS_OPERATION = {FILTER_KEY_OPERATION, "operation"};
const QStringList FILTER_KEYS_CONTENT_TYPE = {FILTER_KEY_CONTENT_TYPE, "contentType"};
class EvidenceFilters {
public:
EvidenceFilters();
static QString standardizeFilterKey(QString key);
QString toString() const;
static EvidenceFilters parseFilter(const QString &text);
public:
QString operationSlug = "";
QString contentType = "";
Tri hasError = Any;
Tri submitted = Any;
QDate startDate = QDate();
QDate endDate = QDate();
public:
static Tri parseTri(const QString &text);
static QString triToString(const Tri &tri);
private:
static std::vector<std::pair<QString, QString>> tokenizeFilterText(const QString &text);
static QDate parseDateString(QString text);
static Tri parseTriFilterValue(const QString &text, bool strict = false);
};
#endif // EVIDENCEFILTER_H

View File

@ -0,0 +1,129 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "evidencefilterform.h"
#include "appsettings.h"
#include "helpers/netman.h"
#include "helpers/ui_helpers.h"
#include "ui_evidencefilterform.h"
static void initializeTriCombobox(QComboBox *box) {
box->clear();
box->addItem("Any");
box->addItem("Yes");
box->addItem("No");
}
static void initializeDateEdit(QDateEdit *dateEdit) {
dateEdit->setDate(QDateTime::currentDateTime().date());
dateEdit->setDisplayFormat("MMM dd, yyyy");
dateEdit->setDateRange(QDate(2000, 01, 01), QDateTime::currentDateTime().date());
dateEdit->setEnabled(false);
}
EvidenceFilterForm::EvidenceFilterForm(QWidget *parent)
: QDialog(parent), ui(new Ui::EvidenceFilterForm) {
ui->setupUi(this);
ui->erroredComboBox->setEditable(false);
ui->operationComboBox->setEditable(false);
ui->submittedComboBox->setEditable(false);
ui->contentTypeComboBox->setEditable(false);
initializeTriCombobox(ui->submittedComboBox);
initializeTriCombobox(ui->erroredComboBox);
ui->contentTypeComboBox->addItem("<None>", "");
ui->contentTypeComboBox->addItem("Image", "image");
ui->contentTypeComboBox->addItem("Codeblock", "codeblock");
initializeDateEdit(ui->fromDateEdit);
initializeDateEdit(ui->toDateEdit);
wireUi();
}
EvidenceFilterForm::~EvidenceFilterForm() { delete ui; }
void EvidenceFilterForm::wireUi() {
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
&EvidenceFilterForm::onOperationListUpdated);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &EvidenceFilterForm::writeForm);
connect(ui->includeStartDateCheckBox, &QCheckBox::stateChanged,
[this](bool checked) { ui->fromDateEdit->setEnabled(checked); });
connect(ui->includeEndDateCheckBox, &QCheckBox::stateChanged,
[this](bool checked) { ui->toDateEdit->setEnabled(checked); });
}
void EvidenceFilterForm::writeForm() {
auto filter = encodeForm();
emit evidenceSet(filter);
}
EvidenceFilters EvidenceFilterForm::encodeForm() {
EvidenceFilters filter;
filter.hasError = EvidenceFilters::parseTri(ui->erroredComboBox->currentText());
filter.submitted = EvidenceFilters::parseTri(ui->submittedComboBox->currentText());
filter.operationSlug = ui->operationComboBox->currentData().toString();
filter.contentType = ui->contentTypeComboBox->currentData().toString();
// swap dates so smaller date is always "from" / after
if (ui->fromDateEdit->isEnabled() && ui->toDateEdit->isEnabled() &&
ui->fromDateEdit->date() > ui->toDateEdit->date()) {
auto copy = ui->fromDateEdit->date();
ui->fromDateEdit->setDate(ui->toDateEdit->date());
ui->toDateEdit->setDate(copy);
}
if (ui->includeStartDateCheckBox->isChecked()) {
filter.startDate = ui->fromDateEdit->date();
}
if (ui->includeEndDateCheckBox->isChecked()) {
filter.endDate = ui->toDateEdit->date();
}
return filter;
}
void EvidenceFilterForm::setForm(const EvidenceFilters &model) {
UiHelpers::setComboBoxValue(ui->operationComboBox, model.operationSlug);
UiHelpers::setComboBoxValue(ui->contentTypeComboBox, model.contentType);
ui->erroredComboBox->setCurrentText(EvidenceFilters::triToString(model.hasError));
ui->submittedComboBox->setCurrentText(EvidenceFilters::triToString(model.submitted));
ui->includeStartDateCheckBox->setChecked(model.startDate.isValid());
ui->fromDateEdit->setDate(model.startDate.isValid() ? model.startDate
: QDateTime::currentDateTime().date());
ui->includeEndDateCheckBox->setChecked(model.endDate.isValid());
ui->toDateEdit->setDate(model.endDate.isValid() ? model.endDate
: QDateTime::currentDateTime().date());
// swap dates so smaller date is always "from" / after
if (model.startDate.isValid() && model.endDate.isValid() &&
ui->fromDateEdit->date() > ui->toDateEdit->date()) {
auto copy = ui->fromDateEdit->date();
ui->fromDateEdit->setDate(ui->toDateEdit->date());
ui->toDateEdit->setDate(copy);
}
}
void EvidenceFilterForm::onOperationListUpdated(bool success,
const std::vector<dto::Operation> &operations) {
ui->operationComboBox->setEnabled(false);
if (!success) {
ui->operationComboBox->setItemText(0, "Unable to fetch operations");
ui->operationComboBox->setCurrentIndex(0);
return;
}
ui->operationComboBox->clear();
ui->operationComboBox->addItem("<None>", "");
for (const auto &op : operations) {
ui->operationComboBox->addItem(op.name, op.slug);
}
UiHelpers::setComboBoxValue(ui->operationComboBox, AppSettings::getInstance().operationSlug());
ui->operationComboBox->setEnabled(true);
}

View File

@ -0,0 +1,42 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef EVIDENCEFILTERFORM_H
#define EVIDENCEFILTERFORM_H
#include <QComboBox>
#include <QDialog>
#include "src/db/databaseconnection.h"
#include "src/dtos/operation.h"
namespace Ui {
class EvidenceFilterForm;
}
class EvidenceFilterForm : public QDialog {
Q_OBJECT
public:
explicit EvidenceFilterForm(QWidget *parent = nullptr);
~EvidenceFilterForm();
private:
void wireUi();
void writeForm();
public:
void setForm(const EvidenceFilters &model);
signals:
void evidenceSet(EvidenceFilters filter);
public slots:
void onOperationListUpdated(bool success, const std::vector<dto::Operation> &operations);
EvidenceFilters encodeForm();
private:
Ui::EvidenceFilterForm *ui;
};
#endif // EVIDENCEFILTERFORM_H

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EvidenceFilterForm</class>
<widget class="QDialog" name="EvidenceFilterForm">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>320</width>
<height>240</height>
</rect>
</property>
<property name="windowTitle">
<string>Evidence Editor</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="1">
<widget class="QLabel" name="_hadErrorLabel">
<property name="text">
<string>Had Error</string>
</property>
</widget>
</item>
<item row="6" column="1" colspan="4">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="_operationLabel">
<property name="text">
<string>Operation</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="_toDateLabel">
<property name="text">
<string>To Date</string>
</property>
</widget>
</item>
<item row="5" column="2" colspan="2">
<widget class="QDateEdit" name="toDateEdit"/>
</item>
<item row="4" column="2" colspan="2">
<widget class="QDateEdit" name="fromDateEdit"/>
</item>
<item row="4" column="1">
<widget class="QLabel" name="_fromDateLabel">
<property name="text">
<string>From Date</string>
</property>
</widget>
</item>
<item row="5" column="4">
<widget class="QCheckBox" name="includeEndDateCheckBox">
<property name="text">
<string>Include</string>
</property>
</widget>
</item>
<item row="4" column="4">
<widget class="QCheckBox" name="includeStartDateCheckBox">
<property name="text">
<string>Include</string>
</property>
</widget>
</item>
<item row="3" column="2" colspan="3">
<widget class="QComboBox" name="submittedComboBox"/>
</item>
<item row="2" column="2" colspan="3">
<widget class="QComboBox" name="erroredComboBox"/>
</item>
<item row="0" column="2" colspan="3">
<widget class="QComboBox" name="operationComboBox">
<property name="enabled">
<bool>false</bool>
</property>
<item>
<property name="text">
<string>Loading...</string>
</property>
</item>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="_wasSubmittedLabel">
<property name="text">
<string>Was Submitted</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>Content Type</string>
</property>
</widget>
</item>
<item row="1" column="2" colspan="3">
<widget class="QComboBox" name="contentTypeComboBox"/>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>EvidenceFilterForm</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>EvidenceFilterForm</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,146 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "getinfo.h"
#include <QMessageBox>
#include "appsettings.h"
#include "components/evidence_editor/evidenceeditor.h"
#include "helpers/netman.h"
#include "helpers/stopreply.h"
#include "helpers/ui_helpers.h"
#include "ui_getinfo.h"
GetInfo::GetInfo(DatabaseConnection* db, qint64 evidenceID, QWidget* parent)
: QDialog(parent), ui(new Ui::GetInfo) {
ui->setupUi(this);
this->db = db;
this->evidenceID = evidenceID;
this->setAttribute(Qt::WA_DeleteOnClose);
evidenceEditor = new EvidenceEditor(evidenceID, db, this);
evidenceEditor->setEnabled(true);
loadingButton = new LoadingButton(ui->submitButton->text(), this);
UiHelpers::replacePlaceholder(ui->evidenceEditorPlaceholder, evidenceEditor, ui->gridLayout);
UiHelpers::replacePlaceholder(ui->submitButton, loadingButton, ui->gridLayout);
wireUi();
// Make the dialog pop up above any other windows but retain title bar and buttons
Qt::WindowFlags flags = this->windowFlags();
flags |= Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;
this->setWindowFlags(flags);
}
GetInfo::~GetInfo() {
delete ui;
delete evidenceEditor;
delete loadingButton;
stopReply(&uploadAssetReply);
}
void GetInfo::wireUi() {
connect(loadingButton, &QPushButton::clicked, this, &GetInfo::submitButtonClicked);
connect(ui->deleteButton, &QPushButton::clicked, this, &GetInfo::deleteButtonClicked);
}
bool GetInfo::saveData() {
auto saveResponse = evidenceEditor->saveEvidence();
if (!saveResponse.actionSucceeded) {
QMessageBox::warning(this, "Cannot Save",
"Unable to save evidence data.\n"
"You can try uploading directly to the website. File Location:\n" +
saveResponse.model.path);
}
return saveResponse.actionSucceeded;
}
void GetInfo::submitButtonClicked() {
loadingButton->startAnimation();
setActionButtonsEnabled(false);
if (saveData()) {
try {
model::Evidence evi = db->getEvidenceDetails(evidenceID);
uploadAssetReply = NetMan::getInstance().uploadAsset(evi);
connect(uploadAssetReply, &QNetworkReply::finished, this, &GetInfo::onUploadComplete);
}
catch (QSqlError& e) {
QMessageBox::warning(this, "Cannot submit evidence",
"Could not retrieve data. Please try again.");
}
}
}
void GetInfo::deleteButtonClicked() {
auto reply = QMessageBox::question(this, "Discard Evidence",
"Are you sure you want to discard this evidence?",
QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
if (reply == QMessageBox::Yes) {
setActionButtonsEnabled(false);
bool shouldClose = true;
model::Evidence evi = evidenceEditor->encodeEvidence();
if (!QFile::remove(evi.path)) {
QMessageBox::warning(this, "Could not delete",
"Unable to delete evidence file.\n"
"You can try deleting the file directly. File Location:\n" +
evi.path);
shouldClose = false;
}
try {
db->deleteEvidence(evidenceID);
}
catch (QSqlError& e) {
std::cout << "Could not delete evidence from internal database. Error: "
<< e.text().toStdString() << std::endl;
}
setActionButtonsEnabled(true);
if (shouldClose) {
this->close();
}
}
}
void GetInfo::setActionButtonsEnabled(bool enabled) {
loadingButton->setEnabled(enabled);
ui->deleteButton->setEnabled(enabled);
}
void GetInfo::onUploadComplete() {
if (uploadAssetReply->error() != QNetworkReply::NoError) {
auto errMessage =
"Unable to upload evidence: Network error (" + uploadAssetReply->errorString() + ")";
try {
db->updateEvidenceError(errMessage, this->evidenceID);
}
catch (QSqlError& e) {
std::cout << "Upload failed. Could not update internal database. Error: "
<< e.text().toStdString() << std::endl;
}
QMessageBox::warning(this, "Cannot submit evidence",
"Upload failed: Network error. Check your connection and try again.\n"
"Note: This evidence has been saved. You can close this window and "
"re-submit from the evidence manager."
"\n(Error: " +
uploadAssetReply->errorString() + ")");
}
else {
try {
db->updateEvidenceSubmitted(this->evidenceID);
this->close();
}
catch (QSqlError& e) {
std::cout << "Upload successful. Could not update internal database. Error: "
<< e.text().toStdString() << std::endl;
}
}
// we don't actually need anything from the uploadAssets reply, so just clean it up.
// one thing we might want to record: evidence uuid... not sure why we'd need it though.
loadingButton->stopAnimation();
setActionButtonsEnabled(true);
tidyReply(&uploadAssetReply);
}

View File

@ -0,0 +1,49 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef GETINFO_H
#define GETINFO_H
#include <QDialog>
#include <QNetworkReply>
#include "components/evidence_editor/evidenceeditor.h"
#include "components/loading/qprogressindicator.h"
#include "components/loading_button/loadingbutton.h"
#include "db/databaseconnection.h"
#include "dtos/tag.h"
namespace Ui {
class GetInfo;
}
class GetInfo : public QDialog {
Q_OBJECT
public:
explicit GetInfo(DatabaseConnection *db, qint64 evidenceID, QWidget *parent = nullptr);
~GetInfo();
private:
void wireUi();
bool saveData();
void setActionButtonsEnabled(bool enabled);
private slots:
void submitButtonClicked();
void deleteButtonClicked();
void onUploadComplete();
private:
Ui::GetInfo *ui;
DatabaseConnection *db;
qint64 evidenceID;
EvidenceEditor *evidenceEditor;
QNetworkReply *uploadAssetReply = nullptr;
LoadingButton *loadingButton;
};
#endif // GETINFO_H

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GetInfo</class>
<widget class="QDialog" name="GetInfo">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>640</width>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
<string>Add Evidence Details</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="1">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1" colspan="2">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="3">
<widget class="QPushButton" name="submitButton">
<property name="text">
<string>Submit</string>
</property>
</widget>
</item>
<item row="3" column="0" alignment="Qt::AlignLeft">
<widget class="QPushButton" name="deleteButton">
<property name="text">
<string>Delete</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<widget class="QLabel" name="evidenceEditorPlaceholder">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,158 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "settings.h"
#include <QDateTime>
#include <QFileDialog>
#include <QString>
#include "appconfig.h"
#include "appsettings.h"
#include "helpers/http_status.h"
#include "helpers/netman.h"
#include "helpers/pathseparator.h"
#include "helpers/stopreply.h"
#include "helpers/ui_helpers.h"
#include "hotkeymanager.h"
#include "ui_settings.h"
using namespace std;
Settings::Settings(HotkeyManager *hotkeyManager, QWidget *parent)
: ButtonBoxForm(parent), ui(new Ui::Settings) {
ui->setupUi(this);
this->hotkeyManager = hotkeyManager;
couldNotSaveSettingsMsg = new QErrorMessage(this);
setButtonBox(ui->buttonBox);
testConnectionButton = new LoadingButton(ui->testHostButton->text(), this, ui->testHostButton);
UiHelpers::replacePlaceholder(ui->testHostButton, testConnectionButton, ui->gridLayout);
wireUi();
// Make the dialog pop up above any other windows but retain title bar and buttons
Qt::WindowFlags flags = this->windowFlags();
flags |= Qt::CustomizeWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint;
this->setWindowFlags(flags);
}
Settings::~Settings() {
delete ui;
delete couldNotSaveSettingsMsg;
stopReply(&currentTestReply);
delete testConnectionButton;
}
void Settings::wireUi() {
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &Settings::routeButtonPress);
connect(testConnectionButton, &QPushButton::clicked, this, &Settings::onTestConnectionClicked);
connect(ui->eviRepoBrowseButton, &QPushButton::clicked, this, &Settings::onBrowseClicked);
}
void Settings::showEvent(QShowEvent *evt) {
QDialog::showEvent(evt);
AppConfig &inst = AppConfig::getInstance();
// reset the form in case a user left junk in the text boxes and pressed "cancel"
ui->eviRepoTextBox->setText(inst.evidenceRepo);
ui->accessKeyTextBox->setText(inst.accessKey);
ui->secretKeyTextBox->setText(inst.secretKey);
ui->hostPathTextBox->setText(inst.apiURL);
ui->screenshotCmdTextBox->setText(inst.screenshotExec);
ui->screenshotShortcutTextBox->setText(inst.screenshotShortcutCombo);
ui->captureWindowCmdTextBox->setText(inst.captureWindowExec);
ui->captureWindowShortCutTextBox->setText(inst.captureWindowShortcut);
// re-enable form
testConnectionButton->setEnabled(true);
}
void Settings::closeEvent(QCloseEvent *event) {
onSaveClicked();
QDialog::closeEvent(event);
}
void Settings::onCancelClicked() {
stopReply(&currentTestReply);
ui->statusIconLabel->setText("");
}
void Settings::onSaveClicked() {
stopReply(&currentTestReply);
ui->statusIconLabel->setText("");
AppConfig &inst = AppConfig::getInstance();
inst.evidenceRepo = ui->eviRepoTextBox->text();
inst.accessKey = ui->accessKeyTextBox->text();
inst.secretKey = ui->secretKeyTextBox->text();
inst.apiURL = ui->hostPathTextBox->text();
inst.screenshotExec = ui->screenshotCmdTextBox->text();
inst.screenshotShortcutCombo = ui->screenshotShortcutTextBox->text();
inst.captureWindowExec = ui->captureWindowCmdTextBox->text();
inst.captureWindowShortcut = ui->captureWindowShortCutTextBox->text();
try {
inst.writeConfig();
}
catch (std::exception &e) {
couldNotSaveSettingsMsg->showMessage("Unable to save settings. Error: " + QString(e.what()));
}
hotkeyManager->updateHotkeys();
}
void Settings::onBrowseClicked() {
auto browseStart = ui->eviRepoTextBox->text();
browseStart = QFile(browseStart).exists() ? browseStart : QDir::homePath();
auto filename = QFileDialog::getExistingDirectory(this, tr("Select a project directory"),
browseStart, QFileDialog::ShowDirsOnly);
if (filename != nullptr) {
ui->eviRepoTextBox->setText(filename);
}
}
void Settings::onTestConnectionClicked() {
testConnectionButton->startAnimation();
testConnectionButton->setEnabled(false);
currentTestReply = NetMan::getInstance().testConnection(
ui->hostPathTextBox->text(), ui->accessKeyTextBox->text(), ui->secretKeyTextBox->text());
connect(currentTestReply, &QNetworkReply::finished, this, &Settings::onTestRequestComplete);
}
void Settings::onTestRequestComplete() {
bool ok = true;
auto statusCode =
currentTestReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(&ok);
if (ok) {
switch (statusCode) {
case HttpStatus::StatusOK:
this->ui->statusIconLabel->setText("Connected");
break;
case HttpStatus::StatusUnauthorized:
this->ui->statusIconLabel->setText(
"Could not connect: Unauthorized (check api key and secret)");
break;
case HttpStatus::StatusNotFound:
this->ui->statusIconLabel->setText("Could not connect: Not Found (check URL)");
break;
default:
QString msg = "Could not connect: Unexpected Error (code: ";
msg.append(statusCode);
msg.append(")");
this->ui->statusIconLabel->setText(msg);
}
}
else {
this->ui->statusIconLabel->setText(
"Could not connect: Unexpected Error (check network connection and URL)");
}
testConnectionButton->stopAnimation();
testConnectionButton->setEnabled(true);
tidyReply(&currentTestReply);
}

View File

@ -0,0 +1,51 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef SETTINGS_H
#define SETTINGS_H
#include <QCloseEvent>
#include <QDialog>
#include <QErrorMessage>
#include <QNetworkReply>
#include "components/loading_button/loadingbutton.h"
#include "forms/buttonboxform.h"
#include "hotkeymanager.h"
namespace Ui {
class Settings;
}
class Settings : public ButtonBoxForm {
Q_OBJECT
public:
explicit Settings(HotkeyManager *hotkeyManager, QWidget *parent = nullptr);
~Settings();
private:
void wireUi();
public slots:
void onSaveClicked() override;
void onCancelClicked() override;
void showEvent(QShowEvent *evt) override;
void closeEvent(QCloseEvent *event) override;
void onTestConnectionClicked();
void onTestRequestComplete();
void onBrowseClicked();
private:
Ui::Settings *ui;
QErrorMessage *couldNotSaveSettingsMsg;
LoadingButton *testConnectionButton;
QNetworkReply *currentTestReply = nullptr;
HotkeyManager *hotkeyManager; // borrowed pointer
};
#endif // SETTINGS_H

View File

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Settings</class>
<widget class="QDialog" name="Settings">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>759</width>
<height>304</height>
</rect>
</property>
<property name="windowTitle">
<string>Settings</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="4" column="2">
<widget class="QLabel" name="_shortcutLabel">
<property name="text">
<string>Shortcut</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLineEdit" name="captureWindowCmdTextBox"/>
</item>
<item row="5" column="2">
<widget class="QLabel" name="_captureWindowShortcutLabel">
<property name="text">
<string>Shortcut</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QPushButton" name="testHostButton">
<property name="text">
<string>Test Connection</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="_screenshotCmdLabel">
<property name="text">
<string>Capture Area Command</string>
</property>
</widget>
</item>
<item row="2" column="1" colspan="4">
<widget class="QLineEdit" name="secretKeyTextBox"/>
</item>
<item row="9" column="0" colspan="5">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="0">
<widget class="QLabel" name="_secretKeyLabel">
<property name="text">
<string>Secret Key</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="4">
<widget class="QLineEdit" name="accessKeyTextBox"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="_imgRepoLabel">
<property name="text">
<string>Evidence Repository</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="_hostPathLabel">
<property name="text">
<string>Host Path</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="screenshotCmdTextBox"/>
</item>
<item row="0" column="4">
<widget class="QPushButton" name="eviRepoBrowseButton">
<property name="text">
<string>Browse</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="_accessKeyLabel">
<property name="text">
<string>Access Key</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="4">
<widget class="QLineEdit" name="hostPathTextBox"/>
</item>
<item row="0" column="1" colspan="3">
<widget class="QLineEdit" name="eviRepoTextBox"/>
</item>
<item row="10" column="0" colspan="5">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Save</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="_captureWindowLabel">
<property name="text">
<string>Capture Window Command</string>
</property>
</widget>
</item>
<item row="4" column="3" colspan="2">
<widget class="QLineEdit" name="screenshotShortcutTextBox"/>
</item>
<item row="5" column="3" colspan="2">
<widget class="QLineEdit" name="captureWindowShortCutTextBox"/>
</item>
<item row="6" column="1" colspan="4">
<widget class="QLabel" name="statusIconLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>eviRepoTextBox</tabstop>
<tabstop>eviRepoBrowseButton</tabstop>
<tabstop>accessKeyTextBox</tabstop>
<tabstop>secretKeyTextBox</tabstop>
<tabstop>hostPathTextBox</tabstop>
<tabstop>screenshotCmdTextBox</tabstop>
<tabstop>screenshotShortcutTextBox</tabstop>
<tabstop>captureWindowCmdTextBox</tabstop>
<tabstop>captureWindowShortCutTextBox</tabstop>
<tabstop>testHostButton</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Settings</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Settings</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,29 @@
#include "clipboardhelper.h"
#include <iostream>
QString ClipboardHelper::readPlaintext() {
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
QString data;
if (mimeData->hasHtml()) {
data = (mimeData->text());
}
else if (mimeData->hasText()) {
data = mimeData->text();
}
else {
data = "";
}
return data;
}
QPixmap ClipboardHelper::readImage() {
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
QPixmap data;
if (mimeData->hasImage()) {
data = qvariant_cast<QPixmap>(mimeData->imageData());
}
return data;
}

View File

@ -0,0 +1,22 @@
#ifndef CLIPBOARDHELPER_H
#define CLIPBOARDHELPER_H
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QObject>
#include <QPixmap>
class ClipboardHelper : public QObject {
Q_OBJECT
public:
explicit ClipboardHelper(QObject *parent = nullptr) = delete;
public:
static QString readPlaintext();
static QPixmap readImage();
signals:
};
#endif // CLIPBOARDHELPER_H

111
src/helpers/file_helpers.h Normal file
View File

@ -0,0 +1,111 @@
#ifndef FILE_HELPERS_H
#define FILE_HELPERS_H
#include <QDir>
#include <QFile>
#include <QIODevice>
#include <QRandomGenerator>
#include <QString>
#include <QStringList>
#include <array>
#include "appconfig.h"
#include "appsettings.h"
#include "exceptions/fileerror.h"
#include "helpers/pathseparator.h"
class FileHelpers {
public:
/// randomText generates an arbitrary number of case-sensitive english letters
static QString randomText(unsigned int numChars) {
std::array<int, 2> asciiOffset = {'A', 'a'};
QStringList replacement;
for (unsigned int i = 0; i < numChars; i++) {
int letter = QRandomGenerator::global()->bounded(52);
auto base = asciiOffset.at(letter < 26 ? 0 : 1);
replacement << QString(char(base + (letter % 26)));
}
return replacement.join("");
}
/**
* @brief randomFilename replaces a string of 6 consecutive X characters with 6 consecutive random
* english letters. Each letter may either be upper or lower case. Similar to what QTemporaryFile
* does.
* @param templateStr The model string, with
* @return The resulting filename
*/
static QString randomFilename(QString templateStr) {
QString replaceToken = "XXXXXX";
int templateIndex = templateStr.indexOf(replaceToken);
QString replacement = randomText(replaceToken.length());
return templateStr.replace(templateIndex, replaceToken.length(), replacement);
}
/// converts a c++ std string into QByteArray, ensuring proper encoding
static QByteArray stdStringToByteArray(std::string str) {
return QByteArray(str.c_str(), str.size());
}
/**
* @brief qstringToByteArray converts a QString into a QByteArray, ensuring proper encoding. Only
* safe for ascii content.
* @param q The string to convert
* @return the QString as a QByteArray
*/
static QByteArray qstringToByteArray(QString q) { return stdStringToByteArray(q.toStdString()); }
/// Returns (and creates, if necessary) the path to where evidence should be stored (includes
/// ending path separator)
static QString pathToEvidence() {
AppConfig &conf = AppConfig::getInstance();
auto op = AppSettings::getInstance().operationSlug();
auto root = conf.evidenceRepo + PATH_SEPARATOR;
if (op != "") {
root += op + PATH_SEPARATOR;
}
QDir().mkpath(root);
return root;
}
/// writeFile write the provided content to the provided path.
/// @throws a FileError if there are issues opening or writing to the file.
static void writeFile(QString path, QString content) {
writeFile(path, qstringToByteArray(content));
}
/// writeFile write the provided content to the provided path.
/// @throws a FileError if there are issues opening or writing to the file.
static void writeFile(QString path, QByteArray content) {
QFile file(path);
bool opened = file.open(QIODevice::WriteOnly);
if (opened) {
file.write(content);
file.close();
}
if (file.error() != QFile::NoError) {
throw FileError::mkError("Unable to write to file", path.toStdString(), file.error());
}
}
/// readFile reads all of the data from the given path.
/// @throws a FileError if any issues occur while writing the filethere are issues opening or
/// reading the file.
static QByteArray readFile(QString path) {
QFile file(path);
QByteArray data;
bool opened = file.open(QIODevice::ReadOnly);
if (opened) {
data = file.readAll();
}
if (file.error() != QFile::NoError) {
throw FileError::mkError("Unable to read from file", path.toStdString(), file.error());
}
return data;
}
};
#endif // FILE_HELPERS_H

77
src/helpers/http_status.h Normal file
View File

@ -0,0 +1,77 @@
#ifndef HTTP_STATUS_H
#define HTTP_STATUS_H
// clang-format off
// copied/modified from https://golang.org/src/net/http/status.go
enum HttpStatus {
StatusContinue = 100, // RFC 7231, 6.2.1
StatusSwitchingProtocols = 101, // RFC 7231, 6.2.2
StatusProcessing = 102, // RFC 2518, 10.1
StatusEarlyHints = 103, // RFC 8297
StatusOK = 200, // RFC 7231, 6.3.1
StatusCreated = 201, // RFC 7231, 6.3.2
StatusAccepted = 202, // RFC 7231, 6.3.3
StatusNonAuthoritativeInfo = 203, // RFC 7231, 6.3.4
StatusNoContent = 204, // RFC 7231, 6.3.5
StatusResetContent = 205, // RFC 7231, 6.3.6
StatusPartialContent = 206, // RFC 7233, 4.1
StatusMultiStatus = 207, // RFC 4918, 11.1
StatusAlreadyReported = 208, // RFC 5842, 7.1
StatusIMUsed = 226, // RFC 3229, 10.4.1
StatusMultipleChoices = 300, // RFC 7231, 6.4.1
StatusMovedPermanently = 301, // RFC 7231, 6.4.2
StatusFound = 302, // RFC 7231, 6.4.3
StatusSeeOther = 303, // RFC 7231, 6.4.4
StatusNotModified = 304, // RFC 7232, 4.1
StatusUseProxy = 305, // RFC 7231, 6.4.5
_ = 306, // RFC 7231, 6.4.6 (Unused)
StatusTemporaryRedirect = 307, // RFC 7231, 6.4.7
StatusPermanentRedirect = 308, // RFC 7538, 3
StatusBadRequest = 400, // RFC 7231, 6.5.1
StatusUnauthorized = 401, // RFC 7235, 3.1
StatusPaymentRequired = 402, // RFC 7231, 6.5.2
StatusForbidden = 403, // RFC 7231, 6.5.3
StatusNotFound = 404, // RFC 7231, 6.5.4
StatusMethodNotAllowed = 405, // RFC 7231, 6.5.5
StatusNotAcceptable = 406, // RFC 7231, 6.5.6
StatusProxyAuthRequired = 407, // RFC 7235, 3.2
StatusRequestTimeout = 408, // RFC 7231, 6.5.7
StatusConflict = 409, // RFC 7231, 6.5.8
StatusGone = 410, // RFC 7231, 6.5.9
StatusLengthRequired = 411, // RFC 7231, 6.5.10
StatusPreconditionFailed = 412, // RFC 7232, 4.2
StatusRequestEntityTooLarge = 413, // RFC 7231, 6.5.11
StatusRequestURITooLong = 414, // RFC 7231, 6.5.12
StatusUnsupportedMediaType = 415, // RFC 7231, 6.5.13
StatusRequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4
StatusExpectationFailed = 417, // RFC 7231, 6.5.14
StatusTeapot = 418, // RFC 7168, 2.3.3
StatusMisdirectedRequest = 421, // RFC 7540, 9.1.2
StatusUnprocessableEntity = 422, // RFC 4918, 11.2
StatusLocked = 423, // RFC 4918, 11.3
StatusFailedDependency = 424, // RFC 4918, 11.4
StatusTooEarly = 425, // RFC 8470, 5.2.
StatusUpgradeRequired = 426, // RFC 7231, 6.5.15
StatusPreconditionRequired = 428, // RFC 6585, 3
StatusTooManyRequests = 429, // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431, // RFC 6585, 5
StatusUnavailableForLegalReasons = 451, // RFC 7725, 3
StatusInternalServerError = 500, // RFC 7231, 6.6.1
StatusNotImplemented = 501, // RFC 7231, 6.6.2
StatusBadGateway = 502, // RFC 7231, 6.6.3
StatusServiceUnavailable = 503, // RFC 7231, 6.6.4
StatusGatewayTimeout = 504, // RFC 7231, 6.6.5
StatusHTTPVersionNotSupported = 505, // RFC 7231, 6.6.6
StatusVariantAlsoNegotiates = 506, // RFC 2295, 8.1
StatusInsufficientStorage = 507, // RFC 4918, 11.5
StatusLoopDetected = 508, // RFC 5842, 7.2
StatusNotExtended = 510, // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511, // RFC 6585, 6
};
// clang-format on
#endif // HTTP_STATUS_H

51
src/helpers/jsonhelpers.h Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef JSONHELPERS_H
#define JSONHELPERS_H
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <vector>
// parseJSONList parses a JSON list into a vector of concrete types from a byte[]. If any error
// occurs during parsing, an empty vector is returned
template <typename T>
static std::vector<T> parseJSONList(QByteArray data, T (*dataToItem)(QJsonObject)) {
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
return std::vector<T>();
}
QJsonArray arr = doc.array();
std::vector<T> list;
for (QJsonValue val : arr) {
auto item = dataToItem(val.toObject());
list.push_back(item);
}
return list;
}
// parseJSONItem parses a single item (assumed to be a Json Object) from a byte[]. If any error
// occurs during parsing, an empty object is returned.
template <typename T>
static T parseJSONItem(QByteArray data, T (*dataToItem)(QJsonObject)) {
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(data, &err);
if (err.error != QJsonParseError::NoError) {
return T();
}
return dataToItem(doc.object());
}
// Possilbe generic version of converting to json
// template <typename T>
// static QByteArray toJSONObject(T item, QJsonObject(*itemToData)(T)) {
// auto obj = itemToData(item);
// return QJsonDocument::fromVariant(obj).toJson();
//}
#endif // JSONHELPERS_H

View File

@ -0,0 +1,107 @@
/*********************************************************************************
* File Name : multipartparser.cpp
* Created By : Ye Yangang
* Creation Date : [2017-02-20 16:50]
* Last Modified : [AUTO_UPDATE_BEFORE_SAVE]
* Description : Generate multipart/form-data POST body
**********************************************************************************/
#include "multipartparser.h"
#include <stdlib.h>
#include <time.h>
#include <algorithm>
#include <fstream>
#include <future>
#include <iostream>
const std::string MultipartParser::boundary_prefix_("----AScreenTrayApp");
const std::string MultipartParser::rand_chars_(
"0123456789"
"abcdefghijklmnopqrstuvwxyz"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ");
MultipartParser::MultipartParser() {
int i = 0;
int len = rand_chars_.size();
boundary_ = boundary_prefix_;
while (i < 16) {
int idx = rand() % len;
boundary_.push_back(rand_chars_[idx]);
++i;
}
}
const std::string &MultipartParser::GenBodyContent() {
std::vector<std::future<std::string>> futures;
body_content_.clear();
for (auto &file : files_) {
std::future<std::string> content_futures = std::async(std::launch::async, [&file]() {
std::ifstream ifile(file.second, std::ios::binary | std::ios::ate);
std::streamsize size = ifile.tellg();
ifile.seekg(0, std::ios::beg);
char *buff = new char[size];
ifile.read(buff, size);
ifile.close();
std::string ret(buff, size);
delete[] buff;
return ret;
});
futures.push_back(std::move(content_futures));
}
for (auto &param : params_) {
body_content_ += "\r\n--";
body_content_ += boundary_;
body_content_ += "\r\nContent-Disposition: form-data; name=\"";
body_content_ += param.first;
body_content_ += "\"\r\n\r\n";
body_content_ += param.second;
}
for (size_t i = 0; i < files_.size(); ++i) {
std::string *filename = new std::string();
std::string *content_type = new std::string();
std::string file_content = futures[i].get();
_get_file_name_type(files_[i].second, filename, content_type);
body_content_ += "\r\n--";
body_content_ += boundary_;
body_content_ += "\r\nContent-Disposition: form-data; name=\"";
body_content_ += files_[i].first;
body_content_ += "\"; filename=\"";
body_content_ += *filename;
body_content_ += "\"\r\nContent-Type: ";
body_content_ += *content_type;
body_content_ += "\r\n\r\n";
body_content_ += file_content;
}
body_content_ += "\r\n--";
body_content_ += boundary_;
body_content_ += "--\r\n";
return body_content_;
}
void MultipartParser::_get_file_name_type(const std::string &file_path, std::string *filename,
std::string *content_type) {
if (filename == NULL || content_type == NULL) return;
size_t last_spliter = file_path.find_last_of("/\\");
*filename = file_path.substr(last_spliter + 1);
size_t dot_pos = filename->find_last_of(".");
if (dot_pos == std::string::npos) {
*content_type = "application/octet-stream";
return;
}
std::string ext = filename->substr(dot_pos + 1);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == "jpg" || ext == "jpeg") {
*content_type = "image/jpeg";
return;
}
if (ext == "txt" || ext == "log") {
*content_type = "text/plain";
return;
}
*content_type = "application/octet-stream";
return;
}

View File

@ -0,0 +1,42 @@
/*********************************************************************************
* File Name : multipartparser.h
* Created By : Ye Yangang
* Creation Date : [2017-02-20 16:50]
* Last Modified : [AUTO_UPDATE_BEFORE_SAVE]
* Description : Generate multipart/form-data POST body
**********************************************************************************/
#ifndef MULTIPARTPARSER_H
#define MULTIPARTPARSER_H
#include <string>
#include <tuple>
#include <vector>
class MultipartParser {
public:
MultipartParser();
inline const std::string &body_content() { return body_content_; }
inline const std::string &boundary() { return boundary_; }
inline void AddParameter(const std::string &name, const std::string &value) {
params_.push_back(std::pair<std::string, std::string>(name, value));
}
inline void AddFile(const std::string &name, const std::string &value) {
files_.push_back(std::pair<std::string, std::string>(name, value));
}
const std::string &GenBodyContent();
private:
void _get_file_name_type(const std::string &file_path, std::string *filenae,
std::string *content_type);
private:
static const std::string boundary_prefix_;
static const std::string rand_chars_;
std::string boundary_;
std::string body_content_;
std::vector<std::pair<std::string, std::string>> params_;
std::vector<std::pair<std::string, std::string>> files_;
};
#endif // MULTIPARTPARSER_H

218
src/helpers/netman.h Normal file
View File

@ -0,0 +1,218 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef NETMAN_H
#define NETMAN_H
#include <QMessageAuthenticationCode>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include "appconfig.h"
#include "dtos/operation.h"
#include "dtos/tag.h"
#include "helpers/file_helpers.h"
#include "helpers/multipartparser.h"
#include "helpers/stopreply.h"
#include "models/evidence.h"
static auto NO_BODY = "";
class NetMan : public QObject {
Q_OBJECT
public:
static NetMan &getInstance() {
static NetMan instance;
return instance;
}
NetMan(NetMan const &) = delete;
void operator=(NetMan const &) = delete;
enum RequestMethod { METHOD_GET = 0, METHOD_POST };
static QString RequestMethodToString(RequestMethod val) {
static QString names[] = {"GET", "POST"};
return names[val];
}
signals:
void operationListUpdated(bool success,
std::vector<dto::Operation> operations = std::vector<dto::Operation>());
private:
QNetworkAccessManager *nam;
NetMan() { nam = new QNetworkAccessManager; }
~NetMan() {
delete nam;
stopReply(&allOpsReply);
}
// mkApiUrl creates a new URL with the appropriate request start
QString mkApiUrl(QString endpoint, QString host = "") {
QString base = (host == "") ? AppConfig::getInstance().apiURL : host;
if (base.size() == 0) { // if a user hasn't set up the application, then base could be empty
return endpoint;
}
if (base.at(base.size() - 1) == '/') {
base.chop(1);
}
return base + endpoint;
}
QString getRFC1123Date() {
return QDateTime::currentDateTimeUtc().toString("ddd, dd MMM yyyy hh:mm:ss 'GMT'");
}
QString generateHash(QString method, QString path, QString date, QByteArray body = NO_BODY,
const QString &secretKey = "") {
auto hashedBody = QCryptographicHash::hash(body, QCryptographicHash::Sha256);
std::string msg = (method + "\n" + path + "\n" + date + "\n").toStdString();
msg += hashedBody.toStdString();
QString secretKeyCopy = QString(secretKey);
if (secretKeyCopy.isEmpty()) {
secretKeyCopy = AppConfig::getInstance().secretKey;
}
QMessageAuthenticationCode code(QCryptographicHash::Sha256);
QByteArray key = QByteArray::fromBase64(FileHelpers::qstringToByteArray(secretKeyCopy));
code.setKey(key);
code.addData(FileHelpers::stdStringToByteArray(msg));
return code.result().toBase64();
}
QNetworkReply *makeJsonRequest(RequestMethod method, QString endpoint, QByteArray body = NO_BODY,
const QString &host = "", const QString &apiKey = "",
const QString &secretKey = "") {
QNetworkRequest req = prepRequest(method, endpoint, body, host, apiKey, secretKey);
switch (method) {
case METHOD_GET:
return nam->get(req);
case METHOD_POST:
req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
return nam->post(req, body);
default:
std::cerr << "makeRequest passed an unsupported method" << std::endl;
}
return nullptr;
}
QNetworkReply *makeFormRequest(RequestMethod method, QString endpoint, QString boundry,
QByteArray body = NO_BODY, QString host = "") {
QNetworkRequest req = prepRequest(method, endpoint, body, host);
switch (method) {
case METHOD_GET:
return nam->get(req);
case METHOD_POST:
req.setHeader(QNetworkRequest::ContentTypeHeader,
"multipart/form-data; boundary=" + boundry);
return nam->post(req, body);
default:
std::cerr << "makeRequest passed an unsupported method" << std::endl;
}
return nullptr;
}
QNetworkRequest prepRequest(RequestMethod method, QString endpoint, QByteArray body, QString host,
const QString &apiKey = "", const QString &secretKey = "") {
QNetworkRequest req;
auto now = getRFC1123Date();
QString reqMethod = RequestMethodToString(method);
QString apiKeyCopy = QString(apiKey);
if (apiKeyCopy.isEmpty()) {
apiKeyCopy = AppConfig::getInstance().accessKey;
}
auto code = generateHash(reqMethod, endpoint, now, body, secretKey);
auto authValue = apiKeyCopy + ":" + code;
req.setUrl(mkApiUrl(endpoint, host));
req.setRawHeader(QByteArray("Date"), FileHelpers::qstringToByteArray(now));
req.setRawHeader(QByteArray("Authorization"), FileHelpers::qstringToByteArray(authValue));
return req;
}
void onGetOpsComplete() {
bool isValid;
auto data = extractResponse(allOpsReply, isValid);
if (isValid) {
std::vector<dto::Operation> ops = dto::Operation::parseDataAsList(data);
std::sort(ops.begin(), ops.end(),
[](dto::Operation i, dto::Operation j) { return i.name < j.name; });
emit operationListUpdated(true, ops);
}
else {
emit operationListUpdated(false);
}
tidyReply(&allOpsReply);
}
public:
QNetworkReply *uploadAsset(model::Evidence evidence) {
MultipartParser parser;
parser.AddParameter("notes", evidence.description.toStdString());
parser.AddParameter("contentType", evidence.contentType.toStdString());
// TODO: convert this time below into a proper unix timestamp (mSecSinceEpoch and secsSinceEpoch
// produce invalid times)
// parser.AddParameter("occurred_at", std::to_string(evidence.recordedDate);
QStringList list;
for (auto tag : evidence.tags) {
list << QString::number(tag.serverTagId);
}
parser.AddParameter("tagIds", ("[" + list.join(",") + "]").toStdString());
parser.AddFile("file", evidence.path.toStdString());
auto body = FileHelpers::stdStringToByteArray(parser.GenBodyContent());
return makeFormRequest(METHOD_POST, "/api/operations/" + evidence.operationSlug + "/evidence",
parser.boundary().c_str(), body);
}
QNetworkReply *testConnection(QString host, QString apiKey, QString secretKey) {
return makeJsonRequest(METHOD_GET, "/api/operations", NO_BODY, host, apiKey, secretKey);
}
QNetworkReply *getAllOperations() { return makeJsonRequest(METHOD_GET, "/api/operations"); }
void refreshOperationsList() {
allOpsReply = getAllOperations();
connect(allOpsReply, &QNetworkReply::finished, this, &NetMan::onGetOpsComplete);
}
QNetworkReply *getOperationTags(QString operationSlug) {
auto url = "/api/operations/" + operationSlug + "/tags";
return makeJsonRequest(METHOD_GET, url);
}
QNetworkReply *createTag(dto::Tag tag, QString operationSlug) {
auto url = "/api/operations/" + operationSlug + "/tags";
return makeJsonRequest(METHOD_POST, url, dto::Tag::toJson(tag));
}
static QByteArray extractResponse(QNetworkReply *reply, bool &valid) {
auto status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
auto reqErr = reply->error();
valid = (QNetworkReply::NoError == reqErr && status.isValid());
valid = valid && (status == 200 || status == 201);
return reply->readAll();
}
private:
QNetworkReply *allOpsReply = nullptr;
};
#endif // NETMAN_H

View File

@ -0,0 +1,20 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef PATHSEPARATOR_H
#define PATHSEPARATOR_H
// FROM:
// https://stackoverflow.com/questions/12971499/how-to-get-the-file-separator-symbol-in-standard-c-c-or
// Note: this is only going to work for Windows and (linux/mac). Boost may have the ability to
// resolve this Note2: We probably don't need this. Qt treats "/" as the universal path separator.
// We would only need this for reflecting the path to the user. See:
// https://doc.qt.io/qt-5/qdir.html
#if defined(WIN32) || defined(_WIN32)
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif
#endif // PATHSEPARATOR_H

View File

@ -0,0 +1,60 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "screenshot.h"
#include <QDir>
#include <QFile>
#include <QObject>
#include <array>
#include <cstdio>
#include <iostream>
#include <string>
#include <utility>
#include "appconfig.h"
#include "helpers/file_helpers.h"
#include "helpers/pathseparator.h"
Screenshot::Screenshot(QObject *parent) : QObject(parent) {}
QString Screenshot::formatScreenshotCmd(QString cmdProto, const QString &filename) {
auto lowerCmd = cmdProto.toLower();
QString key = "%file";
auto idx = lowerCmd.indexOf(key);
if (idx == -1) {
return cmdProto;
}
QString fixedFilename = "'" + filename + "'";
return cmdProto.replace(idx, key.length(), fixedFilename);
}
void Screenshot::captureArea() { basicScreenshot(AppConfig::getInstance().screenshotExec); }
void Screenshot::captureWindow() { basicScreenshot(AppConfig::getInstance().captureWindowExec); }
void Screenshot::basicScreenshot(QString cmdProto) {
auto root = FileHelpers::pathToEvidence();
auto hasPath = QDir().mkpath(root);
if (hasPath) {
auto tempPath = FileHelpers::randomFilename(QDir::tempPath() + PATH_SEPARATOR +
"ashirt_screenshot_XXXXXX.png");
QString cmd = formatScreenshotCmd(std::move(cmdProto), tempPath);
auto lastSlash = tempPath.lastIndexOf(PATH_SEPARATOR) + 1;
QString tempName = tempPath.right(tempPath.length() - lastSlash);
system(cmd.toStdString().c_str());
// check if file exists before doing this
auto finalName = root + tempName;
QFile src(tempPath);
if (src.exists()) {
auto moved = src.rename(QString(finalName));
auto trueName = moved ? finalName : tempName;
emit onScreenshotCaptured(trueName);
}
}
}

25
src/helpers/screenshot.h Normal file
View File

@ -0,0 +1,25 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef SCREENSHOT_H
#define SCREENSHOT_H
#include <QObject>
#include <string>
class Screenshot : public QObject {
Q_OBJECT
public:
Screenshot(QObject* parent = 0);
void captureArea();
void captureWindow();
signals:
void onScreenshotCaptured(QString filepath);
private:
QString formatScreenshotCmd(QString cmdProto, const QString& filename);
void basicScreenshot(QString cmdProto);
};
#endif // SCREENSHOT_H

32
src/helpers/stopreply.cpp Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "stopreply.h"
#include <QNetworkReply>
// stopReply aborts a request, then cleans up the reply (via deleteLater)
// also sets the reply pointer to nullptr. This is for use cases where the
// reply is to be ignored.
void stopReply(QNetworkReply **reply) {
if (*reply == nullptr) {
return;
}
(*reply)->abort();
(*reply)->deleteLater();
*reply = nullptr;
}
// tidyReply cleans up a "completed" reply, closing the connection and marking
// for deletion. Additionally, this sets the reply pointer to nullptr. This is
// for use cases where a reply has extracted all necessary information and clean-up is necessary.
void tidyReply(QNetworkReply **reply) {
if (*reply == nullptr) {
return;
}
(*reply)->close();
(*reply)->deleteLater();
*reply = nullptr;
}

12
src/helpers/stopreply.h Normal file
View File

@ -0,0 +1,12 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef STOPREPLY_H
#define STOPREPLY_H
#include <QNetworkReply>
void stopReply(QNetworkReply **reply);
void tidyReply(QNetworkReply **reply);
#endif // STOPREPLY_H

66
src/helpers/ui_helpers.h Normal file
View File

@ -0,0 +1,66 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef REPLACE_PLACEHOLDER_H
#define REPLACE_PLACEHOLDER_H
#include <QComboBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QWidget>
class UiHelpers {
public:
/**
* @brief overlapPlaceholder Adds a component in the same position as the "placeholder" widget
* @param placeholder The QWidget to overlap
* @param replacement The QWidget that will overlap the placeholder
* @param layout Where _both_ QWidgets should live
*/
static void overlapPlaceholder(QWidget* placeholder, QWidget* replacement, QGridLayout* layout) {
int row, col, rSpan, cSpan;
auto widgetIndex = layout->indexOf(placeholder);
if (widgetIndex == -1) {
throw std::runtime_error("Placeholder is not contained in layout");
}
layout->getItemPosition(widgetIndex, &row, &col, &rSpan, &cSpan);
layout->addWidget(replacement, row, col, rSpan, cSpan);
}
/**
* @brief replacePlaceholder Replaces the _placeholder_ component with the _replacement_
* component. The original/placeholder component is hidden and removed from the layout
* @param placeholder The QWidget to remove
* @param replacement The QWidget to add in the placeholder's position
* @param layout Where the placeholder lives, and where the replacement will live
*/
static void replacePlaceholder(QWidget* placeholder, QWidget* replacement, QGridLayout* layout) {
overlapPlaceholder(placeholder, replacement, layout);
placeholder->setVisible(false);
layout->removeWidget(placeholder);
}
/**
* @brief setComboBoxValue Sets a combobox's value based on the supplied _value_. Sets the value
* to the proper index if found, otherwise sets the index to 0 if not found. Note: this does a
* linear search for values, so may not be appropriate for all boxen.
* @param box The source combobox
* @param value The value to search for in the combobox
*/
static void setComboBoxValue(QComboBox* box, const QString& value) {
bool found = false;
for (int i = 0; i < box->count(); i++) {
if (box->itemData(i) == value) {
box->setCurrentIndex(i);
found = true;
break;
}
}
if (!found) {
box->setCurrentIndex(0);
}
}
};
#endif // REPLACE_PLACEHOLDER_H

51
src/hotkeymanager.cpp Normal file
View File

@ -0,0 +1,51 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "hotkeymanager.h"
#include <QString>
#include <iostream>
#include "appconfig.h"
#include "appsettings.h"
#include "helpers/screenshot.h"
HotkeyManager::HotkeyManager(Screenshot* ss) {
hotkeyManager = new UGlobalHotkeys();
screenshotTool = ss;
connect(hotkeyManager, &UGlobalHotkeys::activated, this, &HotkeyManager::hotkeyTriggered);
}
HotkeyManager::~HotkeyManager() { delete hotkeyManager; }
void HotkeyManager::registerKey(const QString& binding, GlobalHotkeyEvent evt) {
hotkeyManager->registerHotkey(binding, size_t(evt));
}
void HotkeyManager::unregisterKey(GlobalHotkeyEvent evt) {
hotkeyManager->unregisterHotkey(size_t(evt));
}
void HotkeyManager::hotkeyTriggered(size_t hotkeyIndex) {
if (hotkeyIndex == ACTION_CAPTURE_AREA) {
screenshotTool->captureArea();
}
else if (hotkeyIndex == ACTION_CAPTURE_WINDOW) {
screenshotTool->captureWindow();
}
}
void HotkeyManager::updateHotkeys() {
hotkeyManager->unregisterAllHotkeys();
if (!AppSettings::getInstance().isOperationPaused()) {
auto shortcut = AppConfig::getInstance().screenshotShortcutCombo;
if (shortcut != "") {
registerKey(shortcut, ACTION_CAPTURE_AREA);
}
shortcut = AppConfig::getInstance().captureWindowShortcut;
if (shortcut != "") {
registerKey(shortcut, ACTION_CAPTURE_WINDOW);
}
}
}

41
src/hotkeymanager.h Normal file
View File

@ -0,0 +1,41 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef HOTKEYMANAGER_H
#define HOTKEYMANAGER_H
#include <QObject>
#include "helpers/screenshot.h"
#include "tools/UGlobalHotkey/uglobalhotkeys.h"
// HotkeyManager is a singleton for managing global hotkeys.
class HotkeyManager : public QObject {
Q_OBJECT
public:
HotkeyManager(Screenshot* ss);
~HotkeyManager();
enum GlobalHotkeyEvent {
// Reserving 1 (UGlobalHotkey default)
ACTION_CAPTURE_AREA = 2,
ACTION_CAPTURE_WINDOW = 3
};
private:
Screenshot* screenshotTool;
UGlobalHotkeys* hotkeyManager;
public:
void registerKey(const QString& binding, GlobalHotkeyEvent evt);
void unregisterKey(GlobalHotkeyEvent evt);
public slots:
void updateHotkeys();
private slots:
void hotkeyTriggered(size_t hotkeyIndex);
};
#endif // HOTKEYMANAGER_H

106
src/main.cpp Normal file
View File

@ -0,0 +1,106 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
void handleCLI(std::vector<std::string> args);
#ifndef QT_NO_SYSTEMTRAYICON
#include <QApplication>
#include <QMessageBox>
#include "appconfig.h"
#include "appsettings.h"
#include "db/databaseconnection.h"
#include "exceptions/databaseerr.h"
#include "exceptions/fileerror.h"
#include "traymanager.h"
int main(int argc, char* argv[]) {
Q_INIT_RESOURCE(res_icons);
Q_INIT_RESOURCE(res_migrations);
QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
QCoreApplication::setOrganizationName("Verizon Media Group");
QCoreApplication::setOrganizationDomain("verizon.com");
QCoreApplication::setApplicationName("AShirt Screenshot");
DatabaseConnection* conn;
try {
conn = new DatabaseConnection();
conn->connect();
}
catch (FileError& err) {
std::cout << err.what() << std::endl;
return -1;
}
catch (DBDriverUnavailableError& err) {
std::cout << err.what() << std::endl;
return -1;
}
catch (QSqlError& e) {
std::cout << e.text().toStdString() << std::endl;
return -1;
}
catch (std::exception& e) {
std::cout << "Unexpected error: " << e.what() << std::endl;
return -1;
}
auto configError = AppConfig::getInstance().errorText.toStdString();
if (!configError.empty()) { // quick check & preload config data
std::cout << "Unable to load config file: " << configError << std::endl;
return -1;
}
int rtn;
try {
QApplication app(argc, argv);
if (!QSystemTrayIcon::isSystemTrayAvailable()) {
handleCLI(std::vector<std::string>(argv, argv + argc));
}
QApplication::setQuitOnLastWindowClosed(false);
auto window = new TrayManager(conn);
rtn = app.exec();
AppSettings::getInstance().sync();
delete window;
}
catch (std::exception const& ex) {
std::cout << "Exception while running: " << ex.what() << std::endl;
}
catch (...) {
std::cout << "Unhandled exception while running" << std::endl;
}
conn->close();
delete conn;
return rtn;
}
#else
#include <QDebug>
#include <QLabel>
int main(int argc, char *argv[]) { handleCLI(std::vector<string>(argv, argv + argc)); }
#endif
void handleCLI(std::vector<std::string> args) {
size_t trueCount = args.size() - 1;
std::cout << "You provided " << trueCount << " arguments.\n";
if (trueCount == 0) {
std::cout << "Next time try suppling some arguments." << std::endl;
return;
}
std::cout << "All arguments:" << std::endl;
for (size_t i = 1; i < args.size(); i++) {
std::cout << "\t" << args.at(i) << std::endl;
}
}

60
src/models/codeblock.cpp Normal file
View File

@ -0,0 +1,60 @@
#include "codeblock.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QVariant>
#include <utility>
#include "helpers/file_helpers.h"
#include "helpers/jsonhelpers.h"
static Codeblock fromJson(QJsonObject obj) {
Codeblock rtn;
rtn.content = obj["content"].toVariant().toString();
rtn.subtype = obj["contentSubtype"].toString();
QJsonObject meta = obj["metadata"].toObject();
if (!meta.empty()) {
rtn.source = meta["source"].toString();
}
return rtn;
}
Codeblock::Codeblock() = default;
Codeblock::Codeblock(QString content) {
this->filename =
FileHelpers::randomFilename(FileHelpers::pathToEvidence() + "ashirt_codeblock_XXXXXX.json");
this->content = std::move(content);
this->subtype = "";
this->source = "";
}
void Codeblock::saveCodeblock(Codeblock codeblock) {
FileHelpers::writeFile(codeblock.filename, codeblock.encode());
}
Codeblock Codeblock::readCodeblock(const QString& filepath) {
QByteArray data = FileHelpers::readFile(filepath);
Codeblock rtn = parseJSONItem<Codeblock>(data, fromJson);
rtn.filename = filepath;
return rtn;
}
QByteArray Codeblock::encode() {
QJsonObject root;
root.insert("contentSubtype", subtype);
root.insert("content", content);
QJsonObject metadata;
if (source != "") {
metadata.insert("source", source);
}
if (!metadata.empty()) {
root.insert("metadata", metadata);
}
return QJsonDocument(root).toJson();
}

62
src/models/codeblock.h Normal file
View File

@ -0,0 +1,62 @@
#ifndef CODEBLOCK_H
#define CODEBLOCK_H
#include <QString>
/**
* @brief The Codeblock class represents ASHIRT "codeblock"-style evidence. Codeblocks are
* represented by a handful of values: the actual content, where the content was retrieved, and what
* language the codeblock represents.
*
* As this file is meant for storage, it also includes the ability to manage its own on-disk file.
*/
class Codeblock {
public:
/// default constructor, no data is provided
Codeblock();
/// creates a new codeblock with the given content and a new random filename
/// (all other data must be provided)
Codeblock(QString content);
/**
* @brief readCodeblock parses a local codeblock file and returns back the data as a codeblock
* @param filepath The path to the codeblock file
* @throws a FileError if any issues occur while reading the file
* @return a parsed Codeblock object, ready for use.
*/
static Codeblock readCodeblock(const QString& filepath);
public:
/// content stores the actual codeblock data (i.e. the source code)
QString content;
/// subtype stores what language the codeblock was written in, or an empty string if plaintext
QString subtype;
/// source store where the codeblock was found, typically represented as a url
QString source;
private:
/// filename is the path to where this file was read from/will be written to
QString filename;
public:
/// filePath is a small helper to access the filename
inline QString filePath() { return filename; }
/**
* @brief encode converts the Codeblock into a json-encoded QNyteArray (it's normal
* representation)
* @return the encoded Codeblock
*/
QByteArray encode();
public:
/**
* @brief saveCodeblock encodes the provided codeblock, then writes that codeblock to it's
* filePath
* @throws a FileError if any issues occur while writing the file
* @param codeblock The codeblock to save
*/
static void saveCodeblock(Codeblock codeblock);
};
#endif // CODEBLOCK_H

27
src/models/evidence.h Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef MODEL_EVIDENCE_H
#define MODEL_EVIDENCE_H
#include <QDateTime>
#include <QString>
#include "tag.h"
namespace model {
class Evidence {
public:
qint64 id;
QString path;
QString operationSlug;
QString description;
QString errorText;
QString contentType;
QDateTime recordedDate;
QDateTime uploadDate;
std::vector<Tag> tags;
};
} // namespace model
#endif // MODEL_EVIDENCE_H

24
src/models/tag.h Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef MODEL_TAG_H
#define MODEL_TAG_H
#include <QString>
namespace model {
class Tag {
public:
Tag(qint64 id, qint64 tagId, QString name) : Tag(tagId, name) { this->id = id; }
Tag(qint64 tagId, QString name) {
this->serverTagId = tagId;
this->tagName = name;
}
qint64 id;
qint64 serverTagId;
QString tagName;
};
} // namespace model
#endif // MODEL_TAG_H

278
src/traymanager.cpp Normal file
View File

@ -0,0 +1,278 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#include "traymanager.h"
#ifndef QT_NO_SYSTEMTRAYICON
#include <QAction>
#include <QApplication>
#include <QCheckBox>
#include <QCloseEvent>
#include <QComboBox>
#include <QCoreApplication>
#include <QDesktopWidget>
#include <QGroupBox>
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QTextEdit>
#include <QVBoxLayout>
#include <iostream>
#include "appconfig.h"
#include "appsettings.h"
#include "db/databaseconnection.h"
#include "forms/getinfo/getinfo.h"
#include "helpers/clipboard/clipboardhelper.h"
#include "helpers/netman.h"
#include "helpers/screenshot.h"
#include "hotkeymanager.h"
#include "models/codeblock.h"
#include "tools/UGlobalHotkey/uglobalhotkeys.h"
// Tray icons are handled differently between different OS and desktop
// environments. MacOS uses a monochrome mask to render a light or dark icon
// depending on which appearance is used. Gnome does not seam to be aware of a
// light/dark theme within it's tray to be able to automatically take advantage
// of this. Desktop environments and themes in Linux don't appear to be
// consistent so we will default to a light icon given that the default bars
// appear to be dark until we know more. Who knows about windows?
#ifdef Q_OS_MACOS
#define ICON ":/icons/shirt-dark.svg"
#else
#define ICON ":/icons/shirt-light.svg"
#endif
TrayManager::TrayManager(DatabaseConnection* db) {
this->db = db;
screenshotTool = new Screenshot();
hotkeyManager = new HotkeyManager(screenshotTool);
hotkeyManager->updateHotkeys();
settingsWindow = new Settings(hotkeyManager, this);
evidenceManagerWindow = new EvidenceManager(db, this);
creditsWindow = new Credits(this);
// delayed so that windows can listen for get all ops signal
NetMan::getInstance().refreshOperationsList();
wireUi();
createActions();
createTrayMenu();
QIcon icon = QIcon(ICON);
// TODO: figure out if any other environments support masking
#ifdef Q_OS_MACOS
icon.setIsMask(true);
#endif
trayIcon->setIcon(icon);
setActiveOperationLabel();
trayIcon->show();
}
TrayManager::~TrayManager() {
setVisible(false);
delete quitAction;
delete showSettingsAction;
delete currentOperationMenuAction;
delete captureScreenAreaAction;
delete captureWindowAction;
delete showEvidenceManagerAction;
delete showCreditsAction;
delete addCodeblockAction;
cleanChooseOpSubmenu(); // must be done before deleting chooseOpSubmenu/action
delete chooseOpStatusAction;
delete pauseOperationAction;
delete refreshOperationListAction;
delete chooseOpSubmenu;
delete trayIconMenu;
delete trayIcon;
delete screenshotTool;
delete hotkeyManager;
delete settingsWindow;
delete evidenceManagerWindow;
delete creditsWindow;
}
void TrayManager::cleanChooseOpSubmenu() {
for (QAction* act : allOperationActions) {
chooseOpSubmenu->removeAction(act);
delete act;
}
allOperationActions.clear();
}
void TrayManager::wireUi() {
connect(screenshotTool, &Screenshot::onScreenshotCaptured, this,
&TrayManager::onScreenshotCaptured);
connect(&NetMan::getInstance(), &NetMan::operationListUpdated, this,
&TrayManager::onOperationListUpdated);
connect(&AppSettings::getInstance(), &AppSettings::onOperationStateChanged, this,
&TrayManager::setActiveOperationLabel);
connect(&AppSettings::getInstance(), &AppSettings::onOperationUpdated, this,
&TrayManager::setActiveOperationLabel);
}
void TrayManager::closeEvent(QCloseEvent* event) {
#ifdef Q_OS_MACOS
if (!event->spontaneous() || !isVisible()) {
return;
}
#endif
if (trayIcon->isVisible()) {
hide();
event->ignore();
}
}
void TrayManager::createActions() {
quitAction = new QAction(tr("Quit"), this);
connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit);
showSettingsAction = new QAction(tr("Settings"), this);
connect(showSettingsAction, &QAction::triggered, settingsWindow, &QWidget::show);
currentOperationMenuAction = new QAction(this);
currentOperationMenuAction->setEnabled(false);
captureScreenAreaAction = new QAction(tr("Capture Screen Area"), this);
connect(captureScreenAreaAction, &QAction::triggered, screenshotTool, &Screenshot::captureArea);
captureWindowAction = new QAction(tr("Capture Window"), this);
connect(captureWindowAction, &QAction::triggered, screenshotTool, &Screenshot::captureWindow);
showEvidenceManagerAction = new QAction(tr("View Accumulated Evidence"), this);
connect(showEvidenceManagerAction, &QAction::triggered, evidenceManagerWindow, &QWidget::show);
showCreditsAction = new QAction(tr("About"), this);
connect(showCreditsAction, &QAction::triggered, creditsWindow, &QWidget::show);
addCodeblockAction = new QAction(tr("Add Codeblock from Clipboard"), this);
connect(addCodeblockAction, &QAction::triggered, [this]() {
QString clipboardContent = ClipboardHelper::readPlaintext();
if (clipboardContent != "") {
Codeblock evidence(clipboardContent);
Codeblock::saveCodeblock(evidence);
auto evidenceID = db->createEvidence(evidence.filePath(),
AppSettings::getInstance().operationSlug(), "codeblock");
auto getInfoWindow = new GetInfo(db, evidenceID, this);
getInfoWindow->show();
}
});
chooseOpSubmenu = new QMenu(tr("Select Operation"));
chooseOpStatusAction = new QAction("Loading operations...", chooseOpSubmenu);
chooseOpStatusAction->setEnabled(false);
refreshOperationListAction = new QAction(tr("Refresh Operations"), chooseOpSubmenu);
connect(refreshOperationListAction, &QAction::triggered, [this] {
chooseOpStatusAction->setText("Loading operations...");
NetMan::getInstance().refreshOperationsList();
});
pauseOperationAction = new QAction(this);
connect(pauseOperationAction, &QAction::triggered, [this] {
AppSettings::getInstance().toggleOperationPaused();
hotkeyManager->updateHotkeys();
});
chooseOpSubmenu->addAction(chooseOpStatusAction);
chooseOpSubmenu->addAction(refreshOperationListAction);
chooseOpSubmenu->addSeparator();
chooseOpSubmenu->addAction(pauseOperationAction);
chooseOpSubmenu->addSeparator();
}
void TrayManager::createTrayMenu() {
trayIconMenu = new QMenu(this);
trayIconMenu->addAction(this->addCodeblockAction);
trayIconMenu->addAction(this->captureScreenAreaAction);
trayIconMenu->addAction(this->captureWindowAction);
trayIconMenu->addAction(this->showEvidenceManagerAction);
trayIconMenu->addAction(this->showSettingsAction);
trayIconMenu->addSeparator();
trayIconMenu->addAction(this->currentOperationMenuAction);
trayIconMenu->addMenu(chooseOpSubmenu);
trayIconMenu->addSeparator();
trayIconMenu->addAction(this->showCreditsAction);
trayIconMenu->addAction(this->quitAction);
trayIcon = new QSystemTrayIcon(this);
trayIcon->setContextMenu(trayIconMenu);
}
void TrayManager::onScreenshotCaptured(const QString& path) {
std::cout << "Captured screenshot to file: " << path.toStdString() << std::endl;
try {
auto evidenceID =
db->createEvidence(path, AppSettings::getInstance().operationSlug(), "image");
auto getInfoWindow = new GetInfo(db, evidenceID, this);
getInfoWindow->show();
}
catch (QSqlError& e) {
std::cout << "could not write to the database: " << e.text().toStdString() << std::endl;
}
}
void TrayManager::setActiveOperationLabel() {
auto opName = AppSettings::getInstance().operationName();
auto isPaused = AppSettings::getInstance().isOperationPaused();
pauseOperationAction->setText(tr(isPaused ? "Enable Operation" : "Pause Operation"));
QString opLabel = tr(isPaused ? "Paused" : "Active");
opLabel += tr(" Operation: ");
opLabel += (opName == "") ? QString(tr("<None>")) : QString(opName);
currentOperationMenuAction->setText(opLabel);
}
void TrayManager::onOperationListUpdated(bool success,
const std::vector<dto::Operation>& operations) {
auto currentOp = AppSettings::getInstance().operationSlug();
if (success) {
chooseOpStatusAction->setText(tr("Operations loaded"));
cleanChooseOpSubmenu();
for (const auto& op : operations) {
auto newAction = new QAction(op.name, chooseOpSubmenu);
if (currentOp == op.slug) {
newAction->setCheckable(true);
newAction->setChecked(true);
selectedAction = newAction;
}
connect(newAction, &QAction::triggered, [this, newAction, op] {
AppSettings::getInstance().setOperationDetails(op.slug, op.name);
if (selectedAction != nullptr) {
selectedAction->setChecked(false);
selectedAction->setCheckable(false);
}
newAction->setCheckable(true);
newAction->setChecked(true);
selectedAction = newAction;
});
allOperationActions.push_back(newAction);
chooseOpSubmenu->addAction(newAction);
}
}
else {
chooseOpStatusAction->setText(tr("Unable to load operations"));
}
}
#endif

89
src/traymanager.h Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2020, Verizon Media
// Licensed under the terms of GPLv3. See LICENSE file in project root for terms.
#ifndef WINDOW_H
#define WINDOW_H
#include <QSystemTrayIcon>
#include "db/databaseconnection.h"
#include "dtos/operation.h"
#include "forms/credits/credits.h"
#include "forms/evidence/evidencemanager.h"
#include "forms/settings/settings.h"
#include "helpers/screenshot.h"
#include "hotkeymanager.h"
#include "tools/UGlobalHotkey/uglobalhotkeys.h"
#ifndef QT_NO_SYSTEMTRAYICON
#include <QDialog>
QT_BEGIN_NAMESPACE
class QAction;
class QCheckBox;
class QComboBox;
class QGroupBox;
class QLabel;
class QLineEdit;
class QMenu;
class QPushButton;
class QSpinBox;
class QTextEdit;
QT_END_NAMESPACE
class TrayManager : public QDialog {
Q_OBJECT
public:
TrayManager(DatabaseConnection *);
~TrayManager();
protected:
void closeEvent(QCloseEvent *event) override;
public slots:
void onScreenshotCaptured(const QString &filepath);
void setActiveOperationLabel();
private slots:
void onOperationListUpdated(bool success, const std::vector<dto::Operation> &operations);
private:
void createActions();
void createTrayMenu();
void wireUi();
QAction *quitAction;
QAction *showSettingsAction;
QAction *currentOperationMenuAction;
QAction *captureScreenAreaAction;
QAction *captureWindowAction;
QAction *showEvidenceManagerAction;
QAction *showCreditsAction;
QAction *addCodeblockAction;
void cleanChooseOpSubmenu();
QMenu *chooseOpSubmenu;
QAction *chooseOpStatusAction;
QAction *pauseOperationAction;
QAction *refreshOperationListAction;
QAction *selectedAction = nullptr; // note: do not delete; for reference only
std::vector<QAction *> allOperationActions;
QSystemTrayIcon *trayIcon;
QMenu *trayIconMenu;
Settings *settingsWindow;
EvidenceManager *evidenceManagerWindow;
Credits *creditsWindow;
Screenshot *screenshotTool;
HotkeyManager *hotkeyManager;
DatabaseConnection *db;
};
#endif // QT_NO_SYSTEMTRAYICON
#endif

1
tools/UGlobalHotkey Submodule

@ -0,0 +1 @@
Subproject commit 4b1049f8e5248ac816b6b90dfa0e70c54b9b8306