first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-06-08 17:09:23 -04:00
commit df3a033196
17887 changed files with 8637778 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
---
name: Bug report
about: File a bug report in OJS, OMP or OPS. Try our support forum linked below if you can't provide reproduction steps and technical specifications.
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
Please tell us what happens, what you expected to happen, and why you think it is a bug in the software.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**What application are you using?**
OJS, OMP or OPS version X.X.X
**Additional information**
Please add any screenshots, logs or other information we can use to investigate this bug report.
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Feature Request or Suggestion
url: https://forum.pkp.sfu.ca/c/questions/feature-requests/8
about: Suggest improvements to our software in our feature request forum, where our community meets to discuss and prioritize new features.
- name: Software Support
url: https://forum.pkp.sfu.ca/c/questions/5
about: Need help? Post in our software support forum if you need help debugging your journal, press, or preprint server.
- name: Technical Proposals
url: https://github.com/pkp/pkp-lib/discussions
about: Propose changes to the software's code, such as the adoption of a new library, tool or development technique.
- name: Community Forum
url: https://forum.pkp.sfu.ca/
about: Got a question? Visit our community forum to get help, ask a question, and connect with our publishing community.
+20
View File
@@ -0,0 +1,20 @@
<?php
$finder = PhpCsFixer\Finder::create()
->in(__DIR__)
->name('*.php')
// The next two rules are enabled by default, kept for clarity
->ignoreDotFiles(true)
->ignoreVCS(true)
// The pattern is matched against each found filename, thus:
// - The "/" is needed to avoid having "vendor" match "Newsvendor.php"
// - The presence of "node_modules" here doesn't prevent the Finder from recursing on it, so we merge these paths below at the "exclude()"
->notPath($ignoredDirectories = ['cypress/', 'js/', 'locale/', 'node_modules/', 'styles/', 'templates/', 'vendor/'])
// Ignore root based directories
->exclude(array_merge($ignoredDirectories, ['dtd', 'lib', 'registry', 'schemas', 'xml']));
$rules = include '.php_cs_rules';
$config = new PhpCsFixer\Config();
return $config->setRules($rules)
->setFinder($finder);
+26
View File
@@ -0,0 +1,26 @@
<?php
return [
'@PSR12' => true,
'array_indentation' => true,
'array_syntax' => ['syntax' => 'short'],
'binary_operator_spaces' => true,
'concat_space' => ['spacing' => 'one'],
'explicit_string_variable' => true,
'list_syntax' => ['syntax' => 'short'],
'method_chaining_indentation' => true,
'no_unused_imports' => true,
'no_spaces_around_offset' => true,
'no_superfluous_phpdoc_tags' => true,
'no_whitespace_before_comma_in_array' => true,
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'phpdoc_add_missing_param_annotation' => true,
'phpdoc_no_empty_return' => true,
'phpdoc_order' => true,
'phpdoc_separation' => true,
'phpdoc_var_annotation_correct_order' => true,
'single_quote' => true,
'standardize_increment' => true,
'standardize_not_equals' => true,
'ternary_to_null_coalescing' => true,
];
+674
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>.
+18
View File
@@ -0,0 +1,18 @@
PKP Web Application Library
=======
The PKP Web Application Library (PKP-WAL) is a library shared by [Open Journal Systems (OJS)](https://github.com/pkp/ojs), [Open Conference Systems (OCS)](https://github.com/pkp/ocs), [Open Monograph Press (OMP)](http://github.com/pkp/omp), [Open Preprint Systems (OPS)](https://github.com/pkp/ops) and [Open Harvester Systems (OHS)](https://github.com/pkp/harvester). It is distributed with those applications in the `lib/pkp` subdirectory.
Issues (bugs) for all of those applications should be [created against this repository](https://github.com/pkp/pkp-lib/issues).
## Issues
Issues (bugs) for any of the PKP applications should be created here. If you're not sure whether you're encountering a bug or not, consider posting in the [PKP Community Forum](https://forum.pkp.sfu.ca/).
Before creating a new issue, please [search the existing ones](https://github.com/pkp/pkp-lib/issues) to make sure you're not creating a duplicate.
If you confirm that you have found a new bug, or if you have identified a new feature request, [Create a new issue](https://github.com/pkp/pkp-lib/issues/new/choose) using one of the available templates.
Bugs are scheduled against the milestones that a fix will be released in. For example, if a bug was found in OJS 3.3.0, then it will be scheduled against a subsequent release like OJS 3.3.1 or OJS 3.4, which is the first release that will include the fix.
## Community Code of Conduct
This repository is one of PKP's community spaces and all activities here are guided by [PKP's Code of Conduct](https://pkp.sfu.ca/code-of-conduct/). Please review the Code and help us create a welcoming environment for all participants.
@@ -0,0 +1,117 @@
<?php
/**
* @file api/v1/_dois/BackendDoiHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendDoiHandler
*
* @ingroup api_v1_backend
*
* @brief Handle API requests for backend operations.
*
*/
namespace PKP\API\v1\_dois;
use APP\facades\Repo;
use APP\core\Request;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\DoisEnabledPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\GenreDAO;
use Slim\Http\Request as SlimRequest;
class PKPBackendDoiHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . "/publications/{publicationId:\d+}",
'handler' => [$this, 'editPublication'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
]
]
]);
parent::__construct();
}
/**
* @param Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$this->addPolicy(new DoisEnabledPolicy($request->getContext()));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* @throws \Exception
*/
public function editPublication(SlimRequest $slimRequest, APIResponse $response, array $args): \Slim\Http\Response
{
$context = $this->getRequest()->getContext();
$publication = Repo::publication()->get($args['publicationId']);
if (!$publication) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$submission = Repo::submission()->get($publication->getData('submissionId'));
if ($submission->getData('contextId') !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$params = $this->convertStringsToSchema(\PKP\services\PKPSchemaService::SCHEMA_PUBLICATION, $slimRequest->getParsedBody());
$doi = Repo::doi()->get((int) $params['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
Repo::publication()->edit($publication, ['doiId' => $doi->getId()]);
$publication = Repo::publication()->get($publication->getId());
$submission = Repo::submission()->get($publication->getData('submissionId'));
$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$submission->getData('contextId')])
->getMany();
/** @var GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getByContextId($submission->getData('contextId'))->toArray();
return $response->withJson(Repo::publication()->getSchemaMap($submission, $userGroups, $genres)->map($publication), 200);
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/**
* @file api/v1/_email/PKPEmailHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPEmailHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API request to send bulk email
*
*/
namespace PKP\API\v1\_email;
use APP\facades\Repo;
use Illuminate\Support\Facades\Bus;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\jobs\bulk\BulkEmailSender;
use PKP\mail\Mailer;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Psr\Http\Message\ServerRequestInterface;
class PKPEmailHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = '_email';
$this->_endpoints = [
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'create'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Create a jobs queue to send a bulk email to users in one or
* more user groups
*
* @param array $args arguments
*
* @return APIResponse
*/
public function create(ServerRequestInterface $slimRequest, APIResponse $response, array $args)
{
$context = $this->getRequest()->getContext();
$contextId = $context->getId();
if (!in_array($contextId, (array) $this->getRequest()->getSite()->getData('enableBulkEmails'))) {
return $response->withStatus(403)->withJsonError('api.emails.403.disabled');
}
$requestParams = $slimRequest->getParsedBody();
$params = [];
foreach ($requestParams as $param => $val) {
switch ($param) {
case 'userGroupIds':
if (!is_array($val)) {
$val = strlen(trim($val))
? explode(',', $val)
: [];
}
$params[$param] = array_map('intval', $val);
break;
case 'body':
case 'subject':
$params[$param] = $val;
break;
case 'copy':
$params[$param] = (bool) $val;
break;
}
}
$errors = [];
if (empty($params['body'])) {
$errors['body'] = [__('api.emails.400.missingBody')];
}
if (empty($params['subject'])) {
$errors['subject'] = [__('api.emails.400.missingSubject')];
}
if (empty($params['userGroupIds'])) {
$errors['userGroupIds'] = [__('api.emails.400.missingUserGroups')];
}
if ($errors) {
return $response->withJson($errors, 400);
}
foreach ($params['userGroupIds'] as $userGroupId) {
if (!Repo::userGroup()->contextHasGroup($contextId, $userGroupId)
|| in_array($userGroupId, (array) $context->getData('disableBulkEmailUserGroups'))) {
return $response->withJson([
'userGroupIds' => [__('api.emails.403.notAllowedUserGroup')],
], 400);
}
}
$userIds = Repo::user()->getCollector()
->filterByContextIds([$contextId])
->filterByUserGroupIds($params['userGroupIds'])
->getIds()
->toArray();
if (!empty($params['copy'])) {
$currentUserId = $this->getRequest()->getUser()->getId();
if (!in_array($currentUserId, $userIds)) {
$userIds[] = $currentUserId;
}
}
$batches = array_chunk($userIds, Mailer::BULK_EMAIL_SIZE_LIMIT);
$jobs = [];
foreach ($batches as $userIds) {
$jobs[] = new BulkEmailSender(
$userIds,
$contextId,
$params['subject'],
$params['body'],
$context->getData('contactEmail'),
$context->getData('contactName')
);
}
Bus::batch($jobs)->dispatch();
return $response->withJson([
'totalBulkJobs' => count($batches),
], 200);
}
}
@@ -0,0 +1,144 @@
<?php
/**
* @file api/v1/_library/PKPLibraryHandler.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2003-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPLibraryHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API requests for announcement operations.
*
*/
namespace PKP\API\v1\_library;
use APP\core\Application;
use APP\core\Services;
use APP\file\LibraryFileManager;
use PKP\context\LibraryFile;
use PKP\context\LibraryFileDAO;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Psr\Http\Message\ServerRequestInterface;
class PKPLibraryHandler extends APIHandler
{
public function __construct()
{
$this->_handlerPath = '_library';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getLibrary'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
if ($request->getUserVar('includeSubmissionId')) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments, 'includeSubmissionId'));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a list of all files in the library
*
* @param array $args arguments
*
* @return APIResponse
*/
public function getLibrary(ServerRequestInterface $slimRequest, APIResponse $response, array $args)
{
/** @var LibraryFileDAO $libraryFileDao */
$libraryFileDao = DAORegistry::getDAO('LibraryFileDAO');
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$context = $this->getRequest()->getContext();
$contextId = $context->getId();
$libraryFileManager = new LibraryFileManager($contextId);
$files = [];
$params = $slimRequest->getQueryParams();
if (isset($params['includeSubmissionId'])) {
$result = $libraryFileDao->getBySubmissionId($submission->getId());
while ($file = $result->next()) {
$files[] = $this->fileToResponse($file, $libraryFileManager);
}
}
$result = $libraryFileDao->getByContextId($contextId);
while ($file = $result->next()) {
$files[] = $this->fileToResponse($file, $libraryFileManager);
}
return $response->withJson([
'items' => $files,
'itemsMax' => count($files),
], 200);
}
/**
* Convert a file object to the JSON response object
*/
protected function fileToResponse(LibraryFile $file, LibraryFileManager $libraryFileManager): array
{
$request = Application::get()->getRequest();
$urlArgs = [
'libraryFileId' => $file->getId(),
];
if ($file->getSubmissionId()) {
$urlArgs['submissionId'] = $file->getSubmissionId();
}
return [
'id' => $file->getId(),
'filename' => $file->getServerFileName(),
'name' => $file->getName(null),
'mimetype' => $file->getFileType(),
'documentType' => Services::get('file')->getDocumentType($file->getFileType()),
'submissionId' => $file->getSubmissionId() ?? 0,
'type' => $file->getType(),
'typeName' => __($libraryFileManager->getTitleKeyFromType($file->getType())),
'url' => $request->getDispatcher()->url(
$request,
Application::ROUTE_COMPONENT,
null,
'api.file.FileApiHandler',
'downloadLibraryFile',
null,
$urlArgs
),
];
}
}
@@ -0,0 +1,131 @@
<?php
/**
* @file api/v1/_payments/PKPBackendPaymentsSettingsHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendPaymentsSettingsHandler
*
* @ingroup api_v1_backend
*
* @brief A private API endpoint handler for payment settings. It may be
* possible to deprecate this when we have a working endpoint for plugin
* settings.
*/
namespace PKP\API\v1\_payments;
use APP\core\Services;
use Illuminate\Support\Collection;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\plugins\PluginRegistry;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\interfaces\EntityWriteInterface;
class PKPBackendPaymentsSettingsHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$rootPattern = '/{contextPath}/api/{version}/_payments';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'PUT' => [
[
'pattern' => $rootPattern,
'handler' => [$this, 'edit'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
],
],
],
]);
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Receive requests to edit the payments form
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
*
* @return \PKP\core\APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $slimRequest->getParsedBody();
$contextService = Services::get('context');
// Process query params to format incoming data as needed
foreach ($slimRequest->getParsedBody() as $param => $val) {
switch ($param) {
case 'paymentsEnabled':
$params[$param] = $val === 'true';
break;
case 'currency':
$params[$param] = (string) $val;
break;
}
}
if (isset($params['currency'])) {
$errors = $contextService->validate(
EntityWriteInterface::VALIDATE_ACTION_EDIT,
['currency' => $params['currency']],
$context->getSupportedFormLocales(),
$context->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
}
PluginRegistry::loadCategory('paymethod', true);
Hook::call(
'API::payments::settings::edit',
[
$slimRequest,
$request,
$params,
$updatedSettings = new Collection(),
$errors = new Collection()
]
);
if ($errors->isNotEmpty()) {
return $response->withStatus(400)->withJson($errors->toArray());
}
$context = $contextService->get($context->getId());
$contextService->edit($context, $params, $request);
return $response->withJson(array_merge($params, $updatedSettings->toArray()));
}
}
@@ -0,0 +1,254 @@
<?php
/**
* @file api/v1/_submissions/PKPBackendSubmissionsHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPBackendSubmissionsHandler
*
* @ingroup api_v1_backend
*
* @brief Handle API requests for backend operations.
*
*/
namespace PKP\API\v1\_submissions;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Collector;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request;
use Slim\Http\Response;
abstract class PKPBackendSubmissionsHandler extends APIHandler
{
/** @var int Max items that can be requested */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$rootPattern = '/{contextPath}/api/{version}/_submissions';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => "{$rootPattern}",
'handler' => [$this, 'getMany'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_SUB_EDITOR,
Role::ROLE_ID_AUTHOR,
Role::ROLE_ID_REVIEWER,
Role::ROLE_ID_ASSISTANT,
],
],
],
'DELETE' => [
[
'pattern' => "{$rootPattern}/{submissionId:\d+}",
'handler' => [$this, 'delete'],
'roles' => [
Role::ROLE_ID_SITE_ADMIN,
Role::ROLE_ID_MANAGER,
Role::ROLE_ID_AUTHOR,
],
],
],
]);
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$routeName = $this->getSlimRequest()->getAttribute('route')->getName();
if (in_array($routeName, ['delete'])) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of submissions
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return Response
*/
public function getMany($slimRequest, $response, $args)
{
$request = Application::get()->getRequest();
$currentUser = $request->getUser();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$collector = $this->getSubmissionCollector($slimRequest->getQueryParams());
// Anyone not a manager or site admin can only access their assigned
// submissions
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$canAccessUnassignedSubmission = !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles));
Hook::call('API::submissions::params', [$collector, $slimRequest]);
if (!$canAccessUnassignedSubmission) {
if (!is_array($collector->assignedTo)) {
$collector->assignedTo([$currentUser->getId()]);
} elseif ($collector->assignedTo != [$currentUser->getId()]) {
return $response->withStatus(403)->withJsonError('api.submissions.403.requestedOthersUnpublishedSubmissions');
}
}
$submissions = $collector->getMany();
$userGroups = Repo::userGroup()->getCollector()
->filterByContextIds([$context->getId()])
->getMany();
/** @var \PKP\submission\GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getByContextId($context->getId())->toArray();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::submission()->getSchemaMap()->mapManyToSubmissionsList($submissions, $userGroups, $genres)->values(),
], 200);
}
/**
* Configure a submission Collector based on the query params
*/
protected function getSubmissionCollector(array $queryParams): Collector
{
$request = Application::get()->getRequest();
$context = $request->getContext();
$collector = Repo::submission()->getCollector()
->filterByContextIds([$context->getId()])
->limit(30)
->offset(0);
foreach ($queryParams as $param => $val) {
switch ($param) {
case 'orderBy':
if (in_array($val, [
$collector::ORDERBY_DATE_PUBLISHED,
$collector::ORDERBY_DATE_SUBMITTED,
$collector::ORDERBY_LAST_ACTIVITY,
$collector::ORDERBY_LAST_MODIFIED,
$collector::ORDERBY_SEQUENCE,
$collector::ORDERBY_TITLE,
])) {
$direction = isset($queryParams['orderDirection']) && $queryParams['orderDirection'] === $collector::ORDER_DIR_ASC
? $collector::ORDER_DIR_ASC
: $collector::ORDER_DIR_DESC;
$collector->orderBy($val, $direction);
}
break;
case 'status':
$collector->filterByStatus(array_map('intval', $this->paramToArray($val)));
break;
case 'stageIds':
$collector->filterByStageIds(array_map('intval', $this->paramToArray($val)));
break;
case 'categoryIds':
$collector->filterByCategoryIds(array_map('intval', $this->paramToArray($val)));
break;
case 'assignedTo':
$val = array_map('intval', $this->paramToArray($val));
if ($val == [\PKP\submission\Collector::UNASSIGNED]) {
$val = array_shift($val);
}
$collector->assignedTo($val);
break;
case 'daysInactive':
$collector->filterByDaysInactive((int) $val);
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
case 'count':
$collector->limit(min(self::MAX_COUNT, (int) $val));
break;
case 'isIncomplete':
$collector->filterByIncomplete(true);
break;
case 'isOverdue':
$collector->filterByOverdue(true);
break;
}
}
return $collector;
}
/**
* Delete a submission
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return Response
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$submissionId = (int) $args['submissionId'];
$submission = Repo::submission()->get($submissionId);
if (!$submission) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
if ($context->getId() != $submission->getData('contextId')) {
return $response->withStatus(403)->withJsonError('api.submissions.403.deleteSubmissionOutOfContext');
}
if (!Repo::submission()->canCurrentUserDelete($submission)) {
return $response->withStatus(403)->withJsonError('api.submissions.403.unauthorizedDeleteSubmission');
}
Repo::submission()->delete($submission);
return $response->withJson(true);
}
}
@@ -0,0 +1,247 @@
<?php
/**
* @file api/v1/uploadPublicFile/PKPUploadPublicFileHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPUploadPublicFileHandler
*
* @ingroup api_v1_uploadPublicFile
*
* @brief Handle API requests to upload a file to a user's public directory.
*/
namespace PKP\API\v1\_uploadPublicFile;
use APP\core\Application;
use FilesystemIterator;
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\PKPString;
use PKP\file\FileManager;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
class PKPUploadPublicFileHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = '_uploadPublicFile';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_READER];
$this->_endpoints = [
'OPTIONS' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getOptions'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'uploadFile'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method which adds the necessary response headers to allow
* file uploads
*
* @param \PKP\core\APIResponse $response object
*
* @return \PKP\core\APIResponse
*/
private function getResponse($response)
{
return $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, X-PINGOTHER, X-File-Name, Cache-Control');
}
/**
* Upload a requested file
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function uploadFile($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (empty($_FILES) || empty($_FILES['file'])) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
$siteDir = Core::getBaseDir() . '/' . Config::getVar('files', 'public_files_dir') . '/site';
if (!file_exists($siteDir) || !is_writeable($siteDir)) {
return $response->withStatus(500)->withJsonError('api.publicFiles.500.badFilesDir');
}
$userDir = $siteDir . '/images/' . $request->getUser()->getUsername();
$isUserAllowed = true;
$allowedDirSize = Config::getVar('files', 'public_user_dir_size', 5000) * 1024;
$allowedFileTypes = ['gif', 'jpg', 'png', 'webp'];
Hook::call('API::uploadPublicFile::permissions', [
&$userDir,
&$isUserAllowed,
&$allowedDirSize,
&$allowedFileTypes,
$request,
$this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES),
]);
// Allow plugins to control who can upload files
if (!$isUserAllowed) {
return $response->withStatus(403)->withJsonError('api.publicFiles.403.unauthorized');
}
// Don't allow user to exceed the alotted space in their public directory
$currentSize = 0;
if ($allowedDirSize > 0 && file_exists($userDir)) {
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($userDir, FilesystemIterator::SKIP_DOTS)) as $object) {
$currentSize += $object->getSize();
}
}
if (($currentSize + $_FILES['file']['size']) > $allowedDirSize) {
return $response->withStatus(413)->withJsonError('api.publicFiles.413.noDirSpace', [
'fileUploadSize' => ceil($_FILES['file']['size'] / 1024),
'dirSizeLeft' => ceil(($allowedDirSize - $currentSize) / 1024),
]);
}
$fileManager = new FileManager();
$filename = $fileManager->getUploadedFileName('file');
$filename = trim(
preg_replace(
"/[^a-z0-9\.\-]+/",
'',
str_replace(
[' ', '_', ':'],
'-',
strtolower($filename)
)
)
);
$extension = pathinfo(strtolower(trim($filename)), PATHINFO_EXTENSION);
// Only allow permitted file types
if (!in_array($extension, $allowedFileTypes)) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.extensionNotSupported', [
'fileTypes' => join(__('common.commaListSeparator'), $allowedFileTypes)
]);
}
// Perform additional checks on images
if (in_array($extension, ['gif', 'jpg', 'jpeg', 'png', 'jpe'])) {
if (getimagesize($_FILES['file']['tmp_name']) === false) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.invalidImage');
}
$extensionFromMimeType = $fileManager->getImageExtension(PKPString::mime_content_type($_FILES['file']['tmp_name']));
if ($extensionFromMimeType !== '.' . $extension) {
return $response->withStatus(400)->withJsonError('api.publicFiles.400.mimeTypeNotMatched');
}
}
// Save the file
$destinationPath = $this->_getFilename($siteDir . '/images/' . $request->getUser()->getUsername() . '/' . $filename, $fileManager);
$success = $fileManager->uploadFile('file', $destinationPath);
if ($success === false) {
if ($fileManager->uploadError($filename)) {
switch ($fileManager->getUploadErrorCode($filename)) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
return $this->getResponse($response->withJson([
'url' => $request->getBaseUrl() . '/' .
Config::getVar('files', 'public_files_dir') . '/site/images/' .
$request->getUser()->getUsername() . '/' .
pathinfo($destinationPath, PATHINFO_BASENAME),
]));
}
/**
* Respond affirmatively to a HTTP OPTIONS request with headers which allow
* file uploads
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function getOptions($slimRequest, $response, $args)
{
return $this->getResponse($response);
}
/**
* A recursive function to get a filename that will not overwrite an
* existing file
*
* @param string $path Preferred filename
* @param FileManager $fileManager
*
* @return string
*/
private function _getFilename($path, $fileManager)
{
if ($fileManager->fileExists($path)) {
$pathParts = pathinfo($path);
$filename = $pathParts['filename'] . '-' . md5(microtime()) . '.' . $pathParts['extension'];
if (strlen($filename > 255)) {
$filename = substr($filename, -255, 255);
}
return $this->_getFilename($pathParts['dirname'] . '/' . $filename, $fileManager);
}
return $path;
}
}
@@ -0,0 +1,403 @@
<?php
/**
* @file api/v1/announcements/PKPAnnouncementHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAnnouncementHandler
*
* @ingroup api_v1_announcement
*
* @brief Handle API requests for announcement operations.
*
*/
namespace PKP\API\v1\announcements;
use APP\core\Application;
use APP\core\Request;
use APP\facades\Repo;
use Exception;
use Illuminate\Support\Facades\Bus;
use PKP\announcement\Collector;
use PKP\config\Config;
use PKP\context\Context;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\jobs\notifications\NewAnnouncementNotifyUsers;
use PKP\mail\Mailer;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
class PKPAnnouncementHandler extends APIHandler
{
/** @var int The default number of announcements to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of announcements to return in one request */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'announcements';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{announcementId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
if (!Config::getVar('features', 'site_announcements') && !$request->getContext()) {
return false;
}
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single submission
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function get($slimRequest, $response, $args)
{
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
// The assocId in announcements should always point to the contextId
if ($announcement->getData('assocId') !== $this->getRequest()->getContext()?->getId()) {
return $response->withStatus(404)->withJsonError('api.announcements.400.contextsNotMatched');
}
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Get a collection of announcements
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$collector = Repo::announcement()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'typeIds':
$collector->filterByTypeIds(
array_map('intval', $this->paramToArray($val))
);
break;
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
}
}
if ($this->getRequest()->getContext()) {
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
} else {
$collector->withSiteAnnouncements(Collector::SITE_ONLY);
}
Hook::call('API::submissions::params', [$collector, $slimRequest]);
$announcements = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::announcement()->getSchemaMap()->summarizeMany($announcements)->values(),
], 200);
}
/**
* Add an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $slimRequest->getParsedBody());
$params['assocType'] = Application::get()->getContextAssocType();
$params['assocId'] = $context?->getId();
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();
$errors = Repo::announcement()->validate(null, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$announcement = Repo::announcement()->newDataObject($params);
try {
$announcementId = Repo::announcement()->add($announcement);
} catch (StoreTemporaryFileException $e) {
$announcementId = $e->dataObject->getId();
if ($announcementId) {
$announcement = Repo::announcement()->get($announcementId);
Repo::announcement()->delete($announcement);
}
return $response->withStatus(400)->withJson([
'image' => [__('api.400.errorUploadingImage')]
]);
}
$announcement = Repo::announcement()->get($announcementId);
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN);
if ($context) {
$this->notifyUsers($request, $context, $announcementId, $sendEmail);
}
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Edit an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
if ($announcement->getData('assocType') !== Application::get()->getContextAssocType()) {
throw new Exception('Announcement has an assocType that did not match the context.');
}
// Don't allow to edit an announcement from one context from a different context's endpoint
if ($context?->getId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_ANNOUNCEMENT, $slimRequest->getParsedBody());
$params['id'] = $announcement->getId();
$params['typeId'] ??= null;
$primaryLocale = $context ? $context->getPrimaryLocale() : $request->getSite()->getPrimaryLocale();
$allowedLocales = $context ? $context->getSupportedFormLocales() : $request->getSite()->getSupportedLocales();
$errors = Repo::announcement()->validate($announcement, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
try {
Repo::announcement()->edit($announcement, $params);
} catch (StoreTemporaryFileException $e) {
Repo::announcement()->delete($announcement);
return $response->withStatus(400)->withJson([
'image' => __('api.400.errorUploadingImage')
]);
}
$announcement = Repo::announcement()->get($announcement->getId());
return $response->withJson(Repo::announcement()->getSchemaMap()->map($announcement), 200);
}
/**
* Delete an announcement
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param \PKP\core\APIResponse $response object
* @param array $args arguments
*
* @return \PKP\core\APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$announcement = Repo::announcement()->get((int) $args['announcementId']);
if (!$announcement) {
return $response->withStatus(404)->withJsonError('api.announcements.404.announcementNotFound');
}
if ($announcement->getData('assocType') !== Application::get()->getContextAssocType()) {
throw new Exception('Announcement has an assocType that did not match the context.');
}
// Don't allow to delete an announcement from one context from a different context's endpoint
if ($request->getContext()?->getId() !== $announcement->getData('assocId')) {
return $response->withStatus(403)->withJsonError('api.announcements.400.contextsNotMatched');
}
$announcementProps = Repo::announcement()->getSchemaMap()->map($announcement);
Repo::announcement()->delete($announcement);
return $response->withJson($announcementProps, 200);
}
/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
return array_filter($roleAssignments, fn($key) => $key == Role::ROLE_ID_SITE_ADMIN, ARRAY_FILTER_USE_KEY);
}
/**
* Notify subscribed users
*
* This only works for context-level announcements. There is no way to
* determine users who have subscribed to site-level announcements.
*
* @param bool $sendEmail Whether or not the editor chose to notify users by email
*/
protected function notifyUsers(Request $request, Context $context, int $announcementId, bool $sendEmail): void
{
/** @var NotificationSubscriptionSettingsDAO $notificationSubscriptionSettingsDao */
$notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');
// Notify users
$userIdsToNotify = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);
if ($sendEmail) {
$userIdsToMail = $notificationSubscriptionSettingsDao->getSubscribedUserIds(
[NotificationSubscriptionSettingsDAO::BLOCKED_NOTIFICATION_KEY, NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY],
[PKPNotification::NOTIFICATION_TYPE_NEW_ANNOUNCEMENT],
[$context->getId()]
);
$userIdsToNotifyAndMail = $userIdsToNotify->intersect($userIdsToMail);
$userIdsToNotify = $userIdsToNotify->diff($userIdsToMail);
}
$sender = $request->getUser();
$jobs = [];
foreach ($userIdsToNotify->chunk(PKPNotification::NOTIFICATION_CHUNK_SIZE_LIMIT) as $notifyUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale()
);
}
if (isset($userIdsToNotifyAndMail)) {
foreach ($userIdsToNotifyAndMail->chunk(Mailer::BULK_EMAIL_SIZE_LIMIT) as $notifyAndMailUserIds) {
$jobs[] = new NewAnnouncementNotifyUsers(
$notifyAndMailUserIds,
$context->getId(),
$announcementId,
Locale::getPrimaryLocale(),
$sender
);
}
}
Bus::batch($jobs)->dispatch();
}
}
@@ -0,0 +1,708 @@
<?php
/**
* @file api/v1/contexts/PKPContextHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPContextHandler
*
* @ingroup api_v1_context
*
* @brief Base class to handle API requests for contexts (journals/presses).
*/
namespace PKP\API\v1\contexts;
use APP\core\Application;
use APP\core\Services;
use APP\plugins\IDoiRegistrationAgency;
use APP\services\ContextService;
use APP\template\TemplateManager;
use PKP\context\Context;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\plugins\Plugin;
use PKP\plugins\PluginRegistry;
use PKP\plugins\ThemePlugin;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\security\RoleDAO;
use PKP\services\interfaces\EntityWriteInterface;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response as SlimResponse;
class PKPContextHandler extends APIHandler
{
/** @var string One of the SCHEMA_... constants */
public $schemaName = PKPSchemaService::SCHEMA_CONTEXT;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'contexts';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/theme',
'handler' => [$this, 'getTheme'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'edit'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/theme',
'handler' => [$this, 'editTheme'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/registrationAgency',
'handler' => [$this, 'editDoiRegistrationAgencyPlugin'],
'roles' => $roles,
]
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of contexts
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$defaultParams = [
'count' => 20,
'offset' => 0,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = [];
// Process query params to format incoming data as needed
foreach ($requestParams as $param => $val) {
switch ($param) {
case 'isEnabled':
$allowedParams[$param] = (bool) $val;
break;
case 'searchPhrase':
$allowedParams[$param] = trim($val);
break;
case 'count':
$allowedParams[$param] = min(100, (int) $val);
break;
case 'offset':
$allowedParams[$param] = (int) $val;
break;
}
}
Hook::call('API::contexts::params', [&$allowedParams, $slimRequest]);
// Anyone not a site admin should not be able to access contexts that are
// not enabled
if (empty($allowedParams['isEnabled'])) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$canAccessDisabledContexts = !empty(array_intersect([Role::ROLE_ID_SITE_ADMIN], $userRoles));
if (!$canAccessDisabledContexts) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requestedDisabledContexts');
}
}
$items = [];
$contextsIterator = Services::get('context')->getMany($allowedParams);
$propertyArgs = [
'request' => $request,
'slimRequest' => $slimRequest,
];
foreach ($contextsIterator as $context) {
$items[] = Services::get('context')->getSummaryProperties($context, $propertyArgs);
}
$data = [
'itemsMax' => Services::get('context')->getMax($allowedParams),
'items' => $items,
];
return $response->withJson($data, 200);
}
/**
* Get a single context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$user = $request->getUser();
$contextService = Services::get('context');
$context = $contextService->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// A disabled journal can only be access by site admins and users with a
// manager role in that journal
if (!$context->getEnabled()) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$roleDao = DAORegistry::getDao('RoleDAO'); /** @var RoleDAO $roleDao */
if (!$roleDao->userHasRole($context->getId(), $user->getId(), Role::ROLE_ID_MANAGER)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowed');
}
}
}
$data = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($data, 200);
}
/**
* Get the theme and any theme options for a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$user = $request->getUser();
$contextService = Services::get('context');
$context = $contextService->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// A disabled journal can only be access by site admins and users with a
// manager role in that journal
if (!$context->getEnabled()) {
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$roleDao = DAORegistry::getDao('RoleDAO'); /** @var RoleDAO $roleDao */
if (!$roleDao->userHasRole($context->getId(), $user->getId(), Role::ROLE_ID_MANAGER)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowed');
}
}
}
$allThemes = PluginRegistry::loadCategory('themes', true);
$activeTheme = null;
foreach ($allThemes as $theme) {
if ($context->getData('themePluginPath') === $theme->getDirName()) {
$activeTheme = $theme;
break;
}
}
if (!$activeTheme) {
return $response->withStatus(404)->withJsonError('api.themes.404.themeUnavailable');
}
$data = array_merge(
$activeTheme->getOptionValues($context->getId()),
['themePluginPath' => $theme->getDirName()]
);
ksort($data);
return $response->withJson($data, 200);
}
/**
* Add a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
// This endpoint is only available at the site-wide level
if ($request->getContext()) {
return $response->withStatus(404)->withJsonError('api.submissions.404.siteWideEndpoint');
}
$site = $request->getSite();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$primaryLocale = $site->getPrimaryLocale();
$allowedLocales = $site->getSupportedLocales();
// If the site only supports a single locale, set the context's locales
if (count($allowedLocales) === 1) {
if (!isset($params['primaryLocale'])) {
$params['primaryLocale'] = $primaryLocale;
}
if (!isset($params['supportedLocales'])) {
$params['supportedLocales'] = $allowedLocales;
}
}
$contextService = Services::get('context'); /** @var ContextService $contextService */
$errors = $contextService->validate(EntityWriteInterface::VALIDATE_ACTION_ADD, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$context = Application::getContextDAO()->newDataObject();
$context->setAllData($params);
$context = $contextService->add($context, $request);
$contextProps = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($contextProps, 200);
}
/**
* Edit a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$request->getContext()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!$requestContext && !in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$params['id'] = $contextId;
$site = $request->getSite();
$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$errors = $contextService->validate(EntityWriteInterface::VALIDATE_ACTION_EDIT, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$context = $contextService->edit($context, $params, $request);
$contextProps = $contextService->getFullProperties($context, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($contextProps, 200);
}
/**
* Edit a context's theme and theme options
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function editTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$requestContext) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$allowedRoles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
if (!array_intersect($allowedRoles, $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
$params = $slimRequest->getParsedBody();
// Validate the themePluginPath and allow themes to perform their own validation
$themePluginPath = empty($params['themePluginPath']) ? null : $params['themePluginPath'];
if ($themePluginPath !== $context->getData('themePluginPath')) {
$errors = $contextService->validate(
EntityWriteInterface::VALIDATE_ACTION_EDIT,
['themePluginPath' => $themePluginPath],
$context->getSupportedFormLocales(),
$context->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$newContext = $contextService->edit($context, ['themePluginPath' => $themePluginPath], $request);
}
// Get the appropriate theme plugin
/** @var iterable<ThemePlugin> */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$selectedTheme = null;
foreach ($allThemes as $theme) {
if ($themePluginPath === $theme->getDirName()) {
$selectedTheme = $theme;
break;
}
}
// Run the theme's init() method if a new theme has been selected
if (isset($newContext)) {
$selectedTheme->init();
}
$errors = $selectedTheme->validateOptions($params, $themePluginPath, $context->getId(), $request);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
// Only accept params that are defined in the theme options
$options = $selectedTheme->getOptionsConfig();
foreach ($options as $optionName => $optionConfig) {
if (!array_key_exists($optionName, $params)) {
continue;
}
$selectedTheme->saveOption($optionName, $params[$optionName], $context->getId());
}
// Clear the template cache so that new settings can take effect
$templateMgr = TemplateManager::getManager(Application::get()->getRequest());
$templateMgr->clearTemplateCache();
$templateMgr->clearCssCache();
$data = array_merge(
$selectedTheme->getOptionValues($context->getId()),
['themePluginPath' => $themePluginPath]
);
ksort($data);
return $response->withJson($data, 200);
}
/** @param APIResponse $response */
public function editDoiRegistrationAgencyPlugin(SlimRequest $slimRequest, SlimResponse $response, array $args): SlimResponse
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$contextId = (int) $args['contextId'];
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $contextId) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
// Don't allow to edit the context from the site-wide API, because the
// context's plugins will not be enabled
if (!$requestContext) {
return $response->withStatus(403)->withJsonError('api.contexts.403.requiresContext');
}
/** @var ContextService $contextService */
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!array_intersect([Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER], $userRoles)) {
return $response->withStatus(403)->withJsonError('api.contexts.403.notAllowedEdit');
}
/** @var PKPSchemaService $schemaService */
$schemaService = Services::get('schema');
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_CONTEXT, $slimRequest->getParsedBody());
$contextFullProps = array_flip($schemaService->getFullProps(PKPSchemaService::SCHEMA_CONTEXT));
$contextParams = array_intersect_key(
$params,
$contextFullProps,
);
// Validate the registrationAgency and automatic deposit fields
// and allow agencies to perform their own validation.
if (!empty($contextParams)) {
$errors = $contextService->validate(
ContextService::VALIDATE_ACTION_EDIT,
$contextParams,
$context->getSupportedFormLocales(),
$context->getPrimaryLocale(),
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$contextService->edit(
$context,
$contextParams,
$request
);
}
// Return if no registration agency enabled;
if ($contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] === null) {
return $response->withJson($contextParams, 200);
}
// Get the appropriate agency plugin
$plugins = PluginRegistry::loadCategory('generic', true);
$selectedPlugin = null;
foreach ($plugins as $plugin) {
if (
$contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] === $plugin->getName()
) {
$selectedPlugin = $plugin;
break;
}
}
// Check if it's a registration agency plugin
if (!$selectedPlugin instanceof IDoiRegistrationAgency) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPluginType');
}
// If it's a new/different registration agency plugin, update the enabled DOI types based on
// allowed types per the registration agency plugin
if (
$context->getData(Context::SETTING_CONFIGURED_REGISTRATION_AGENCY) !== $contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] &&
$contextParams[Context::SETTING_CONFIGURED_REGISTRATION_AGENCY] !== null
) {
/** @var Context $newContext */
$newContext = $contextService->get($contextId);
$enabledPubObjectTypes = $newContext->getEnabledDoiTypes();
$allowedPubObjectTypes = $selectedPlugin->getAllowedDoiTypes();
$filteredPubObjectTypes = array_intersect($enabledPubObjectTypes, $allowedPubObjectTypes);
if ($filteredPubObjectTypes != $enabledPubObjectTypes) {
$contextService->edit(
$newContext,
[Context::SETTING_ENABLED_DOI_TYPES => $filteredPubObjectTypes],
$request
);
}
}
$settingsObject = $selectedPlugin->getSettingsObject();
$params = $this->convertStringsToSchema($settingsObject::class, $slimRequest->getParsedBody());
$pluginParams = array_intersect_key(
$params,
(array) $settingsObject->getSchema()->properties,
);
// Validate plugin settings
$errors = $settingsObject->validate($pluginParams);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$this->updateRegistrationAgencyPluginSettings(
$contextId,
$selectedPlugin,
$settingsObject::class,
$pluginParams,
);
return $response->withJson(
array_merge($contextParams, $pluginParams),
200,
);
}
/**
* Delete a context
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
// This endpoint is only available at the site-wide level
if ($this->getRequest()->getContext()) {
return $response->withStatus(404)->withJsonError('api.submissions.404.siteWideEndpoint');
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (!in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)) {
$response->withStatus(403)->withJsonError('api.contexts.403.notAllowedDelete');
}
$contextId = (int) $args['contextId'];
$contextService = Services::get('context');
$context = $contextService->get($contextId);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.contexts.404.contextNotFound');
}
$contextProps = $contextService->getSummaryProperties($context, [
'request' => $this->getRequest(),
'slimRequest' => $slimRequest
]);
$contextService->delete($context);
return $response->withJson($contextProps, 200);
}
/**
* Updates a settings plugin according to a given schema. Used in lieu of a generic plugin settings management workflow.
*
* @param Plugin $plugin Currently configured registration agency plugin. Should also implement IDoiRegistrationAgency
* @param string $schemaName Name of RegistrationAgencySettings child class used as schema name
* @param array $props Plugin properties to update
*/
protected function updateRegistrationAgencyPluginSettings(int $contextId, Plugin $plugin, string $schemaName, array $props): void
{
/** @var PKPSchemaService $schemaService */
$schemaService = Services::get('schema');
$sanitizedProps = $schemaService->sanitize($schemaName, $props);
foreach ($sanitizedProps as $fieldName => $value) {
$plugin->updateSetting($contextId, $fieldName, $value);
}
}
}
+731
View File
@@ -0,0 +1,731 @@
<?php
/**
* @file api/v1/dois/PKPDoiHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPDoiHandler
*
* @ingroup api_v1_dois
*
* @brief Handle API requests for DOI operations.
*
*/
namespace PKP\API\v1\dois;
use APP\core\Application;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\context\Context;
use PKP\core\APIResponse;
use PKP\doi\Doi;
use PKP\doi\exceptions\DoiException;
use PKP\file\TemporaryFileManager;
use PKP\handler\APIHandler;
use PKP\jobs\doi\DepositSubmission;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\DoisEnabledPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPDoiHandler extends APIHandler
{
/** @var int The default number of DOIs to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of DOIs to return in one request */
public const MAX_COUNT = 100;
/** @var array Handlers that must be authorized to access a submission */
public $requiresSubmissionAccess = [];
/** @var array Handlers that must be authorized to write to a publication */
public $requiresPublicationWriteAccess = [];
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'dois';
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/exports/{fileId:\d+}',
'handler' => [$this, 'getExportedFile'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
]
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/assignDois',
'handler' => [$this, 'assignSubmissionDois'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/export',
'handler' => [$this, 'exportSubmissions'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/deposit',
'handler' => [$this, 'depositSubmissions'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markRegistered',
'handler' => [$this, 'markSubmissionsRegistered'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markUnregistered',
'handler' => [$this, 'markSubmissionsUnregistered'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/submissions/markStale',
'handler' => [$this, 'markSubmissionsStale'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/depositAll',
'handler' => [$this, 'depositAllDois'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN]
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{doiId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
]);
parent::__construct();
}
/**
* @param \APP\core\Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
// DOIs must be enabled to access DOI API endpoints
$this->addPolicy(new DoisEnabledPolicy($request->getContext()));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single DOI
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
*/
public function get(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound"');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.contextsNotMatched');
}
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
/**
* Get a collection of DOIs
*/
public function getMany(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$collector = Repo::doi()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'status':
$collector->filterByStatus(array_map('intval', $this->paramToArray($val)));
break;
}
}
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
Hook::call('API::dois::params', [$collector, $slimRequest]);
$dois = $collector->getMany();
return $response->withJson(
[
'itemsMax' => $collector->limit(null)->offset(0)->getCount(),
'items' => Repo::doi()->getSchemaMap()->summarizeMany($dois)->values(),
],
200
);
}
/**
* Add a DOI
*/
public function add(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$request = $this->getRequest();
$context = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DOI, $slimRequest->getParsedBody());
$params['contextId'] = $context->getId();
$errors = Repo::doi()->validate(null, $params);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$doi = Repo::doi()->newDataObject($params);
$id = Repo::doi()->add($doi);
if ($id === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.creationFailed');
}
$doi = Repo::doi()->get($id);
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
/**
* Edit a DOI.
*
* When a pub object type and id are provided as body parameters, the DOI should only be modified for that pub object.
* To prevent the DOI from being modified for other objects it may be assigned to, we must create a new DOI
* and assign it to the object instead of editing the old DOI.
*
* When a pub object type and id are NOT provided, this function will only edit the DOI with ID of `doiId`
* without any side effects.
*/
public function edit(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_DOI, $slimRequest->getParsedBody());
$errors = Repo::doi()->validate($doi, $params);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$pubObjectType = $slimRequest->getParsedBodyParam('pubObjectType');
$pubObjectId = $slimRequest->getParsedBodyParam('pubObjectId');
// Default behaviour, only edits DOI
if (empty($pubObjectType) && empty($pubObjectId)) {
Repo::doi()->edit($doi, $params);
$doi = Repo::doi()->get($doi->getId());
return $response->withJson(Repo::doi()->getSchemaMap()->map($doi), 200);
}
$pubObjectHandler = $this->getPubObjectHandler($pubObjectType);
if (is_null($pubObjectHandler)) {
return $response->withStatus(403)->withJsonError('api.dois.403.pubTypeNotRecognized');
}
// Check pubObject for doiId
$pubObject = $this->getViaPubObjectHandler($pubObjectHandler, $pubObjectId);
if ($pubObject?->getData('doiId') != $doi->getId()) {
return $response->withStatus(404)->withJsonError('api.dois.404.pubObjectNotFound');
}
// Copy DOI object data
$newDoi = clone $doi;
$newDoi->unsetData('id');
$newDoi->setAllData(array_merge($newDoi->getAllData(), ['doi' => $params['doi']]));
$newDoiId = Repo::doi()->add($newDoi);
// Update pubObject with new DOI and remove elsewhere if no longer in use
$this->editViaPubObjectHandler($pubObjectHandler, $pubObject, $newDoiId);
if (!Repo::doi()->isAssigned($doi->getId(), $pubObjectType)) {
Repo::doi()->delete($doi);
}
return $response->withJson(Repo::doi()->getSchemaMap()->map($newDoi), 200);
}
/**
* Delete a DOI
*
* When a pub object type and id are provided as body parameters, the DOI should only be deleted for that object.
* To prevent the DOI from being removed for other objects it may be assigned to, we remove the doiId from the
* pubObject then check if it's in use anywhere else before removing the DOI object directly.
*/
public function delete(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$doi = Repo::doi()->get((int) $args['doiId']);
if (!$doi) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
// The contextId should always point to the requested contextId
if ($doi->getData('contextId') !== $this->getRequest()->getContext()->getId()) {
return $response->withStatus(403)->withJsonError('api.dois.403.editItemOutOfContext');
}
$doiProps = Repo::doi()->getSchemaMap()->map($doi);
$pubObjectType = $slimRequest->getParsedBodyParam('pubObjectType');
$pubObjectId = $slimRequest->getParsedBodyParam('pubObjectId');
// Default behaviour, directly delete DOI
if (empty($pubObjectType) && empty($pubObjectId)) {
Repo::doi()->delete($doi);
return $response->withJson($doiProps, 200);
}
$pubObjectHandler = $this->getPubObjectHandler($pubObjectType);
if (is_null($pubObjectHandler)) {
return $response->withStatus(403)->withJsonError('api.dois.403.pubTypeNotRecognized');
}
// Check pubObject for doiId
$pubObject = $this->getViaPubObjectHandler($pubObjectHandler, $pubObjectId);
if ($pubObject?->getData('doiId') != $doi->getId()) {
return $response->withStatus(404)->withJsonError('api.dois.404.pubObjectNotFound');
}
// Remove reference to DOI from pubObject and remove DOI object if no longer in use elsewhere
$this->editViaPubObjectHandler($pubObjectHandler, $pubObject, null);
if (!Repo::doi()->isAssigned($doi->getId(), $pubObjectType)) {
Repo::doi()->delete($doi);
}
return $response->withJson($doiProps, 200);
}
/**
* Export XML for configured DOI registration agency
*/
public function exportSubmissions(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve and validate submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPubObjectIncluded');
}
/** @var Submission[] $submissions */
$submissions = [];
foreach ($requestIds as $id) {
$submissions[] = Repo::submission()->get($id);
}
if (empty($submissions[0])) {
return $response->withStatus(404)->withJsonError('api.dois.404.doiNotFound');
}
$agency = $context->getConfiguredDoiAgency();
if ($agency === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.noRegistrationAgencyConfigured');
}
// Invoke IDoiRegistrationAgency::exportSubmissions
$responseData = $agency->exportSubmissions($submissions, $context);
if (!empty($responseData['xmlErrors'])) {
return $response->withStatus(400)->withJsonError('api.dois.400.xmlExportFailed');
}
return $response->withJson(['temporaryFileId' => $responseData['temporaryFileId']], 200);
}
/**
* Deposit XML for configured DOI registration agency
*/
public function depositSubmissions(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve and validate the submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
/** @var Context $context */
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
return $response->withStatus(400)->withJsonError('api.dois.400.invalidPubObjectIncluded');
}
$agency = $context->getConfiguredDoiAgency();
if ($agency === null) {
return $response->withStatus(400)->withJsonError('api.dois.400.noRegistrationAgencyConfigured');
}
$doiIdsToUpdate = [];
foreach ($requestIds as $submissionId) {
dispatch(new DepositSubmission($submissionId, $context, $agency));
$doiIdsToUpdate = array_merge($doiIdsToUpdate, Repo::doi()->getDoisForSubmission($submissionId));
}
Repo::doi()->markSubmitted($doiIdsToUpdate);
return $response->withStatus(200);
}
/**
* Mark submission DOIs as registered with a DOI registration agency.
*/
public function markSubmissionsRegistered(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
$submissionTitle = Repo::submission()->get($id)?->getCurrentPublication()->getLocalizedFullTitle() ?? '[' . __('api.dois.404.submissionNotFound') . ']';
return new DoiException(DoiException::SUBMISSION_NOT_PUBLISHED, $submissionTitle, $submissionTitle);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
foreach ($doiIds as $doiId) {
Repo::doi()->markRegistered($doiId);
}
}
return $response->withStatus(200);
}
public function depositAllDois(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$context = $this->getRequest()->getContext();
Repo::doi()->depositAll($context);
return $response->withStatus(200);
}
/**
* Mark submission DOIs as no longer registered with a DOI registration agency.
*/
public function markSubmissionsUnregistered(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
return new DoiException(DoiException::INCORRECT_SUBMISSION_CONTEXT, $id, $id);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
foreach ($doiIds as $doiId) {
Repo::doi()->markUnregistered($doiId);
}
}
return $response->withStatus(200);
}
/**
* Mark submission DOIs as stale, indicating a need to be resubmitted to registration agency with updated metadata.
*/
public function markSubmissionsStale(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if (!count($requestIds)) {
return $response->withStatus(404)->withJsonError('api.dois.404.noPubObjectIncluded');
}
$context = $this->getRequest()->getContext();
$validIds = Repo::submission()
->getCollector()
->filterByContextIds([$context->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
// Items can only be considered stale if they have been deposited/queued for deposit in the first place
->filterByDoiStatuses([Doi::STATUS_SUBMITTED, Doi::STATUS_REGISTERED])
->getIds()
->toArray();
$invalidIds = array_diff($requestIds, $validIds);
if (count($invalidIds)) {
$failedDoiActions = array_map(function (int $id) {
$submissionTitle = Repo::submission()->get($id)?->getCurrentPublication()->getLocalizedFullTitle() ?? '[' . __('api.dois.404.submissionNotFound') . ']';
return new DoiException(DoiException::INCORRECT_STALE_STATUS, $submissionTitle, $submissionTitle);
}, $invalidIds);
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
foreach ($requestIds as $id) {
$doiIds = Repo::doi()->getDoisForSubmission($id);
Repo::doi()->markStale($doiIds);
}
return $response->withStatus(200);
}
/**
* Assign DOIs to submissions
*/
public function assignSubmissionDois(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
// Retrieve submissions
$requestIds = $slimRequest->getParsedBody()['ids'] ?? [];
if ($requestIds == null) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$context = $this->getRequest()->getContext();
$doiPrefix = $context->getData(Context::SETTING_DOI_PREFIX);
if (empty($doiPrefix)) {
return $response->withStatus(403)->withJsonError('api.dois.403.prefixRequired');
}
$failedDoiActions = [];
// Assign DOIs
foreach ($requestIds as $id) {
$submission = Repo::submission()->get($id);
if ($submission !== null) {
if ($submission->getData('contextId') !== $context->getId()) {
$creationFailureResults = [
new DoiException(
DoiException::INCORRECT_SUBMISSION_CONTEXT,
$id,
$id
)
];
} else {
$creationFailureResults = Repo::submission()->createDois($submission);
}
$failedDoiActions = array_merge($failedDoiActions, $creationFailureResults);
}
}
if (!empty($failedDoiActions)) {
return $response->withJson(
[
'failedDoiActions' => array_map(
function (DoiException $item) {
return $item->getMessage();
},
$failedDoiActions
)
],
400
);
}
return $response->withJson(['failedDoiActions' => $failedDoiActions], 200);
}
/**
* Download exported DOI XML from temporary file ID
*/
public function getExportedFile(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$fileId = $args['fileId'];
$currentUser = Application::get()->getRequest()->getUser();
$tempFileManager = new TemporaryFileManager();
$isSuccess = $tempFileManager->downloadById($fileId, $currentUser->getId());
if (!$isSuccess) {
return $response->withStatus(403)->withJsonError('api.403.unauthorized');
}
return $response->withStatus(200);
}
/**
* Gets a "handler" (either a repo or DAO) for a pub object to perform DOI-related operations.
* See PKPDoiHandler::edit() and PKPDoiHandler::delete().
*
* @param string $type One of Repo::doi()::TYPE_*
*
* @return mixed Returns either a repo or, for pub objects without repos, a DAO
*/
protected function getPubObjectHandler(string $type): mixed
{
return match ($type) {
Repo::doi()::TYPE_PUBLICATION => Repo::publication(),
Repo::doi()::TYPE_REPRESENTATION => Repo::galley(),
default => null,
};
}
/**
* Retrieve the pub object with the given ID.
*
* @param mixed $pubObjectHandler Either a repo or DAO for the pub object type
*
* @return mixed The actual pub object
*/
protected function getViaPubObjectHandler(mixed $pubObjectHandler, int $pubObjectId): mixed
{
return $pubObjectHandler->get($pubObjectId);
}
/**
* Edit the DOI ID for the given pub object via the "handler" (repo or DAO).
*
* @param mixed $pubObjectHandler Either a repo or DAO for the pub object type
* @param mixed $pubObject The pub object th edit
*/
protected function editViaPubObjectHandler(mixed $pubObjectHandler, mixed $pubObject, ?int $doiId): void
{
$pubObjectHandler->edit($pubObject, ['doiId' => $doiId]);
}
}
@@ -0,0 +1,295 @@
<?php
/**
* @file api/v1/emailTemplates/PKPEmailTemplateHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPEmailTemplateHandler
*
* @ingroup api_v1_email_templates
*
* @brief Base class to handle API requests for email templates.
*/
namespace PKP\API\v1\emailTemplates;
use APP\core\Application;
use PKP\core\APIResponse;
use PKP\facades\Repo;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPEmailTemplateHandler extends APIHandler
{
public const MAX_PER_PAGE = 100;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'emailTemplates';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => array_merge($roles, [Role::ROLE_ID_SUB_EDITOR, ROLE::ROLE_ID_ASSISTANT]),
],
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'get'],
'roles' => array_merge($roles, [Role::ROLE_ID_SUB_EDITOR, ROLE::ROLE_ID_ASSISTANT]),
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => $roles,
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'edit'],
'roles' => $roles,
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/restoreDefaults',
'handler' => [$this, 'restoreDefaults'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{key}',
'handler' => [$this, 'delete'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextRequiredPolicy($request));
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of email templates
*/
public function getMany(SlimRequest $slimRequest, Response $response, array $args): Response
{
$request = $this->getRequest();
$collector = Repo::emailTemplate()->getCollector($request->getContext()->getId());
// Process query params to format incoming data as needed
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'alternateTo':
$collector->alternateTo($this->paramToArray($val));
break;
case 'isModified':
$collector->isModified((bool) $val);
break;
case 'searchPhrase':
$collector->searchPhrase(trim($val));
break;
case 'count':
$collector->limit(min((int) $val, self::MAX_PER_PAGE));
break;
case 'offset':
$collector->offset((int) $val);
break;
}
}
Hook::call('API::emailTemplates::params', [$collector, $slimRequest]);
$emailTemplates = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::emailTemplate()->getSchemaMap()->summarizeMany($emailTemplates),
], 200);
}
/**
* Get a single email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$emailTemplate = Repo::emailTemplate()->getByKey($request->getContext()->getId(), $args['key']);
if (!$emailTemplate) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Add an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param Response $response object
* @param array $args arguments
*
* @return Response
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_EMAIL_TEMPLATE, $slimRequest->getParsedBody());
$params['contextId'] = $requestContext->getId();
$errors = Repo::emailTemplate()->validate(null, $params, $requestContext);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$emailTemplate = Repo::emailTemplate()->newDataObject($params);
Repo::emailTemplate()->add($emailTemplate);
$emailTemplate = Repo::emailTemplate()->getByKey($emailTemplate->getData('contextId'), $emailTemplate->getData('key'));
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Edit an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$emailTemplate = Repo::emailTemplate()->getByKey($requestContext->getId(), $args['key']);
if (!$emailTemplate) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_EMAIL_TEMPLATE, $slimRequest->getParsedBody());
$params['key'] = $args['key'];
// Only allow admins to change the context an email template is attached to.
// Set the contextId if it has not been passed or the user is not an admin
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
if (isset($params['contextId'])
&& !in_array(Role::ROLE_ID_SITE_ADMIN, $userRoles)
&& $params['contextId'] !== $requestContext->getId()) {
return $response->withStatus(403)->withJsonError('api.emailTemplates.403.notAllowedChangeContext');
} elseif (!isset($params['contextId'])) {
$params['contextId'] = $requestContext->getId();
}
$errors = Repo::emailTemplate()->validate(
$emailTemplate,
$params,
$requestContext
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
Repo::emailTemplate()->edit($emailTemplate, $params);
$emailTemplate = Repo::emailTemplate()->getByKey(
// context ID is null if edited for the first time
$emailTemplate->getData('contextId') ?? $params['contextId'],
$emailTemplate->getData('key')
);
return $response->withJson(Repo::emailTemplate()->getSchemaMap()->map($emailTemplate), 200);
}
/**
* Delete an email template
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$request = $this->getRequest();
$requestContext = $request->getContext();
$emailTemplate = Repo::emailTemplate()->getByKey($requestContext->getId(), $args['key']);
// Only custom email templates can be deleted, so return 404 if no id exists
if (!$emailTemplate || !$emailTemplate->getData('id')) {
return $response->withStatus(404)->withJsonError('api.emailTemplates.404.templateNotFound');
}
$props = Repo::emailTemplate()->getSchemaMap()->map($emailTemplate);
Repo::emailTemplate()->delete($emailTemplate);
return $response->withJson($props, 200);
}
/**
* Restore defaults in the email template settings
*
* @param SlimRequest $slimRequest Slim request object
* @param Response $response object
* @param array $args arguments
*
* @return Response
*/
public function restoreDefaults($slimRequest, $response, $args)
{
$contextId = $this->getRequest()->getContext()->getId();
$deletedKeys = Repo::emailTemplate()->restoreDefaults($contextId);
return $response->withJson($deletedKeys, 200);
}
}
@@ -0,0 +1,302 @@
<?php
/**
* @file api/v1/highlights/HighlightsHandler.php
*
* Copyright (c) 2014-2023 Simon Fraser University
* Copyright (c) 2003-2023 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class HighlightsHandler
*
* @ingroup api_v1_highlights
*
* @brief Handle API requests for highlights.
*
*/
namespace PKP\API\v1\highlights;
use APP\facades\Repo;
use Exception;
use PKP\core\APIResponse;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\handler\APIHandler;
use PKP\highlight\Collector;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
class HighlightsHandler extends APIHandler
{
/** @var int The maximum number of highlights to return in one request */
public const MAX_COUNT = 100;
public function __construct()
{
$this->_handlerPath = 'highlights';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
[
'pattern' => $this->getEndpointPattern() . '/order',
'handler' => [$this, 'order'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{highlightId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
if (!$request->getContext()) {
$roleAssignments = $this->getSiteRoleAssignments($roleAssignments);
}
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single highlight
*/
public function get(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$highlight = Repo::highlight()->get((int) $args['highlightId'], $this->getRequest()->getContext());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
return $response->withJson(
Repo::highlight()
->getSchemaMap()
->map($highlight)
, 200
);
}
/**
* Get a collection of highlights
*/
public function getMany(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$collector = Repo::highlight()->getCollector()
->limit(self::MAX_COUNT)
->offset(0);
if ($this->getRequest()->getContext()) {
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
} else {
$collector->withSiteHighlights(Collector::SITE_ONLY);
}
Hook::run('API::highlights::params', [$collector, $slimRequest]);
$highlights = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::highlight()->getSchemaMap()->summarizeMany($highlights)->values(),
], 200);
}
/**
* Add a highlight
*/
public function add(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_HIGHLIGHT, $slimRequest->getParsedBody());
$params['contextId'] = $context?->getId();
if (!$params['sequence']) {
$params['sequence'] = Repo::highlight()->getNextSequence($context?->getId());
}
$errors = Repo::highlight()->validate(null, $params, $context);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$highlight = Repo::highlight()->newDataObject($params);
try {
$highlightId = Repo::highlight()->add($highlight);
} catch (StoreTemporaryFileException $e) {
$highlight = Repo::highlight()->get($highlightId, $context?->getId());
Repo::highlight()->delete($highlight);
return $response->withStatus(400)->withJson([
'image' => __('api.400.errorUploadingImage')
]);
}
$highlight = Repo::highlight()->get($highlightId, $context?->getId());
return $response->withJson(Repo::highlight()->getSchemaMap()->map($highlight), 200);
}
/**
* Edit a highlight
*/
public function edit(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$highlight = Repo::highlight()->get((int) $args['highlightId'], $context?->getId());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_HIGHLIGHT, $slimRequest->getParsedBody());
$params['id'] = $highlight->getId();
// Not allowed to change the context of a highlight through the API
unset($params['contextId']);
$errors = Repo::highlight()->validate($highlight, $params, $context);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
try {
Repo::highlight()->edit($highlight, $params);
} catch (Exception $e) {
Repo::highlight()->delete($highlight);
return $response->withStatus(400)->withJson([
'image' => __('api.highlights.400.errorUploadingImage')
]);
}
$highlight = Repo::highlight()->get($highlight->getId(), $context?->getId());
return $response->withJson(Repo::highlight()->getSchemaMap()->map($highlight), 200);
}
/**
* Order the highlights
*/
public function order(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$params = $slimRequest->getParsedBody();
$sequence = (array) $params['sequence'];
if (empty($sequence)) {
return $response->withStatus(400)->withJson(['sequence' => __('api.highlights.400.noOrderData')]);
}
$highlights = array_map(
function($item) use ($context) {
return isset($item['id']) && isset($item['sequence'])
? Repo::highlight()->get($item['id'], $context?->getId())
: null;
},
$sequence
);
if (in_array(null, $highlights)) {
return $response->withStatus(400)->withJson(['sequence' => __('api.highlights.400.orderHighlightNotFound')]);
}
foreach ($highlights as $index => $highlight) {
Repo::highlight()->edit($highlight, ['sequence' => $sequence[$index]['sequence']]);
}
$collector = Repo::highlight()
->getCollector()
->limit(self::MAX_COUNT);
if ($context) {
$collector->filterByContextIds([$context->getId()]);
} else {
$collector->withSiteHighlights(Collector::SITE_ONLY);
}
$highlights = $collector->getMany();
return $response->withJson([
'items' => Repo::highlight()->getSchemaMap()->summarizeMany($highlights)->values(),
'itemsMax' => $highlights->count(),
], 200);
}
/**
* Delete a highlight
*/
public function delete(SlimRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$context = $this->getRequest()->getContext();
$highlight = Repo::highlight()->get((int) $args['highlightId'], $context?->getId());
if (!$highlight) {
return $response->withStatus(404)->withJsonError('api.highlights.404.highlightNotFound');
}
$highlightProps = Repo::highlight()->getSchemaMap()->map($highlight);
Repo::highlight()->delete($highlight);
return $response->withJson($highlightProps, 200);
}
/**
* Modify the role assignments so that only
* site admins have access
*/
protected function getSiteRoleAssignments(array $roleAssignments): array
{
return array_filter($roleAssignments, fn($key) => $key == Role::ROLE_ID_SITE_ADMIN, ARRAY_FILTER_USE_KEY);
}
}
@@ -0,0 +1,235 @@
<?php
/**
* @file api/v1/institutions/PKPInstitutionHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPInstitutionHandler
*
* @ingroup api_v1_institutions
*
* @brief Handle API requests for institution operations.
*
*/
namespace PKP\API\v1\institutions;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimHttpRequest;
class PKPInstitutionHandler extends APIHandler
{
/** @var int The default number of institutions to return in one request */
public const DEFAULT_COUNT = 30;
/** @var int The maximum number of institutions to return in one request */
public const MAX_COUNT = 100;
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'institutions';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER],
],
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{institutionId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
$this->addPolicy(new ContextRequiredPolicy($request));
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a single institution
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
if (!Repo::institution()->exists((int) $args['institutionId'], $this->getRequest()->getContext()->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Get a collection of institutions
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$collector = Repo::institution()->getCollector()
->limit(self::DEFAULT_COUNT)
->offset(0);
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'count':
$collector->limit(min((int) $val, self::MAX_COUNT));
break;
case 'offset':
$collector->offset((int) $val);
break;
case 'searchPhrase':
$collector->searchPhrase($val);
break;
}
}
$collector->filterByContextIds([$this->getRequest()->getContext()->getId()]);
Hook::call('API::institutions::params', [$collector, $slimRequest]);
$institutions = $collector->getMany();
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => Repo::institution()->getSchemaMap()->summarizeMany($institutions->values())->values(),
], 200);
}
/**
* Add an institution
*
* @throws \Exception For sending a request to the API endpoint of a particular context.
*/
public function add(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_INSTITUTION, $slimRequest->getParsedBody());
$params['contextId'] = $request->getContext()->getId();
// Convert IP ranges string to array
if (!empty($params['ipRanges'])) {
$params['ipRanges'] = $this->convertIpToArray($params['ipRanges']);
}
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getSupportedFormLocales();
$errors = Repo::institution()->validate(null, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$institution = Repo::institution()->newDataObject($params);
$id = Repo::institution()->add($institution);
$institution = Repo::institution()->get($id);
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Edit an institution
*/
public function edit(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
if (!Repo::institution()->exists((int) $args['institutionId'], $context->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_INSTITUTION, $slimRequest->getParsedBody());
$params['id'] = $institution->getId();
$params['contextId'] = $context->getId();
// Convert IP ranges string to array
if (!empty($params['ipRanges'])) {
$params['ipRanges'] = $this->convertIpToArray($params['ipRanges']);
}
$primaryLocale = $context->getPrimaryLocale();
$allowedLocales = $context->getSupportedFormLocales();
$errors = Repo::institution()->validate($institution, $params, $allowedLocales, $primaryLocale);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
Repo::institution()->edit($institution, $params);
$institution = Repo::institution()->get($institution->getId());
return $response->withJson(Repo::institution()->getSchemaMap()->map($institution), 200);
}
/**
* Delete an institution
*/
public function delete(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
if (!Repo::institution()->exists((int) $args['institutionId'], $this->getRequest()->getContext()->getId())) {
return $response->withStatus(404)->withJsonError('api.institutions.404.institutionNotFound');
}
$institution = Repo::institution()->get((int) $args['institutionId']);
$institutionProps = Repo::institution()->getSchemaMap()->map($institution);
Repo::institution()->delete($institution);
return $response->withJson($institutionProps, 200);
}
/**
* Convert IP ranges string to array
*/
protected function convertIpToArray(string $ipString): array
{
return array_map('trim', explode(PHP_EOL, trim($ipString)));
}
}
+204
View File
@@ -0,0 +1,204 @@
<?php
/**
* @file api/v1/jobs/PKPJobHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPJobHandler
*
* @ingroup api_v1_jobs
*
* @brief Handle API requests for jobs
*
*/
namespace PKP\API\v1\jobs;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class PKPJobHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_apiForAdmin = true;
$this->_handlerPath = 'jobs';
$roles = [Role::ROLE_ID_SITE_ADMIN];
$this->_endpoints = array_merge_recursive($this->_endpoints, [
'GET' => [
[
'pattern' => $this->getEndpointPattern() . '/all',
'handler' => [$this, 'getJobs'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/failed/all',
'handler' => [$this, 'getFailedJobs'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern() . '/redispatch/{jobId:\d+}',
'handler' => [$this, 'redispatchFailedJob'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/redispatch/all',
'handler' => [$this, 'redispatchAllFailedJob'],
'roles' => $roles,
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/failed/delete/{jobId:\d+}',
'handler' => [$this, 'deleteFailedJob'],
'roles' => $roles,
],
],
]);
parent::__construct();
}
/**
* @param \APP\core\Request $request
* @param array $args
* @param array $roleAssignments
*
* @return bool
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get all pending jobs in the queue waiting to get executed
*/
public function getJobs(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$params = $slimRequest->getQueryParams();
$jobs = Repo::job()
->setOutputFormat(Repo::failedJob()::OUTPUT_HTTP)
->setPage($params['page'] ?? 1)
->showJobs();
return $response->withJson([
'data' => $jobs->all(),
'total' => Repo::job()->total(),
'pagination' => [
'lastPage' => $jobs->lastPage(),
'currentPage' => $jobs->currentPage(),
],
], 200);
}
/**
* Get all failed jobs in the failed list
*/
public function getFailedJobs(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$params = $slimRequest->getQueryParams();
$failedJobs = Repo::failedJob()
->setOutputFormat(Repo::failedJob()::OUTPUT_HTTP)
->setPage($params['page'] ?? 1)
->showJobs();
return $response->withJson([
'data' => $failedJobs->all(),
'total' => Repo::failedJob()->total(),
'pagination' => [
'lastPage' => $failedJobs->lastPage(),
'currentPage' => $failedJobs->currentPage(),
],
], 200);
}
/**
* Redispatch all failed jobs back to queue
* It will only redispatch failed jobs that has valid payload attribute
*/
public function redispatchAllFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
if (Repo::failedJob()->total() <= 0) {
return $response->withStatus(406)->withJson([
'errorMessage' => __('api.jobs.406.failedJobEmpty')
]);
}
$redispatableFailedJobs = Repo::failedJob()->getRedispatchableJobsInQueue(null, ['id']);
return Repo::failedJob()->redispatchToQueue(null, $redispatableFailedJobs->pluck('id')->toArray())
? $response->withJson(['message' => __('api.jobs.200.allFailedJobRedispatchedSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobRedispatchedFailed')]);
}
/**
* Redispatch a failed job back to queue
*/
public function redispatchFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$failedJob = Repo::failedJob()->get((int) $args['jobId']);
if (!$failedJob) {
return $response->withStatus(404)->withJson([
'errorMessage' => __('api.jobs.404.failedJobNotFound')
]);
}
if (!$failedJob->payload) {
return $response->withStatus(406)->withJson([
'errorMessage' => __('api.jobs.406.failedJobPayloadMissing')
]);
}
return Repo::failedJob()->redispatchToQueue(null, [$failedJob->id])
? $response->withJson(['message' => __('api.jobs.200.failedJobRedispatchedSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobRedispatchedFailed')]);
}
/**
* Delete a failed job from failed list
*/
public function deleteFailedJob(SlimRequest $slimRequest, APIResponse $response, array $args): Response
{
$failedJob = Repo::failedJob()->get((int) $args['jobId']);
if (!$failedJob) {
return $response->withStatus(404)->withJson([
'errorMessage' => __('api.jobs.404.failedJobNotFound')
]);
}
return $failedJob->delete()
? $response->withJson(['message' => __('api.jobs.200.failedJobDeleteSucceed')], 200)
: $response->withStatus(400)->withJson(['errorMessage' => __('api.jobs.400.failedJobDeleteFailed')]);
}
}
@@ -0,0 +1,106 @@
<?php
/**
* @file api/v1/mailables/MailableHandler.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class MailableHandler
*
* @ingroup api_v1_mailables
*
* @brief Base class to handle API requests for mailables.
*/
namespace PKP\API\v1\mailables;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
use Slim\Http\Response;
class MailableHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'mailables';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/{id}',
'handler' => [$this, 'get'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
// This endpoint is not available at the site-wide level
$this->addPolicy(new ContextRequiredPolicy($request));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get an array of mailables
*/
public function getMany(SlimRequest $slimRequest, Response $response, array $args): Response
{
$mailables = Repo::mailable()->getMany(
$this->getRequest()->getContext(),
$slimRequest->getQueryParam('searchPhrase')
)
->map(fn (string $class) => Repo::mailable()->summarizeMailable($class))
->sortBy('name');
return $response->withJson($mailables->values(), 200);
}
/**
* Get a mailable by its class name
*
* @param APIResponse $response
*/
public function get(SlimRequest $slimRequest, Response $response, array $args): Response
{
$context = $this->getRequest()->getContext();
$mailable = Repo::mailable()->get($args['id'], $context);
if (!$mailable) {
return $response->withStatus(404)->withJsonError('api.mailables.404.mailableNotFound');
}
return $response->withJson(Repo::mailable()->describeMailable($mailable, $context->getId()), 200);
}
}
+256
View File
@@ -0,0 +1,256 @@
<?php
/**
* @file api/v1/site/PKPSiteHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPSiteHandler
*
* @ingroup api_v1_users
*
* @brief Base class to handle API requests for the site object.
*/
namespace PKP\API\v1\site;
use APP\core\Application;
use APP\core\Services;
use APP\template\TemplateManager;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\PluginRegistry;
use PKP\plugins\ThemePlugin;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use Slim\Http\Request as SlimRequest;
class PKPSiteHandler extends APIHandler
{
/** @var string One of the SCHEMA_... constants */
public $schemaName = PKPSchemaService::SCHEMA_SITE;
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'site';
$roles = [Role::ROLE_ID_SITE_ADMIN];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/theme',
'handler' => [$this, 'getTheme'],
'roles' => $roles,
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'edit'],
'roles' => $roles,
],
[
'pattern' => $this->getEndpointPattern() . '/theme',
'handler' => [$this, 'editTheme'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
$siteProps = Services::get('site')
->getFullProperties($request->getSite(), [
'request' => $request,
]);
return $response->withJson($siteProps, 200);
}
/**
* Get the active theme on the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getTheme($slimRequest, $response, $args)
{
$site = $this->getRequest()->getSite();
/** @var ThemePlugin[] */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$activeTheme = null;
foreach ($allThemes as $theme) {
if ($site->getData('themePluginPath') === $theme->getDirName()) {
$activeTheme = $theme;
break;
}
}
if (!$activeTheme) {
return $response->withStatus(404)->withJsonError('api.themes.404.themeUnavailable');
}
$data = array_merge(
$activeTheme->getOptionValues(\PKP\core\PKPApplication::CONTEXT_ID_NONE),
['themePluginPath' => $theme->getDirName()]
);
ksort($data);
return $response->withJson($data, 200);
}
/**
* Edit the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$site = $request->getSite();
$siteService = Services::get('site');
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SITE, $slimRequest->getParsedBody());
$errors = $siteService->validate($params, $site->getSupportedLocales(), $site->getPrimaryLocale());
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
$site = $siteService->edit($site, $params, $request);
$siteProps = $siteService->getFullProperties($site, [
'request' => $request,
'slimRequest' => $slimRequest
]);
return $response->withJson($siteProps, 200);
}
/**
* Edit the active theme and theme options on the site
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function editTheme($slimRequest, $response, $args)
{
$request = $this->getRequest();
$site = $request->getSite();
$siteService = Services::get('site');
$params = $slimRequest->getParsedBody();
// Validate the themePluginPath and allow themes to perform their own validation
$themePluginPath = empty($params['themePluginPath']) ? null : $params['themePluginPath'];
if ($themePluginPath !== $site->getData('themePluginPath')) {
$errors = $siteService->validate(
['themePluginPath' => $themePluginPath],
$site->getSupportedLocales(),
$site->getPrimaryLocale()
);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
$newSite = $siteService->edit($site, ['themePluginPath' => $themePluginPath], $request);
}
// Get the appropriate theme plugin
/** @var iterable<ThemePlugin> */
$allThemes = PluginRegistry::loadCategory('themes', true);
/** @var ?ThemePlugin */
$selectedTheme = null;
foreach ($allThemes as $theme) {
if ($themePluginPath === $theme->getDirName()) {
$selectedTheme = $theme;
break;
}
}
// Run the theme's init() method if a new theme has been selected
if (isset($newSite)) {
$selectedTheme->init();
}
$errors = $selectedTheme->validateOptions($params, $themePluginPath, \PKP\core\PKPApplication::CONTEXT_ID_NONE, $request);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
// Only accept params that are defined in the theme options
$options = $selectedTheme->getOptionsConfig();
foreach ($options as $optionName => $optionConfig) {
if (!array_key_exists($optionName, $params)) {
continue;
}
$selectedTheme->saveOption($optionName, $params[$optionName], \PKP\core\PKPApplication::CONTEXT_ID_NONE);
}
// Clear the template cache so that new settings can take effect
$templateMgr = TemplateManager::getManager(Application::get()->getRequest());
$templateMgr->clearTemplateCache();
$templateMgr->clearCssCache();
$data = array_merge(
$selectedTheme->getOptionValues(\PKP\core\PKPApplication::CONTEXT_ID_NONE),
['themePluginPath' => $themePluginPath]
);
ksort($data);
return $response->withJson($data, 200);
}
}
@@ -0,0 +1,443 @@
<?php
/**
* @file api/v1/stats/contexts/PKPStatsContextHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsContextHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for context statistics.
*
*/
namespace PKP\API\v1\stats\contexts;
use APP\core\Services;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\statistics\PKPStatisticsHelper;
use Slim\Http\Request as SlimHttpRequest;
class PKPStatsContextHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/contexts';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_SITE_ADMIN]
],
[
'pattern' => $this->getEndpointPattern() . '/timeline',
'handler' => [$this, 'getManyTimeline'],
'roles' => [Role::ROLE_ID_SITE_ADMIN]
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{contextId:\d+}/timeline',
'handler' => [$this, 'getTimeline'],
'roles' => $roles
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get total views of the homepages for a set of contexts
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'count' => 30,
'offset' => 0,
'orderDirection' => PKPStatisticsHelper::STATISTICS_ORDER_DESC,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'count',
'offset',
'orderDirection',
'searchPhrase',
'contextIds',
]);
Hook::call('API::stats::contexts::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
if (!in_array($allowedParams['orderDirection'], [PKPStatisticsHelper::STATISTICS_ORDER_ASC, PKPStatisticsHelper::STATISTICS_ORDER_DESC])) {
return $response->withStatus(400)->withJsonError('api.stats.400.invalidOrderDirection');
}
// Identify contexts which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
if (empty($allowedParams['contextIds'])) {
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
}
// Get a list of contexts with their total views matching the params
$statsService = Services::get('contextStats');
$totalMetrics = $statsService->getTotals($allowedParams);
// Get the stats for each context
$items = [];
foreach ($totalMetrics as $totalMetric) {
$contextId = $totalMetric->context_id;
$contextViews = $totalMetric->metric;
if ($responseCSV) {
$items[] = $this->getItemForCSV($contextId, $contextViews);
} else {
$items[] = $this->getItemForJSON($slimRequest, $contextId, $contextViews);
}
}
$itemsMax = $statsService->getCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get a monthly or daily timeline of total views for a set of contexts
*/
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
'searchPhrase',
'contextIds',
]);
Hook::call('API::stats::contexts::timeline::params', [&$allowedParams, $slimRequest]);
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
}
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
// Identify contexts which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedContextIds = empty($allowedParams['contextIds']) ? [] : $allowedParams['contextIds'];
$allowedParams['contextIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedContextIds);
if (empty($allowedParams['contextIds'])) {
$dateStart = empty($allowedParams['dateStart']) ? PKPStatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
$emptyTimeline = Services::get('contextStats')->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
}
return $response->withJson($emptyTimeline, 200);
}
}
$data = Services::get('contextStats')->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* Get a single context's usage statistics
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$request = $this->getRequest();
$context = Services::get('context')->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'dateStart',
'dateEnd',
]);
Hook::call('API::stats::context::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$statsService = Services::get('contextStats');
$contextViews = $statsService->getTotal($context->getId(), $dateStart, $dateEnd);
// Get basic context details for display
$propertyArgs = [
'request' => $request,
'slimRequest' => $slimRequest,
];
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
if ($responseCSV) {
$csvColumnNames = $this->_getContextReportColumnNames();
$items = [$this->getItemForCSV($context->getId(), $contextViews)];
return $response->withCSV($items, $csvColumnNames, 1);
}
return $response->withJson([
'total' => $contextViews,
'context' => $contextProps
], 200);
}
/**
* Get a monthly or daily timeline of total views for a context
*/
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$request = $this->getRequest();
$context = Services::get('context')->get((int) $args['contextId']);
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
// Don't allow to get one context from a different context's endpoint
if ($request->getContext() && $request->getContext()->getId() !== $context->getId()) {
return $response->withStatus(403)->withJsonError('api.contexts.403.contextsDidNotMatch');
}
$defaultParams = [
'timelineInterval' => PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
]);
$allowedParams['contextIds'] = [$context->getId()];
Hook::call('API::stats::context::timeline::params', [&$allowedParams, $slimRequest]);
if (!$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
return $response->withStatus(400)->withJsonError('api.stats.400.wrongTimelineInterval');
}
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$statsService = Services::get('contextStats');
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = Services::get('contextStats')->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* A helper method to filter and sanitize the request params
*
* Only allows the specified params through and enforces variable
* type where needed.
*/
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
{
$returnParams = [];
foreach ($requestParams as $requestParam => $value) {
if (!in_array($requestParam, $allowedParams)) {
continue;
}
switch ($requestParam) {
case 'dateStart':
case 'dateEnd':
case 'timelineInterval':
$returnParams[$requestParam] = $value;
break;
case 'count':
$returnParams[$requestParam] = min(100, (int) $value);
break;
case 'offset':
$returnParams[$requestParam] = (int) $value;
break;
case 'orderDirection':
$returnParams[$requestParam] = strtoupper($value);
break;
case 'contextIds':
if (is_string($value) && strpos($value, ',') > -1) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams[$requestParam] = array_map('intval', $value);
break;
}
}
return $returnParams;
}
/**
* A helper method to get the contextIds param when a searchPhase
* param is also passed.
*
* If the searchPhrase and contextIds params were both passed in the
* request, then we only return IDs that match both conditions.
*/
protected function _processSearchPhrase(string $searchPhrase, array $contextIds = []): array
{
$searchPhraseContextIds = Services::get('context')->getIds(['searchPhrase' => $searchPhrase]);
if (!empty($contextIds)) {
return array_intersect($contextIds, $searchPhraseContextIds->toArray());
}
return $searchPhraseContextIds->toArray();
}
/**
* Get CSV report columns
*/
protected function _getContextReportColumnNames(): array
{
return [
__('common.id'),
__('common.title'),
__('stats.total'),
];
}
/**
* Get CSV row with context index page metrics
*/
protected function getItemForCSV(int $contextId, int $contextViews): array
{
// Get context title for display
$contexts = Services::get('context')->getManySummary([]);
$context = array_filter($contexts, function ($context) use ($contextId) {
return $context->id == $contextId;
});
$title = current($context)->name;
return [
$contextId,
$title,
$contextViews
];
}
/**
* Get JSON data with context index page metrics
*/
protected function getItemForJSON(SlimHttpRequest $slimRequest, int $contextId, int $contextViews): array
{
// Get basic context details for display
$propertyArgs = [
'request' => $this->getRequest(),
'slimRequest' => $slimRequest,
];
$context = Services::get('context')->get($contextId);
$contextProps = Services::get('context')->getSummaryProperties($context, $propertyArgs);
return [
'total' => $contextViews,
'context' => $contextProps,
];
}
/**
* Check if the timeline interval is valid
*/
protected function isValidTimelineInterval(string $interval): bool
{
return in_array($interval, [
PKPStatisticsHelper::STATISTICS_DIMENSION_DAY,
PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH
]);
}
}
@@ -0,0 +1,176 @@
<?php
/**
* @file api/v1/stats/editorial/PKPStatsEditorialHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsEditorialHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for publication statistics.
*
*/
namespace PKP\API\v1\stats\editorial;
use APP\core\Services;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request;
abstract class PKPStatsEditorialHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/editorial';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
[
'pattern' => $this->getEndpointPattern() . '/averages',
'handler' => [$this, 'getAverages'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/** The name of the section ids query param for this application */
abstract public function getSectionIdsQueryParam();
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get editorial stats
*
* Returns information on submissions received, accepted, declined,
* average response times and more.
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response Response
* @param array $args
*
* @return APIResponse Response
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'dateStart':
case 'dateEnd':
$params[$param] = $value;
break;
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
}
}
Hook::call('API::stats::editorial::params', [&$params, $slimRequest]);
$params['contextIds'] = [$request->getContext()->getId()];
$result = $this->_validateStatDates($params);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
return $response->withJson(array_map(
function ($item) {
$item['name'] = __($item['name']);
return $item;
},
Services::get('editorialStats')->getOverview($params)
));
}
/**
* Get yearly averages of editorial stats
*
* Returns information on average submissions received, accepted
* and declined per year.
*
* @param Request $slimRequest Slim request object
* @param APIResponse $response Response
* @param array $args
*
* @return APIResponse Response
*/
public function getAverages($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
}
}
Hook::call('API::stats::editorial::averages::params', [&$params, $slimRequest]);
$params['contextIds'] = [$request->getContext()->getId()];
return $response->withJson(Services::get('editorialStats')->getAverages($params));
}
}
@@ -0,0 +1,980 @@
<?php
/**
* @file api/v1/stats/publications/PKPStatsPublicationHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsPublicationHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for submission statistics.
*
*/
namespace PKP\API\v1\stats\publications;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use APP\statistics\StatisticsHelper;
use APP\submission\Submission;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimHttpRequest;
use Sokil\IsoCodes\IsoCodesFactory;
abstract class PKPStatsPublicationHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/publications';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/timeline',
'handler' => [$this, 'getManyTimeline'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionId:\d+}/timeline',
'handler' => [$this, 'getTimeline'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/files',
'handler' => [$this, 'getManyFiles'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/countries',
'handler' => [$this, 'getManyCountries'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/regions',
'handler' => [$this, 'getManyRegions'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/cities',
'handler' => [$this, 'getManyCities'],
'roles' => $roles
],
],
];
parent::__construct();
}
/** The name of the section ids query param for this application */
abstract public function getSectionIdsQueryParam();
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$routeName = null;
$slimRequest = $this->getSlimRequest();
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
if (!is_null($slimRequest) && ($route = $slimRequest->getAttribute('route'))) {
$routeName = $route->getName();
}
if (in_array($routeName, ['get', 'getTimeline'])) {
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method to filter and sanitize the application specific request params
*/
protected function _processAppSpecificAllowedParams(string $requestParam, mixed $value, array &$returnParams): void
{
}
/**
* Get allowed parameters for getMany methods:
* getMany(), getManyFiles(), getManyCountries(), getManyRegions(), getManyCities
*/
protected function getManyAllowedParams()
{
$allowedParams = [
'dateStart',
'dateEnd',
'count',
'offset',
'orderDirection',
'searchPhrase',
'submissionIds',
];
$allowedParams[] = $this->getSectionIdsQueryParam();
return $allowedParams;
}
/**
* Get allowed parameters for getManyTimeline method
*/
protected function getManyTimelineAllowedParams()
{
$allowedParams = [
'dateStart',
'dateEnd',
'timelineInterval',
'searchPhrase',
'submissionIds',
'type'
];
$allowedParams[] = $this->getSectionIdsQueryParam();
return $allowedParams;
}
/**
* Get usage stats for a set of publications
*
* Returns total views by abstract, all galleys, pdf galleys,
* html galleys, and other galleys.
*/
public function getMany(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getSubmissionReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('publicationStats');
// Get a list of top submissions by total views
$totalMetrics = $statsService->getTotals($allowedParams);
// Get the stats for each submission
$items = [];
foreach ($totalMetrics as $totalMetric) {
$submissionId = $totalMetric->submission_id;
// get abstract, pdf, html and other views for the submission
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$metricsByType = $statsService->getTotalsByType($submissionId, $this->getRequest()->getContext()->getId(), $dateStart, $dateEnd);
if ($responseCSV) {
$items[] = $this->getItemForCSV($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
} else {
$items[] = $this->getItemForJSON($submissionId, $metricsByType['abstract'], $metricsByType['pdf'], $metricsByType['html'], $metricsByType['other']);
}
}
// Get the total count of submissions
$itemsMax = $statsService->getCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getSubmissionReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get the total abstract or files views for a set of submissions
* in a timeline broken down by month or day
*/
public function getManyTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$initAllowedParams = $this->getManyTimelineAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::timeline::params', [&$allowedParams, $slimRequest]);
$statsService = Services::get('publicationStats');
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
$dateStart = empty($allowedParams['dateStart']) ? StatisticsHelper::STATISTICS_EARLIEST_DATE : $allowedParams['dateStart'];
$dateEnd = empty($allowedParams['dateEnd']) ? date('Ymd', strtotime('yesterday')) : $allowedParams['dateEnd'];
$emptyTimeline = $statsService->getEmptyTimelineIntervals($dateStart, $dateEnd, $allowedParams['timelineInterval']);
if ($responseCSV) {
$csvColumnNames = $statsService->getTimelineReportColumnNames();
return $response->withCSV($emptyTimeline, $csvColumnNames, 0);
}
return $response->withJson($emptyTimeline, 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
};
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
if ($responseCSV) {
$csvColumnNames = $statsService->getTimelineReportColumnNames();
return $response->withCSV($data, $csvColumnNames, count($data));
}
return $response->withJson($data, 200);
}
/**
* Get a single submission's usage statistics
*/
public function get(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$allowedParams = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'dateStart',
'dateEnd',
]);
Hook::call('API::stats::publication::params', [&$allowedParams, $slimRequest]);
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
$statsService = Services::get('publicationStats');
// get abstract, pdf, html and other views for the submission
$dateStart = array_key_exists('dateStart', $allowedParams) ? $allowedParams['dateStart'] : null;
$dateEnd = array_key_exists('dateEnd', $allowedParams) ? $allowedParams['dateEnd'] : null;
$metricsByType = $statsService->getTotalsByType($submission->getId(), $request->getContext()->getId(), $dateStart, $dateEnd);
$galleyViews = $metricsByType['pdf'] + $metricsByType['html'] + $metricsByType['other'];
return $response->withJson([
'abstractViews' => $metricsByType['abstract'],
'galleyViews' => $galleyViews,
'pdfViews' => $metricsByType['pdf'],
'htmlViews' => $metricsByType['html'],
'otherViews' => $metricsByType['other'],
'publication' => Repo::submission()->getSchemaMap()->mapToStats($submission),
], 200);
}
/**
* Get the total abstract of files views for a submission broken down by
* month or day
*/
public function getTimeline(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$defaultParams = [
'timelineInterval' => StatisticsHelper::STATISTICS_DIMENSION_MONTH,
];
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, [
'dateStart',
'dateEnd',
'timelineInterval',
'type'
]);
Hook::call('API::stats::publication::timeline::params', [&$allowedParams, $slimRequest]);
$allowedParams['contextIds'] = [$request->getContext()->getId()];
$allowedParams['submissionIds'] = [$submission->getId()];
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION];
if (array_key_exists('type', $allowedParams) && $allowedParams['type'] == 'files') {
$allowedParams['assocTypes'] = [Application::ASSOC_TYPE_SUBMISSION_FILE];
};
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
if (!in_array($allowedParams['timelineInterval'], [StatisticsHelper::STATISTICS_DIMENSION_DAY, StatisticsHelper::STATISTICS_DIMENSION_MONTH])) {
return $response->withStatus(400)->withJsonError('api.stats.400.invalidTimelineInterval');
}
$statsService = Services::get('publicationStats');
$data = $statsService->getTimeline($allowedParams['timelineInterval'], $allowedParams);
return $response->withJson($data, 200);
}
/**
* Get total usage stats for a set of submission files.
*/
public function getManyFiles(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::files::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getFileReportColumnNames();
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('publicationStats');
$filesMetrics = $statsService->getFilesTotals($allowedParams);
$items = $submissionTitles = [];
foreach ($filesMetrics as $fileMetric) {
$submissionId = $fileMetric->submission_id;
$submissionFileId = $fileMetric->submission_file_id;
$downloads = $fileMetric->metric;
$type = $fileMetric->assoc_type;
if (!isset($submissionTitles[$submissionId])) {
$submission = Repo::submission()->get($submissionId);
$submissionTitles[$submissionId] = $submission->getCurrentPublication()->getLocalizedTitle();
}
if ($responseCSV) {
$items[] = $this->getFilesCSVItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
} else {
$items[] = $this->getFilesJSONItem($submissionFileId, $downloads, $type, $submissionId, $submissionTitles[$submissionId]);
}
}
// Get the total count of submissions files
$itemsMax = $statsService->getFilesCount($allowedParams);
if ($responseCSV) {
$csvColumnNames = $this->_getFileReportColumnNames();
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for a set of countries
*
* Returns total count of views, downloads, unique views and unique downloads in a country
*/
public function getManyCountries(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::countries::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top countries by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
// Get the stats for each country
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : $total->country;
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName);
}
}
// Get the total count of countries
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_COUNTRY);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for set of regions
*
* Returns total count of views, downloads, unique views and unique downloads in a region
*/
public function getManyRegions(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::regions::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top regions by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
// Get the stats for each region
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
if (!empty($total->country) && !empty($total->region)) {
$regionCode = $total->country . '-' . $total->region;
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
$regionName = $region ? $region->getLocalName() : $regionCode;
}
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName);
}
}
// Get the total count of regions
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_REGION);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_REGION);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Get usage stats for set of cities
*
* Returns total count of views, downloads, unique views and unique downloads in a city
*/
public function getManyCities(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseCSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_CSV) ? true : false;
$defaultParams = [
'orderDirection' => StatisticsHelper::STATISTICS_ORDER_DESC,
];
$initAllowedParams = $this->getManyAllowedParams();
$requestParams = array_merge($defaultParams, $slimRequest->getQueryParams());
$allowedParams = $this->_processAllowedParams($requestParams, $initAllowedParams);
Hook::call('API::stats::publications::cities::params', [&$allowedParams, $slimRequest]);
// Check/validate, filter and sanitize the request params
try {
$allowedParams = $this->validateParams($allowedParams);
} catch (\Exception $e) {
if ($e->getCode() == 200) {
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
return $response->withCSV([], $csvColumnNames, 0);
}
return $response->withJson([
'items' => [],
'itemsMax' => 0,
], 200);
}
return $response->withStatus($e->getCode())->withJsonError($e->getMessage());
}
$statsService = Services::get('geoStats');
// Get a list of top cities by total views
$totals = $statsService->getTotals($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
// Get the stats for each city
$items = [];
$isoCodes = app(IsoCodesFactory::class);
foreach ($totals as $total) {
$country = !empty($total->country) ? $isoCodes->getCountries()->getByAlpha2($total->country) : null;
$countryName = $country ? $country->getLocalName() : __('stats.unknown');
$regionName = !empty($total->region) ? $total->region : __('stats.unknown');
if (!empty($total->country) && !empty($total->region)) {
$regionCode = $total->country . '-' . $total->region;
$region = $isoCodes->getSubdivisions()->getByCode($regionCode);
$regionName = $region ? $region->getLocalName() : $regionCode;
}
$cityName = !empty($total->city) ? $total->city : __('stats.unknown');
$metric = $total->metric;
$metric_unique = $total->metric_unique;
if ($responseCSV) {
$items[] = $this->getGeoCSVItem($metric, $metric_unique, $countryName, $regionName, $cityName);
} else {
$items[] = $this->getGeoJSONItem($metric, $metric_unique, $countryName, $regionName, $cityName);
}
}
// Get the total count of cities
$itemsMax = $statsService->getCount($allowedParams, StatisticsHelper::STATISTICS_DIMENSION_CITY);
if ($responseCSV) {
$csvColumnNames = $this->_getGeoReportColumnNames(StatisticsHelper::STATISTICS_DIMENSION_CITY);
return $response->withCSV($items, $csvColumnNames, $itemsMax);
}
return $response->withJson([
'items' => $items,
'itemsMax' => $itemsMax,
], 200);
}
/**
* Validate, filter, sanitize the params
*
* @throws \Exception
*/
protected function validateParams(array $allowedParams): array
{
$request = $this->getRequest();
$allowedParams['contextIds'] = [$request->getContext()->getId()];
$result = $this->_validateStatDates($allowedParams);
if ($result !== true) {
throw new \Exception($result, 400);
}
if (array_key_exists('orderDirection', $allowedParams) && !in_array($allowedParams['orderDirection'], [StatisticsHelper::STATISTICS_ORDER_ASC, StatisticsHelper::STATISTICS_ORDER_DESC])) {
throw new \Exception('api.stats.400.invalidOrderDirection', 400);
}
if (array_key_exists('timelineInterval', $allowedParams) && !$this->isValidTimelineInterval($allowedParams['timelineInterval'])) {
throw new \Exception('api.stats.400.invalidTimelineInterval', 400);
}
// Identify submissions which should be included in the results when a searchPhrase is passed
if (!empty($allowedParams['searchPhrase'])) {
$allowedSubmissionIds = empty($allowedParams['submissionIds']) ? [] : $allowedParams['submissionIds'];
$allowedParams['submissionIds'] = $this->_processSearchPhrase($allowedParams['searchPhrase'], $allowedSubmissionIds);
if (empty($allowedParams['submissionIds'])) {
throw new \Exception('', 200);
}
unset($allowedParams['searchPhrase']);
}
return $allowedParams;
}
/**
* Filter and sanitize the request param
*/
protected function _processParam(string $requestParam, mixed $value): array
{
$returnParams = [];
$sectionIdsQueryParam = $this->getSectionIdsQueryParam();
switch ($requestParam) {
case 'dateStart':
case 'dateEnd':
case 'timelineInterval':
case 'searchPhrase':
case 'type':
$returnParams[$requestParam] = $value;
break;
case 'count':
$returnParams[$requestParam] = min(100, (int) $value);
break;
case 'offset':
$returnParams[$requestParam] = (int) $value;
break;
case 'orderDirection':
$returnParams[$requestParam] = strtoupper($value);
break;
case $sectionIdsQueryParam:
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams['pkpSectionIds'] = array_map('intval', $value);
break;
case 'submissionIds':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$returnParams[$requestParam] = array_map('intval', $value);
break;
}
return $returnParams;
}
/**
* A helper method to filter and sanitize the request params
*
* Only allows the specified params through and enforces variable
* type where needed.
*/
protected function _processAllowedParams(array $requestParams, array $allowedParams): array
{
$returnParams = [];
foreach ($requestParams as $requestParam => $value) {
if (!in_array($requestParam, $allowedParams)) {
continue;
}
$returnParams += $this->_processParam($requestParam, $value);
}
// Get the context's earliest date of publication if no start date is set
if (in_array('dateStart', $allowedParams) && !isset($returnParams['dateStart'])) {
$dateRange = Repo::publication()->getDateBoundaries(
Repo::publication()
->getCollector()
->filterByContextIds([$this->getRequest()->getContext()->getId()])
);
$returnParams['dateStart'] = $dateRange->min_date_published;
}
return $returnParams;
}
/**
* A helper method to get the submissionIds param when a searchPhase
* param is also passed.
*
* If the searchPhrase and submissionIds params were both passed in the
* request, then we only return IDs that match both conditions.
*/
protected function _processSearchPhrase(string $searchPhrase, array $submissionIds = []): array
{
$searchPhraseSubmissionIds = Repo::submission()
->getCollector()
->filterByContextIds([Application::get()->getRequest()->getContext()->getId()])
->filterByStatus([Submission::STATUS_PUBLISHED])
->searchPhrase($searchPhrase)
->getIds();
if (!empty($submissionIds)) {
$submissionIds = array_intersect($submissionIds, $searchPhraseSubmissionIds->toArray());
} else {
$submissionIds = $searchPhraseSubmissionIds->toArray();
}
return $submissionIds;
}
/**
* Get column names for the submission CSV report
*/
protected function _getSubmissionReportColumnNames(): array
{
return [
__('common.id'),
__('common.title'),
__('stats.total'),
__('submission.abstractViews'),
__('stats.fileViews'),
__('stats.pdf'),
__('stats.html'),
__('common.other')
];
}
/**
* Get column names for the file CSV report
*/
protected function _getFileReportColumnNames(): array
{
return [
__('common.publication') . ' ' . __('common.id'),
__('submission.title'),
__('common.file') . ' ' . __('common.id'),
__('common.fileName'),
__('common.type'),
__('stats.fileViews'),
];
}
/**
* Get column names for the country, region and city CSV report
*/
protected function _getGeoReportColumnNames(string $scale, bool $withPublication = false): array
{
$publicationColumns = [];
if ($withPublication) {
$publicationColumns = [
__('common.id'),
__('common.title')
];
}
$scaleColumns = [];
if ($scale == StatisticsHelper::STATISTICS_DIMENSION_CITY) {
$scaleColumns = [
__('stats.city'),
__('stats.region'),
__('common.country')
];
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_REGION) {
$scaleColumns = [__('stats.region'), __('common.country')];
} elseif ($scale == StatisticsHelper::STATISTICS_DIMENSION_COUNTRY) {
$scaleColumns = [__('common.country'),];
}
return array_merge(
$publicationColumns,
$scaleColumns,
[__('stats.total'), __('stats.unique')]
);
}
/**
* Get the CSV row with submission metrics
*/
protected function getItemForCSV(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
{
$galleyViews = $pdfViews + $htmlViews + $otherViews;
$totalViews = $abstractViews + $galleyViews;
// Get submission title for display
$submission = Repo::submission()->get($submissionId);
$submissionTitle = $submission->getCurrentPublication()->getLocalizedTitle();
return [
$submissionId,
$submissionTitle,
$totalViews,
$abstractViews,
$galleyViews,
$pdfViews,
$htmlViews,
$otherViews
];
}
/**
* Get the JSON data with submission metrics
*/
protected function getItemForJSON(int $submissionId, int $abstractViews, int $pdfViews, int $htmlViews, int $otherViews): array
{
$galleyViews = $pdfViews + $htmlViews + $otherViews;
// Get basic submission details for display
$submission = Repo::submission()->get($submissionId);
$submissionProps = Repo::submission()->getSchemaMap()->mapToStats($submission);
return [
'abstractViews' => $abstractViews,
'galleyViews' => $galleyViews,
'pdfViews' => $pdfViews,
'htmlViews' => $htmlViews,
'otherViews' => $otherViews,
'publication' => $submissionProps,
];
}
/**
* Get CSV row with file metrics
*/
protected function getFilesCSVItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
{
// Get submission file title for display
$submissionFile = Repo::submissionFile()->get($submissionFileId);
$title = $submissionFile->getLocalizedData('name');
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
return [
$submissionId,
$submissionTitle,
$submissionFileId,
$title,
$type,
$downloads
];
}
/**
* Get JSON data with file metrics
*/
protected function getFilesJSONItem(int $submissionFileId, int $downloads, int $assocType, int $submissionId, string $submissionTitle): array
{
// Get submission file title for display
$submissionFile = Repo::submissionFile()->get($submissionFileId);
$title = $submissionFile->getLocalizedData('name');
$type = $assocType == Application::ASSOC_TYPE_SUBMISSION_FILE ? __('stats.file.type.primaryFile') : __('stats.file.type.suppFile');
return [
'submissionId' => $submissionId,
'submissionTitle' => $submissionTitle,
'submissionFileId' => $submissionFileId,
'fileName' => $title,
'fileType' => $type,
'downloads' => $downloads
];
}
/**
* Get CSV row with geographical (country, region, and/or city) metrics
*/
protected function getGeoCSVItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
{
$item = [];
if (isset($city)) {
$item[] = $city;
}
if (isset($region)) {
$item[] = $region;
}
return array_merge($item, [$country, $metric, $metric_unique]);
}
/**
* Get JSON data with geographical (country, region, and/or city) metrics
*/
protected function getGeoJSONItem(int $metric, int $metric_unique, string $country, ?string $region = null, ?string $city = null): array
{
$item = [];
if (isset($city)) {
$item['city'] = $city;
}
if (isset($region)) {
$item['region'] = $region;
}
return array_merge(
$item,
[
'country' => $country,
'total' => $metric,
'unique' => $metric_unique
]
);
}
/**
* Check if the timeline interval is valid
*/
protected function isValidTimelineInterval(string $interval): bool
{
return in_array($interval, [
StatisticsHelper::STATISTICS_DIMENSION_DAY,
StatisticsHelper::STATISTICS_DIMENSION_MONTH
]);
}
}
@@ -0,0 +1,358 @@
<?php
/**
* @file api/v1/stats/sushi/PKPStatsSushiHandler.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsSushiHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for COUNTER R5 SUSHI statistics.
*
*/
namespace PKP\API\v1\stats\sushi;
use APP\core\Application;
use APP\facades\Repo;
use APP\sushi\PR;
use APP\sushi\PR_P1;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextRequiredPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\sushi\CounterR5Report;
use PKP\sushi\SushiException;
use PKP\validation\ValidatorFactory;
use Slim\Http\Request as SlimHttpRequest;
class PKPStatsSushiHandler extends APIHandler
{
/** @var bool Whether the API is public */
public $isPublic = true;
/**
* Constructor
*/
public function __construct()
{
$site = Application::get()->getRequest()->getSite();
$context = Application::get()->getRequest()->getContext();
if (($site->getData('isSushiApiPublic') !== null && !$site->getData('isSushiApiPublic')) ||
($context->getData('isSushiApiPublic') !== null && !$context->getData('isSushiApiPublic'))) {
$this->isPublic = false;
}
$this->_handlerPath = 'stats/sushi';
$roles = $this->isPublic ? null : [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER];
$this->_endpoints = [
'GET' => $this->getGETDefinitions($roles)
];
parent::__construct();
}
/**
* Get this API's endpoints definitions
*/
protected function getGETDefinitions(array $roles = null): array
{
return [
[
'pattern' => $this->getEndpointPattern() . '/status',
'handler' => [$this, 'getStatus'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/members',
'handler' => [$this, 'getMembers'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports',
'handler' => [$this, 'getReports'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports/pr',
'handler' => [$this, 'getReportsPR'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reports/pr_p1',
'handler' => [$this, 'getReportsPR1'],
'roles' => $roles
],
];
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new ContextRequiredPolicy($request));
if (!$this->isPublic) {
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the current status of the reporting service
*/
public function getStatus(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
// use only the name in the context primary locale to be consistent
$contextName = $context->getName($context->getPrimaryLocale());
return $response->withJson([
'Description' => __('sushi.status.description', ['contextName' => $contextName]),
'Service_Active' => true,
], 200);
}
/**
* Get the list of consortium members related to a Customer_ID
*/
public function getMembers(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
$site = $request->getSite();
$params = $slimRequest->getQueryParams();
if (!isset($params['customer_id'])) {
// error: missing required customer_id
return $response->withJson([
'Code' => 1030,
'Severity' => 'Fatal',
'Message' => 'Insufficient Information to Process Request',
'Data' => __('sushi.exception.1030.missing', ['params' => 'customer_id'])
], 400);
}
$platformId = $context->getPath();
if ($site->getData('isSiteSushiPlatform')) {
$platformId = $site->getData('sushiPlatformID');
}
$institutionName = $institutionId = null;
$customerId = $params['customer_id'];
if (is_numeric($customerId)) {
$customerId = (int) $customerId;
if ($customerId == 0) {
$institutionName = 'The World';
} else {
$institution = Repo::institution()->get($customerId);
if (isset($institution) && $institution->getContextId() == $context->getId()) {
$institutionId = [];
$institutionName = $institution->getLocalizedName();
if (!empty($institution->getROR())) {
$institutionId[] = ['Type' => 'ROR', 'Value' => $institution->getROR()];
}
$institutionId[] = ['Type' => 'Proprietary', 'Value' => $platformId . ':' . $customerId];
}
}
}
if (!isset($institutionName)) {
// error: invalid customer_id
return $response->withJson([
'Code' => 1030,
'Severity' => 'Fatal',
'Message' => 'Insufficient Information to Process Request',
'Data' => __('sushi.exception.1030.invalid', ['params' => 'customer_id'])
], 400);
}
$item = [
'Customer_ID' => $customerId,
'Name' => $institutionName,
];
if (isset($institutionId)) {
$item['Institution_ID'] = $institutionId;
}
return $response->withJson([$item], 200);
}
/**
* Get list of reports supported by the API
*/
public function getReports(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$items = $this->getReportList();
return $response->withJson($items, 200);
}
/**
* Get the application specific list of reports supported by the API
*/
protected function getReportList(): array
{
return [
[
'Report_Name' => 'Platform Master Report',
'Report_ID' => 'PR',
'Release' => '5',
'Report_Description' => __('sushi.reports.pr.description'),
'Path' => 'reports/pr'
],
[
'Report_Name' => 'Platform Usage',
'Report_ID' => 'PR_P1',
'Release' => '5',
'Report_Description' => __('sushi.reports.pr_p1.description'),
'Path' => 'reports/pr_p1'
],
];
}
/**
* COUNTER 'Platform Usage' [PR_P1].
* A customizable report summarizing activity across the Platform (journal, press, or server).
*/
public function getReportsPR(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
return $this->getReportResponse(new PR(), $slimRequest, $response, $args);
}
/**
* COUNTER 'Platform Master Report' [PR].
* This is a Standard View of the Platform Master Report that presents usage for the overall Platform broken down by Metric_Type
*/
public function getReportsPR1(SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
return $this->getReportResponse(new PR_P1(), $slimRequest, $response, $args);
}
/** Validate user input for TSV reports */
protected function _validateUserInput(CounterR5Report $report, array $params): array
{
$request = $this->getRequest();
$context = $request->getContext();
$earliestDate = CounterR5Report::getEarliestDate();
$lastDate = CounterR5Report::getLastDate();
$submissionIds = Repo::submission()->getCollector()->filterByContextIds([$context->getId()])->getIds()->implode(',');
$rules = [
'begin_date' => [
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
'after_or_equal:' . $earliestDate,
'before_or_equal:end_date',
],
'end_date' => [
'regex:/^\d{4}-\d{2}(-\d{2})?$/',
'before_or_equal:' . $lastDate,
'after_or_equal:begin_date',
],
'item_id' => [
// TO-ASK: shell this rather be just validation for positive integer?
'in:' . $submissionIds,
],
'yop' => [
'regex:/^\d{4}((\||-)\d{4})*$/',
],
];
$reportId = $report->getID();
if (in_array($reportId, ['PR', 'TR', 'IR'])) {
$rules['metric_type'] = ['required'];
}
$errors = [];
$validator = ValidatorFactory::make(
$params,
$rules,
[
'begin_date.regex' => __(
'manager.statistics.counterR5Report.settings.wrongDateFormat'
),
'end_date.regex' => __(
'manager.statistics.counterR5Report.settings.wrongDateFormat'
),
'begin_date.after_or_equal' => __(
'stats.dateRange.invalidStartDateMin'
),
'end_date.before_or_equal' => __(
'stats.dateRange.invalidEndDateMax'
),
'begin_date.before_or_equal' => __(
'stats.dateRange.invalidDateRange'
),
'end_date.after_or_equal' => __(
'stats.dateRange.invalidDateRange'
),
'item_id.*' => __(
'manager.statistics.counterR5Report.settings.wrongItemId'
),
'yop.regex' => __(
'manager.statistics.counterR5Report.settings.wrongYOPFormat'
),
]
);
if ($validator->fails()) {
$errors = $validator->errors()->getMessages();
}
return $errors;
}
/**
* Get the requested report
*/
protected function getReportResponse(CounterR5Report $report, SlimHttpRequest $slimRequest, APIResponse $response, array $args): APIResponse
{
$responseTSV = str_contains($slimRequest->getHeaderLine('Accept'), APIResponse::RESPONSE_TSV) ? true : false;
$params = $slimRequest->getQueryParams();
if ($responseTSV) {
$errors = $this->_validateUserInput($report, $params);
if (!empty($errors)) {
return $response->withJson($errors, 400);
}
}
try {
$report->processReportParams($this->getRequest(), $params);
} catch (SushiException $e) {
return $response->withJson($e->getResponseData(), $e->getHttpStatusCode());
}
if ($responseTSV) {
$reportHeader = $report->getTSVReportHeader();
$reportColumnNames = $report->getTSVColumnNames();
$reportItems = $report->getTSVReportItems();
// consider 3030 error (no usage available)
$key = array_search('3030', array_column($report->warnings, 'Code'));
if ($key !== false) {
$error = $report->warnings[$key]['Code'] . ':' . $report->warnings[$key]['Message'] . '(' . $report->warnings[$key]['Data'] . ')';
foreach ($reportHeader as &$headerRow) {
if (in_array('Exceptions', $headerRow)) {
$headerRow[1] =
$headerRow[1] == '' ?
$error :
$headerRow[1] . ';' . $error;
}
}
}
$report = array_merge($reportHeader, [['']], $reportColumnNames, $reportItems);
return $response->withCSV($report, [], count($reportItems), APIResponse::RESPONSE_TSV);
}
$reportHeader = $report->getReportHeader();
$reportItems = $report->getReportItems();
return $response->withJson([
'Report_Header' => $reportHeader,
'Report_Items' => $reportItems,
], 200);
}
}
@@ -0,0 +1,130 @@
<?php
/**
* @file api/v1/stats/users/PKPStatsUserHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsUserHandler
*
* @ingroup api_v1_stats
*
* @brief Handle API requests for publication statistics.
*
*/
namespace PKP\API\v1\stats\users;
use APP\facades\Repo;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
class PKPStatsUserHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'stats/users';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR],
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get user stats
*
* Returns the count of users broken down by roles
*
* @param \Slim\Http\Request $slimRequest Slim request object
* @param object $response Response
* @param array $args
*
* @return object Response
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!$request->getContext()) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$collector = Repo::user()->getCollector();
$dateParams = [];
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'registeredAfter':
$collector->filterRegisteredAfter($value);
$dateParams['dateStart'] = $value;
break;
case 'registeredBefore':
$collector->filterRegisteredBefore($value);
$dateParams['dateEnd'] = $value;
break;
case 'status': switch ($value) {
case 'disabled':
$collector->filterByStatus($collector::STATUS_DISABLED);
break;
case 'all':
$collector->filterByStatus($collector::STATUS_ALL);
break;
default:
case 'active':
$collector->filterByStatus($collector::STATUS_ACTIVE);
break;
}
}
}
Hook::call('API::stats::users::params', [$collector, $slimRequest]);
$collector->filterByContextIds([$request->getContext()->getId()]);
$result = $this->_validateStatDates($dateParams);
if ($result !== true) {
return $response->withStatus(400)->withJsonError($result);
}
return $response->withJson(array_map(
function ($item) {
$item['name'] = __($item['name']);
return $item;
},
Repo::user()->getRolesOverview($collector)
));
}
}
@@ -0,0 +1,590 @@
<?php
/**
* @file api/v1/submissions/PKPSubmissionFileHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPSubmissionFileHandler
*
* @ingroup api_v1_submission
*
* @brief Handle API requests for submission operations.
*
*/
namespace PKP\API\v1\submissions;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use PKP\core\APIResponse;
use PKP\db\DAORegistry;
use PKP\file\FileManager;
use PKP\handler\APIHandler;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\internal\SubmissionFileStageAccessPolicy;
use PKP\security\authorization\SubmissionAccessPolicy;
use PKP\security\authorization\SubmissionFileAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\submission\GenreDAO;
use PKP\submission\reviewRound\ReviewRoundDAO;
use PKP\submissionFile\SubmissionFile;
class PKPSubmissionFileHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'submissions/{submissionId:\d+}/files';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'get'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'add'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
'PUT' => [
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'edit'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}/copy',
'handler' => [$this, 'copy'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR],
],
],
'DELETE' => [
[
'pattern' => $this->getEndpointPattern() . '/{submissionFileId:\d+}',
'handler' => [$this, 'delete'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
];
parent::__construct();
}
//
// Implement methods from PKPHandler
//
public function authorize($request, &$args, $roleAssignments)
{
$route = $this->getSlimRequest()->getAttribute('route');
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
$this->addPolicy(new SubmissionAccessPolicy($request, $args, $roleAssignments));
if ($route->getName() === 'add') {
$params = $this->getSlimRequest()->getParsedBody();
$fileStage = isset($params['fileStage']) ? (int) $params['fileStage'] : 0;
$this->addPolicy(new SubmissionFileStageAccessPolicy($fileStage, SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_MODIFY, 'api.submissionFiles.403.unauthorizedFileStageIdWrite'));
} elseif ($route->getName() === 'getMany') {
// Anyone passing SubmissionAccessPolicy is allowed to access getMany,
// but the endpoint will return different files depending on the user's
// stage assignments.
} else {
$accessMode = $this->getSlimRequest()->getMethod() === 'GET'
? SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ
: SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_MODIFY;
$this->addPolicy(new SubmissionFileAccessPolicy($request, $args, $roleAssignments, $accessMode, (int) $route->getArgument('submissionFileId')));
}
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of submission files
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$params = [];
foreach ($slimRequest->getQueryParams() as $param => $val) {
switch ($param) {
case 'fileStages':
case 'reviewRoundIds':
if (is_string($val)) {
$val = explode(',', $val);
} elseif (!is_array($val)) {
$val = [$val];
}
$params[$param] = array_map('intval', $val);
break;
}
}
$userRoles = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_USER_ROLES);
$stageAssignments = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_ACCESSIBLE_WORKFLOW_STAGES);
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
// @see PKP\submissionFile\Repository::getAssignedFileStages() for excluded file stages
$allowedFileStages = [
SubmissionFile::SUBMISSION_FILE_SUBMISSION,
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_FINAL,
SubmissionFile::SUBMISSION_FILE_COPYEDIT,
SubmissionFile::SUBMISSION_FILE_PROOF,
SubmissionFile::SUBMISSION_FILE_PRODUCTION_READY,
SubmissionFile::SUBMISSION_FILE_ATTACHMENT,
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION,
];
// Managers can access files for submissions they are not assigned to
if (!$stageAssignments && !count(array_intersect([Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN], $userRoles))) {
return $response->withStatus(403)->withJsonError('api.403.unauthorized');
}
// Set the allowed file stages based on stage assignment
// @see PKP\submissionFile\Repository::getAssignedFileStages() for excluded file stages
if ($stageAssignments) {
$allowedFileStages = Repo::submissionFile()
->getAssignedFileStages(
$stageAssignments,
SubmissionFileAccessPolicy::SUBMISSION_FILE_ACCESS_READ
);
}
$fileStages = empty($params['fileStages'])
? $allowedFileStages
: $params['fileStages'];
foreach ($fileStages as $fileStage) {
if (!in_array($fileStage, $allowedFileStages)) {
return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedFileStageId');
}
}
$collector = Repo::submissionFile()
->getCollector()
->filterBySubmissionIds([$submission->getId()])
->filterByFileStages($fileStages);
// Filter by requested review round ids
if (!empty($params['reviewRoundIds'])) {
$reviewRoundIds = $params['reviewRoundIds'];
$allowedReviewRoundIds = [];
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao*/
if (!empty(array_intersect([SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION], $fileStages))) {
$result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_INTERNAL_REVIEW);
while ($reviewRound = $result->next()) {
$allowedReviewRoundIds[] = $reviewRound->getId();
}
}
if (!empty(array_intersect([SubmissionFile::SUBMISSION_FILE_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION], $fileStages))) {
$result = $reviewRoundDao->getBySubmissionId($submission->getId(), WORKFLOW_STAGE_ID_EXTERNAL_REVIEW);
while ($reviewRound = $result->next()) {
$allowedReviewRoundIds[] = $reviewRound->getId();
}
}
foreach ($reviewRoundIds as $reviewRoundId) {
if (!in_array($reviewRoundId, $allowedReviewRoundIds)) {
return $response->withStatus(403)->withJsonError('api.submissionFiles.403.unauthorizedReviewRound');
}
}
$collector->filterByReviewRoundIds($reviewRoundIds);
}
$files = $collector->getMany();
$items = Repo::submissionFile()
->getSchemaMap()
->summarizeMany($files, $this->getFileGenres());
$data = [
'itemsMax' => $files->count(),
'items' => $items->values(),
];
return $response->withJson($data, 200);
}
/**
* Get a single submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Add a new submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function add($slimRequest, $response, $args)
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
if (empty($_FILES)) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
return $this->getUploadErrorResponse($response, $_FILES['file']['error']);
}
$fileManager = new FileManager();
$extension = $fileManager->parseFileExtension($_FILES['file']['name']);
$submissionDir = Repo::submissionFile()
->getSubmissionDir(
$request->getContext()->getId(),
$submission->getId()
);
$fileId = Services::get('file')->add(
$_FILES['file']['tmp_name'],
$submissionDir . '/' . uniqid() . '.' . $extension
);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody());
$params['fileId'] = $fileId;
$params['submissionId'] = $submission->getId();
$params['uploaderUserId'] = (int) $request->getUser()->getId();
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getData('supportedSubmissionLocales');
// Set the name if not passed with the request
if (empty($params['name'])) {
$params['name'][$primaryLocale] = $_FILES['file']['name'];
}
// If no genre has been set and there is only one genre possible, set it automatically
if (empty($params['genreId'])) {
/** @var GenreDAO */
$genreDao = DAORegistry::getDAO('GenreDAO');
$genres = $genreDao->getEnabledByContextId($request->getContext()->getId());
[$firstGenre, $secondGenre] = [$genres->next(), $genres->next()];
if ($firstGenre && !$secondGenre) {
$params['genreId'] = $firstGenre->getId();
}
}
$errors = Repo::submissionFile()
->validate(
null,
$params,
$allowedLocales,
$primaryLocale
);
if (!empty($errors)) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJson($errors);
}
// Review attachments and discussion files can not be uploaded through this API endpoint
$notAllowedFileStages = [
SubmissionFile::SUBMISSION_FILE_NOTE,
SubmissionFile::SUBMISSION_FILE_REVIEW_ATTACHMENT,
SubmissionFile::SUBMISSION_FILE_QUERY,
];
if (in_array($params['fileStage'], $notAllowedFileStages)) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.403.unauthorizedFileStageIdWrite');
}
// A valid review round is required when uploading to a review file stage
$reviewFileStages = [
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION,
SubmissionFile::SUBMISSION_FILE_REVIEW_FILE,
SubmissionFile::SUBMISSION_FILE_REVIEW_REVISION,
];
if (in_array($params['fileStage'], $reviewFileStages)) {
if (empty($params['assocType']) || $params['assocType'] !== Application::ASSOC_TYPE_REVIEW_ROUND || empty($params['assocId'])) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.missingReviewRoundAssocType');
}
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO'); /** @var ReviewRoundDAO $reviewRoundDao */
$reviewRound = $reviewRoundDao->getById($params['assocId']);
$stageId = in_array($params['fileStage'], [SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_FILE, SubmissionFile::SUBMISSION_FILE_INTERNAL_REVIEW_REVISION])
? WORKFLOW_STAGE_ID_INTERNAL_REVIEW
: WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
if (!$reviewRound
|| $reviewRound->getData('submissionId') != $params['submissionId']
|| $reviewRound->getData('stageId') != $stageId) {
Services::get('file')->delete($fileId);
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundSubmissionNotMatch');
}
}
$submissionFile = Repo::submissionFile()
->newDataObject($params);
$submissionFileId = Repo::submissionFile()
->add($submissionFile);
$submissionFile = Repo::submissionFile()
->get($submissionFileId);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Edit a submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function edit($slimRequest, $response, $args)
{
$request = $this->getRequest();
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$params = $this->convertStringsToSchema(PKPSchemaService::SCHEMA_SUBMISSION_FILE, $slimRequest->getParsedBody());
// Don't allow these properties to be modified
unset($params['submissionId'], $params['fileId'], $params['uploaderUserId']);
if (empty($params) && empty($_FILES['file'])) {
return $response->withStatus(400)->withJsonError('api.submissionsFiles.400.noParams');
}
$primaryLocale = $request->getContext()->getPrimaryLocale();
$allowedLocales = $request->getContext()->getData('supportedSubmissionLocales');
$errors = Repo::submissionFile()
->validate(
$submissionFile,
$params,
$allowedLocales,
$primaryLocale
);
if (!empty($errors)) {
return $response->withStatus(400)->withJson($errors);
}
// Upload a new file
if (!empty($_FILES['file'])) {
if ($_FILES['file']['error'] !== UPLOAD_ERR_OK) {
return $this->getUploadErrorResponse($response, $_FILES['file']['error']);
}
$fileManager = new FileManager();
$extension = $fileManager->parseFileExtension($_FILES['file']['name']);
$submissionDir = Repo::submissionFile()
->getSubmissionDir(
$request->getContext()->getId(),
$submission->getId()
);
$fileId = Services::get('file')->add(
$_FILES['file']['tmp_name'],
$submissionDir . '/' . uniqid() . '.' . $extension
);
$params['fileId'] = $fileId;
$params['uploaderUserId'] = $request->getUser()->getId();
if (empty($params['name'])) {
$params['name'][$primaryLocale] = $_FILES['file']['name'];
}
}
Repo::submissionFile()
->edit(
$submissionFile,
$params
);
$submissionFile = Repo::submissionFile()
->get($submissionFile->getId());
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Copy a submission file to another file stage
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function copy($slimRequest, $response, $args)
{
$submission = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION);
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$params = $slimRequest->getParsedBody();
if (empty($params['toFileStage'])) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.noFileStageId');
}
$toFileStage = (int) $params['toFileStage'];
if (!in_array($toFileStage, Repo::submissionFile()->getFileStages())) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.invalidFileStage');
}
// Expect a review round id when copying to a review stage, or use the latest
// round in that stage by default
$reviewRoundId = null;
if (in_array($toFileStage, Repo::submissionFile()->reviewFileStages)) {
if (!empty($params['reviewRoundId'])) {
$reviewRoundId = (int) $params['reviewRoundId'];
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getById($reviewRoundId);
if (!$reviewRound || $reviewRound->getSubmissionId() != $submission->getId()) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundSubmissionNotMatch');
}
} else {
// Use the latest review round of the appropriate stage
$stageId = in_array($toFileStage, SubmissionFile::INTERNAL_REVIEW_STAGES)
? WORKFLOW_STAGE_ID_INTERNAL_REVIEW
: WORKFLOW_STAGE_ID_EXTERNAL_REVIEW;
/** @var ReviewRoundDAO $reviewRoundDao */
$reviewRoundDao = DAORegistry::getDAO('ReviewRoundDAO');
$reviewRound = $reviewRoundDao->getLastReviewRoundBySubmissionId($submission->getId(), $stageId);
if ($reviewRound) {
$reviewRoundId = $reviewRound->getId();
}
}
if ($reviewRoundId === null) {
return $response->withStatus(400)->withJsonError('api.submissionFiles.400.reviewRoundIdRequired');
}
}
$newSubmissionFileId = Repo::submissionFile()->copy(
$submissionFile,
$toFileStage,
$reviewRoundId
);
$newSubmissionFile = Repo::submissionFile()->get($newSubmissionFileId);
$data = Repo::submissionFile()
->getSchemaMap()
->map($newSubmissionFile, $this->getFileGenres());
return $response->withJson($data, 200);
}
/**
* Delete a submission file
*
* @param \Slim\Http\Request $slimRequest
* @param APIResponse $response
* @param array $args arguments
*
* @return APIResponse
*/
public function delete($slimRequest, $response, $args)
{
$submissionFile = $this->getAuthorizedContextObject(Application::ASSOC_TYPE_SUBMISSION_FILE);
$data = Repo::submissionFile()
->getSchemaMap()
->map($submissionFile, $this->getFileGenres());
Repo::submissionFile()->delete($submissionFile);
return $response->withJson($data, 200);
}
/**
* Helper method to get the file genres for the current context
*
* @return \PKP\submission\Genre[]
*/
protected function getFileGenres(): array
{
/** @var GenreDAO $genreDao */
$genreDao = DAORegistry::getDAO('GenreDAO');
return $genreDao->getByContextId($this->getRequest()->getContext()->getId())->toArray();
}
/**
* Helper method to get the appropriate response when an error
* has occurred during a file upload
*
* @param APIResponse $response
* @param int $error One of the UPLOAD_ERR_ constants
*
* @return APIResponse
*/
private function getUploadErrorResponse($response, $error)
{
switch ($error) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
<?php
/**
* @file api/v1/temporaryFiles/PKPTemporaryFilesHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPTemporaryFilesHandler
*
* @ingroup api_v1_users
*
* @brief Handle API requests to upload a file and receive a temporary file ID.
*/
namespace PKP\API\v1\temporaryFiles;
use APP\core\Application;
use APP\core\Services;
use PKP\file\TemporaryFileManager;
use PKP\core\APIResponse;
use PKP\handler\APIHandler;
use PKP\security\authorization\PolicySet;
use PKP\security\authorization\RoleBasedHandlerOperationPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
class PKPTemporaryFilesHandler extends APIHandler
{
/**
* @copydoc APIHandler::__construct()
*/
public function __construct()
{
$this->_handlerPath = 'temporaryFiles';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_REVIEWER, Role::ROLE_ID_AUTHOR, Role::ROLE_ID_ASSISTANT];
$this->_endpoints = [
'OPTIONS' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getOptions'],
'roles' => $roles,
],
],
'POST' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'uploadFile'],
'roles' => $roles,
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$rolePolicy = new PolicySet(PolicySet::COMBINING_PERMIT_OVERRIDES);
foreach ($roleAssignments as $role => $operations) {
$rolePolicy->addPolicy(new RoleBasedHandlerOperationPolicy($request, $role, $operations));
}
$this->addPolicy($rolePolicy);
return parent::authorize($request, $args, $roleAssignments);
}
/**
* A helper method which adds the necessary response headers to allow
* file uploads
*
* @param APIResponse $response object
*
* @return APIResponse
*/
private function getResponse($response)
{
return $response->withHeader('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With, X-PINGOTHER, X-File-Name, Cache-Control');
}
/**
* Upload a requested file
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function uploadFile($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (empty($_FILES)) {
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
}
$temporaryFileManager = new TemporaryFileManager();
$fileName = $temporaryFileManager->getFirstUploadedPostName();
$uploadedFile = $temporaryFileManager->handleUpload($fileName, $request->getUser()->getId());
if ($uploadedFile === false) {
if ($temporaryFileManager->uploadError($fileName)) {
switch ($temporaryFileManager->getUploadErrorCode($fileName)) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
return $response->withStatus(400)->withJsonError('api.files.400.fileSize', ['maxSize' => Application::getReadableMaxFileSize()]);
case UPLOAD_ERR_PARTIAL:
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
case UPLOAD_ERR_NO_FILE:
return $response->withStatus(400)->withJsonError('api.files.400.noUpload');
case UPLOAD_ERR_NO_TMP_DIR:
case UPLOAD_ERR_CANT_WRITE:
case UPLOAD_ERR_EXTENSION:
return $response->withStatus(400)->withJsonError('api.files.400.config');
}
}
return $response->withStatus(400)->withJsonError('api.files.400.uploadFailed');
}
return $this->getResponse($response->withJson([
'id' => $uploadedFile->getId(),
'name' => $uploadedFile->getData('originalFileName'),
'mimetype' => $uploadedFile->getData('filetype'),
'documentType' => Services::get('file')->getDocumentType($uploadedFile->getData('filetype')),
]));
}
/**
* Respond affirmatively to a HTTP OPTIONS request with headers which allow
* file uploads
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getOptions($slimRequest, $response, $args)
{
return $this->getResponse($response);
}
}
+377
View File
@@ -0,0 +1,377 @@
<?php
/**
* @file api/v1/users/PKPUserHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPUserHandler
*
* @ingroup api_v1_users
*
* @brief Base class to handle API requests for user operations.
*
*/
namespace PKP\API\v1\users;
use APP\facades\Repo;
use Exception;
use PKP\core\APIResponse;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use Slim\Http\Request as SlimRequest;
class PKPUserHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'users';
$roles = [Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_MANAGER, Role::ROLE_ID_SUB_EDITOR];
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/reviewers',
'handler' => [$this, 'getReviewers'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/{userId:\d+}',
'handler' => [$this, 'get'],
'roles' => $roles
],
[
'pattern' => $this->getEndpointPattern() . '/report',
'handler' => [$this, 'getReport'],
'roles' => $roles
],
],
];
parent::__construct();
}
/**
* @copydoc PKPHandler::authorize()
*/
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get a collection of users
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getMany($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'assignedToCategory',
'assignedToSection',
'assignedToSubmission',
'assignedToSubmissionStage',
'count',
'offset',
'orderBy',
'orderDirection',
'roleIds',
'searchPhrase',
'status',
]);
$params['contextId'] = $context->getId();
Hook::call('API::users::params', [&$params, $slimRequest]);
$collector = Repo::user()->getCollector();
// Convert from $params array to what the Collector expects
$orderBy = null;
switch ($params['orderBy'] ?? 'id') {
case 'id': $orderBy = $collector::ORDERBY_ID;
break;
case 'givenName': $orderBy = $collector::ORDERBY_GIVENNAME;
break;
case 'familyName': $orderBy = $collector::ORDERBY_FAMILYNAME;
break;
default: throw new Exception('Unknown orderBy specified');
}
$orderDirection = null;
switch ($params['orderDirection'] ?? 'ASC') {
case 'ASC': $orderDirection = $collector::ORDER_DIR_ASC;
break;
case 'DESC': $orderDirection = $collector::ORDER_DIR_DESC;
break;
default: throw new Exception('Unknown orderDirection specified');
}
$collector->assignedTo($params['assignedToSubmission'] ?? null, $params['assignedToSubmissionStage'] ?? null)
->assignedToSectionIds(isset($params['assignedToSection']) ? [$params['assignedToSection']] : null)
->assignedToCategoryIds(isset($params['assignedToCategory']) ? [$params['assignedToCategory']] : null)
->filterByRoleIds($params['roleIds'] ?? null)
->searchPhrase($params['searchPhrase'] ?? null)
->orderBy($orderBy, $orderDirection, [Locale::getLocale(), $request->getSite()->getPrimaryLocale()])
->limit($params['count'] ?? null)
->offset($params['offset'] ?? null)
->filterByStatus($params['status'] ?? $collector::STATUS_ALL);
$users = $collector->getMany();
$map = Repo::user()->getSchemaMap();
$items = [];
foreach ($users as $user) {
$items[] = $map->summarize($user);
}
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => $items,
], 200);
}
/**
* Get a single user
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function get($slimRequest, $response, $args)
{
$request = $this->getRequest();
if (!empty($args['userId'])) {
$user = Repo::user()->get($args['userId']);
}
if (!$user) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$data = Repo::user()->getSchemaMap()->map($user);
return $response->withJson($data, 200);
}
/**
* Get a collection of reviewers
*
* @param SlimRequest $slimRequest Slim request object
* @param APIResponse $response object
* @param array $args arguments
*
* @return APIResponse
*/
public function getReviewers($slimRequest, $response, $args)
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = $this->_processAllowedParams($slimRequest->getQueryParams(), [
'averageCompletion',
'count',
'daysSinceLastAssignment',
'offset',
'orderBy',
'orderDirection',
'reviewerRating',
'reviewsActive',
'reviewsCompleted',
'reviewStage',
'searchPhrase',
'reviewerIds',
'status',
]);
Hook::call('API::users::reviewers::params', [&$params, $slimRequest]);
$collector = Repo::user()->getCollector()
->filterByContextIds([$context->getId()])
->includeReviewerData()
->filterByRoleIds([Role::ROLE_ID_REVIEWER])
->filterByWorkflowStageIds([$params['reviewStage']])
->searchPhrase($params['searchPhrase'] ?? null)
->filterByReviewerRating($params['reviewerRating'] ?? null)
->filterByReviewsCompleted($params['reviewsCompleted'][0] ?? null)
->filterByReviewsActive(...($params['reviewsActive'] ?? []))
->filterByDaysSinceLastAssignment(...($params['daysSinceLastAssignment'] ?? []))
->filterByAverageCompletion($params['averageCompletion'][0] ?? null)
->filterByUserIds($params['reviewerIds'] ?? null)
->limit($params['count'] ?? null)
->offset($params['offset'] ?? null);
$usersCollection = $collector->getMany();
$items = [];
$map = Repo::user()->getSchemaMap();
foreach ($usersCollection as $user) {
$items[] = $map->summarizeReviewer($user);
}
return $response->withJson([
'itemsMax' => $collector->limit(null)->offset(null)->getCount(),
'items' => $items,
], 200);
}
/**
* Convert the query params passed to the end point. Exclude unsupported
* params and coerce the type of those passed.
*
* @param array $params Key/value of request params
* @param array $allowedKeys The param keys which should be processed and returned
*
* @return array
*/
private function _processAllowedParams($params, $allowedKeys)
{
// Merge query params over default params
$defaultParams = [
'count' => 20,
'offset' => 0,
];
$requestParams = array_merge($defaultParams, $params);
// Process query params to format incoming data as needed
$returnParams = [];
foreach ($requestParams as $param => $val) {
if (!in_array($param, $allowedKeys)) {
continue;
}
switch ($param) {
case 'orderBy':
if (in_array($val, ['id', 'familyName', 'givenName'])) {
$returnParams[$param] = $val;
}
break;
case 'orderDirection':
$returnParams[$param] = $val === 'ASC' ? $val : 'DESC';
break;
case 'status':
if (in_array($val, ['all', 'active', 'disabled'])) {
$returnParams[$param] = $val;
}
break;
// Always convert roleIds to array
case 'reviewerIds':
case 'roleIds':
if (is_string($val)) {
$val = explode(',', $val);
} elseif (!is_array($val)) {
$val = [$val];
}
$returnParams[$param] = array_map('intval', $val);
break;
case 'assignedToCategory':
case 'assignedToSection':
case 'assignedToSubmissionStage':
case 'assignedToSubmission':
case 'reviewerRating':
case 'reviewStage':
case 'offset':
case 'searchPhrase':
$returnParams[$param] = trim($val);
break;
case 'reviewsCompleted':
case 'reviewsActive':
case 'daysSinceLastAssignment':
case 'averageCompletion':
if (is_array($val)) {
$val = array_map('intval', $val);
} elseif (strpos($val, '-') !== false) {
$val = array_map('intval', explode('-', $val));
} else {
$val = [(int) $val];
}
$returnParams[$param] = $val;
break;
// Enforce a maximum count per request
case 'count':
$returnParams[$param] = min(100, (int) $val);
break;
}
}
return $returnParams;
}
/**
* Retrieve the user report
*/
public function getReport(SlimRequest $slimRequest, APIResponse $response, array $args): ?APIResponse
{
$request = $this->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$params = ['contextIds' => [$context->getId()]];
foreach ($slimRequest->getQueryParams() as $param => $value) {
switch ($param) {
case 'userGroupIds':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = array_map('intval', $value);
break;
case 'mappings':
if (is_string($value) && str_contains($value, ',')) {
$value = explode(',', $value);
} elseif (!is_array($value)) {
$value = [$value];
}
$params[$param] = $value;
break;
}
}
Hook::call('API::users::user::report::params', [&$params, $slimRequest]);
$this->getApp()->getContainer()->get('settings')->replace(['outputBuffering' => false]);
$report = Repo::user()->getReport($params);
header('content-type: text/comma-separated-values');
header('content-disposition: attachment; filename="user-report-' . date('Y-m-d') . '.csv"');
$report->serialize(fopen('php://output', 'w+'));
exit;
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
/**
* @file api/v1/vocabs/PKPVocabHandler.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPVocabHandler
*
* @ingroup api_v1_vocab
*
* @brief Handle API requests for controlled vocab operations.
*
*/
namespace PKP\API\v1\vocabs;
use APP\core\Application;
use PKP\core\APIResponse;
use PKP\core\PKPString;
use PKP\db\DAORegistry;
use PKP\facades\Locale;
use PKP\handler\APIHandler;
use PKP\plugins\Hook;
use PKP\security\authorization\ContextAccessPolicy;
use PKP\security\authorization\UserRolesRequiredPolicy;
use PKP\security\Role;
use PKP\submission\SubmissionAgencyDAO;
use PKP\submission\SubmissionDisciplineDAO;
use PKP\submission\SubmissionKeywordDAO;
use PKP\submission\SubmissionLanguageDAO;
use PKP\submission\SubmissionSubjectDAO;
use Slim\Http\Request;
use Stringy\Stringy;
class PKPVocabHandler extends APIHandler
{
/**
* Constructor
*/
public function __construct()
{
$this->_handlerPath = 'vocabs';
$this->_endpoints = [
'GET' => [
[
'pattern' => $this->getEndpointPattern(),
'handler' => [$this, 'getMany'],
'roles' => [Role::ROLE_ID_MANAGER, Role::ROLE_ID_SITE_ADMIN, Role::ROLE_ID_SUB_EDITOR, Role::ROLE_ID_ASSISTANT, Role::ROLE_ID_AUTHOR],
],
],
];
parent::__construct();
}
//
// Implement methods from PKPHandler
//
public function authorize($request, &$args, $roleAssignments)
{
$this->addPolicy(new UserRolesRequiredPolicy($request), true);
$this->addPolicy(new ContextAccessPolicy($request, $roleAssignments));
return parent::authorize($request, $args, $roleAssignments);
}
/**
* Get the controlled vocab entries available in this context
*/
public function getMany(Request $slimRequest, APIResponse $response, array $args): APIResponse
{
$request = Application::get()->getRequest();
$context = $request->getContext();
if (!$context) {
return $response->withStatus(404)->withJsonError('api.404.resourceNotFound');
}
$requestParams = $slimRequest->getQueryParams();
$vocab = $requestParams['vocab'] ?? '';
$locale = $requestParams['locale'] ?? Locale::getLocale();
$term = $requestParams['term'] ?? null;
if (!in_array($locale, $context->getData('supportedSubmissionLocales'))) {
return $response->withStatus(400)->withJsonError('api.vocabs.400.localeNotSupported', ['locale' => $locale]);
}
switch ($vocab) {
case SubmissionKeywordDAO::CONTROLLED_VOCAB_SUBMISSION_KEYWORD:
$submissionKeywordEntryDao = DAORegistry::getDAO('SubmissionKeywordEntryDAO'); /** @var \PKP\submission\SubmissionKeywordEntryDAO $submissionKeywordEntryDao */
$entries = $submissionKeywordEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionSubjectDAO::CONTROLLED_VOCAB_SUBMISSION_SUBJECT:
$submissionSubjectEntryDao = DAORegistry::getDAO('SubmissionSubjectEntryDAO'); /** @var \PKP\submission\SubmissionSubjectEntryDAO $submissionSubjectEntryDao */
$entries = $submissionSubjectEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionDisciplineDAO::CONTROLLED_VOCAB_SUBMISSION_DISCIPLINE:
$submissionDisciplineEntryDao = DAORegistry::getDAO('SubmissionDisciplineEntryDAO'); /** @var \PKP\submission\SubmissionDisciplineEntryDAO $submissionDisciplineEntryDao */
$entries = $submissionDisciplineEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
case SubmissionLanguageDAO::CONTROLLED_VOCAB_SUBMISSION_LANGUAGE:
$words = array_filter(PKPString::regexp_split('/\s+/', $term), 'strlen');
$languageNames = [];
foreach (Locale::getLanguages() as $language) {
if ($language->getAlpha2() && $language->getType() === 'L' && $language->getScope() === 'I' && Stringy::create($language->getLocalName())->containsAny($words, false)) {
$languageNames[] = $language->getLocalName();
}
}
asort($languageNames);
return $response->withJson($languageNames, 200);
case SubmissionAgencyDAO::CONTROLLED_VOCAB_SUBMISSION_AGENCY:
$submissionAgencyEntryDao = DAORegistry::getDAO('SubmissionAgencyEntryDAO'); /** @var \PKP\submission\SubmissionAgencyEntryDAO $submissionAgencyEntryDao */
$entries = $submissionAgencyEntryDao->getByContextId($vocab, $context->getId(), $locale, $term)->toArray();
break;
default:
$entries = [];
Hook::call('API::vocabs::getMany', [$vocab, &$entries, $slimRequest, $response, $request]);
}
$data = [];
foreach ($entries as $entry) {
$data[] = $entry->getData($vocab, $locale);
}
$data = array_values(array_unique($data));
return $response->withJson($data, 200);
}
}
@@ -0,0 +1,346 @@
<?php
/**
* @defgroup announcement Announcement
* Implements announcements that can be presented to website visitors.
*/
/**
* @file classes/announcement/Announcement.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Announcement
*
* @ingroup announcement
*
* @see DAO
*
* @brief Basic class describing a announcement.
*/
namespace PKP\announcement;
use APP\core\Application;
use APP\facades\Repo;
use APP\file\PublicFileManager;
use PKP\db\DAORegistry;
class Announcement extends \PKP\core\DataObject
{
//
// Get/set methods
//
/**
* Get assoc ID for this announcement.
*
* @return int
*/
public function getAssocId()
{
return $this->getData('assocId');
}
/**
* Set assoc ID for this announcement.
*
* @param int $assocId
*/
public function setAssocId($assocId)
{
$this->setData('assocId', $assocId);
}
/**
* Get assoc type for this announcement.
*
* @return int
*/
public function getAssocType()
{
return $this->getData('assocType');
}
/**
* Set assoc type for this announcement.
*
* @param int $assocType
*/
public function setAssocType($assocType)
{
$this->setData('assocType', $assocType);
}
/**
* Get the announcement type of the announcement.
*
* @return int
*/
public function getTypeId()
{
return $this->getData('typeId');
}
/**
* Set the announcement type of the announcement.
*
* @param int $typeId
*/
public function setTypeId($typeId)
{
$this->setData('typeId', $typeId);
}
/**
* Get the announcement type name of the announcement.
*
* @return string|null
*/
public function getAnnouncementTypeName()
{
$announcementTypeDao = DAORegistry::getDAO('AnnouncementTypeDAO'); /** @var AnnouncementTypeDAO $announcementTypeDao */
$announcementType = $announcementTypeDao->getById($this->getData('typeId'));
return $announcementType ? $announcementType->getLocalizedTypeName() : null;
}
/**
* Get localized announcement title
*
* @return string
*/
public function getLocalizedTitle()
{
return $this->getLocalizedData('title');
}
/**
* Get full localized announcement title including type name
*
* @return string
*/
public function getLocalizedTitleFull()
{
$typeName = $this->getAnnouncementTypeName();
if (!empty($typeName)) {
return $typeName . ': ' . $this->getLocalizedTitle();
} else {
return $this->getLocalizedTitle();
}
}
/**
* Get announcement title.
*
* @param string $locale
*
* @return string
*/
public function getTitle($locale)
{
return $this->getData('title', $locale);
}
/**
* Set announcement title.
*
* @param string $title
* @param string $locale
*/
public function setTitle($title, $locale)
{
$this->setData('title', $title, $locale);
}
/**
* Get localized short description
*
* @return string
*/
public function getLocalizedDescriptionShort()
{
return $this->getLocalizedData('descriptionShort');
}
/**
* Get announcement brief description.
*
* @param string $locale
*
* @return string
*/
public function getDescriptionShort($locale)
{
return $this->getData('descriptionShort', $locale);
}
/**
* Set announcement brief description.
*
* @param string $descriptionShort
* @param string $locale
*/
public function setDescriptionShort($descriptionShort, $locale)
{
$this->setData('descriptionShort', $descriptionShort, $locale);
}
/**
* Get localized full description
*
* @return string
*/
public function getLocalizedDescription()
{
return $this->getLocalizedData('description');
}
/**
* Get announcement description.
*
* @param string $locale
*
* @return string
*/
public function getDescription($locale)
{
return $this->getData('description', $locale);
}
/**
* Set announcement description.
*
* @param string $description
* @param string $locale
*/
public function setDescription($description, $locale)
{
$this->setData('description', $description, $locale);
}
/**
* Get announcement expiration date.
*
* @return string (YYYY-MM-DD)
*/
public function getDateExpire()
{
return $this->getData('dateExpire');
}
/**
* Set announcement expiration date.
*
* @param string $dateExpire (YYYY-MM-DD)
*/
public function setDateExpire($dateExpire)
{
$this->setData('dateExpire', $dateExpire);
}
/**
* Get announcement posted date.
*
* @return string (YYYY-MM-DD)
*/
public function getDatePosted()
{
return date('Y-m-d', strtotime($this->getData('datePosted')));
}
/**
* Get announcement posted datetime.
*
* @return string (YYYY-MM-DD HH:MM:SS)
*/
public function getDatetimePosted()
{
return $this->getData('datePosted');
}
/**
* Set announcement posted date.
*
* @param string $datePosted (YYYY-MM-DD)
*/
public function setDatePosted($datePosted)
{
$this->setData('datePosted', $datePosted);
}
/**
* Set announcement posted datetime.
*
* @param string $datetimePosted (YYYY-MM-DD HH:MM:SS)
*/
public function setDatetimePosted($datetimePosted)
{
$this->setData('datePosted', $datetimePosted);
}
/**
* Get the featured image data
*/
public function getImage(): ?array
{
return $this->getData('image');
}
/**
* Set the featured image data
*/
public function setImage(array $image): void
{
$this->setData('image', $image);
}
/**
* Get the full URL to the image
*
* @param bool $withTimestamp Pass true to include a query argument with a timestamp
* of the date the image was uploaded in order to workaround cache bugs in browsers
*/
public function getImageUrl(bool $withTimestamp = true): string
{
$image = $this->getImage();
if (!$image) {
return '';
}
$filename = $image['uploadName'];
if ($withTimestamp) {
$filename .= '?'. strtotime($image['dateUploaded']);
}
$publicFileManager = new PublicFileManager();
return join('/', [
Application::get()->getRequest()->getBaseUrl(),
$this->getAssocId()
? $publicFileManager->getContextFilesPath((int) $this->getAssocId())
: $publicFileManager->getSiteFilesPath(),
Repo::announcement()->getImageSubdirectory(),
$filename
]);
}
/**
* Get the alt text for the image
*/
public function getImageAltText(): string
{
$image = $this->getImage();
if (!$image || !$image['altText']) {
return '';
}
return $image['altText'];
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\announcement\Announcement', '\Announcement');
}
@@ -0,0 +1,82 @@
<?php
/**
* @file classes/announcement/AnnouncementType.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AnnouncementType
*
* @ingroup announcement
*
* @see AnnouncementTypeDAO, AnnouncementTypeForm
*
* @brief Basic class describing an announcement type.
*/
namespace PKP\announcement;
class AnnouncementType extends \PKP\core\DataObject
{
//
// Get/set methods
//
/**
* Get context ID for this announcement.
*
* @return int
*/
public function getContextId()
{
return $this->getData('contextId');
}
/**
* Set context ID for this announcement.
*
* @param int $contextId
*/
public function setContextId($contextId)
{
$this->setData('contextId', $contextId);
}
/**
* Get the type of the announcement type.
*
* @return string
*/
public function getLocalizedTypeName()
{
return $this->getLocalizedData('name');
}
/**
* Get the type of the announcement type.
*
* @param string $locale
*
* @return string
*/
public function getName($locale)
{
return $this->getData('name', $locale);
}
/**
* Set the type of the announcement type.
*
* @param string $name
* @param string $locale
*/
public function setName($name, $locale)
{
$this->setData('name', $name, $locale);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\announcement\AnnouncementType', '\AnnouncementType');
}
@@ -0,0 +1,210 @@
<?php
/**
* @file classes/announcement/AnnouncementTypeDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class AnnouncementTypeDAO
*
* @ingroup announcement
*
* @see AnnouncementType
*
* @brief Operations for retrieving and modifying AnnouncementType objects.
*/
namespace PKP\announcement;
use APP\facades\Repo;
class AnnouncementTypeDAO extends \PKP\db\DAO
{
/**
* Generate a new data object.
*
* @return AnnouncementType
*/
public function newDataObject()
{
return new AnnouncementType();
}
/**
* Retrieve an announcement type by announcement type ID.
*
* @param ?int $typeId Announcement type ID
* @param ?int $contextId Optional context ID
*
* @return AnnouncementType
*/
public function getById($typeId, $contextId = null)
{
$params = [(int) $typeId];
if ($contextId !== null) {
$params[] = (int) $contextId;
}
$result = $this->retrieve(
'SELECT * FROM announcement_types WHERE type_id = ?' .
($contextId !== null ? ' AND context_id = ?' : ''),
$params
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Get the locale field names.
*
* @return array
*/
public function getLocaleFieldNames()
{
return ['name'];
}
/**
* Internal function to return an AnnouncementType object from a row.
*
* @param array $row
*
* @return AnnouncementType
*/
public function _fromRow($row)
{
$announcementType = $this->newDataObject();
$announcementType->setId($row['type_id']);
$announcementType->setData('contextId', $row['context_id']);
$this->getDataObjectSettings('announcement_type_settings', 'type_id', $row['type_id'], $announcementType);
return $announcementType;
}
/**
* Update the localized settings for this object
*
* @param AnnouncementType $announcementType
*/
public function updateLocaleFields($announcementType)
{
$this->updateDataObjectSettings(
'announcement_type_settings',
$announcementType,
['type_id' => (int) $announcementType->getId()]
);
}
/**
* Insert a new AnnouncementType.
*
* @param AnnouncementType $announcementType
*
* @return int
*/
public function insertObject($announcementType)
{
$this->update(
sprintf('INSERT INTO announcement_types
(context_id)
VALUES
(?)'),
[
$announcementType->getContextId()
? (int) $announcementType->getContextId()
: null
]
);
$announcementType->setId($this->getInsertId());
$this->updateLocaleFields($announcementType);
return $announcementType->getId();
}
/**
* Update an existing announcement type.
*
* @param AnnouncementType $announcementType
*
* @return bool
*/
public function updateObject($announcementType)
{
$returner = $this->update(
'UPDATE announcement_types
SET context_id = ?
WHERE type_id = ?',
[
$announcementType->getContextId(),
(int) $announcementType->getId()
]
);
$this->updateLocaleFields($announcementType);
return $returner;
}
/**
* Delete an announcement type. Note that all announcements with this type are also
* deleted.
*
* @param AnnouncementType $announcementType
*/
public function deleteObject($announcementType)
{
return $this->deleteById($announcementType->getId());
}
/**
* Delete an announcement type by announcement type ID. Note that all announcements with
* this type ID are also deleted.
*
* @param int $typeId
*/
public function deleteById($typeId)
{
$this->update('DELETE FROM announcement_type_settings WHERE type_id = ?', [(int) $typeId]);
$this->update('DELETE FROM announcement_types WHERE type_id = ?', [(int) $typeId]);
$collector = Repo::announcement()->getCollector()->filterByTypeIds([(int) $typeId]);
Repo::announcement()->deleteMany($collector);
}
/**
* Delete announcement types by context ID.
*
* @param int $contextId
*/
public function deleteByContextId($contextId)
{
foreach ($this->getByContextId($contextId) as $type) {
$this->deleteObject($type);
}
}
/**
* Retrieve an array of announcement types matching a particular context ID.
*
* @return \Generator<int,AnnouncementType> Matching AnnouncementTypes
*/
public function getByContextId(?int $contextId)
{
if ($contextId) {
$result = $this->retrieve(
'SELECT * FROM announcement_types WHERE context_id = ? ORDER BY type_id',
[$contextId]
);
} else {
$result = $this->retrieve(
'SELECT * FROM announcement_types WHERE context_id IS NULL ORDER BY type_id'
);
}
foreach ($result as $row) {
yield $row->type_id => $this->_fromRow((array) $row);
}
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\announcement\AnnouncementTypeDAO', '\AnnouncementTypeDAO');
}
+240
View File
@@ -0,0 +1,240 @@
<?php
/**
* @file classes/announcement/Collector.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Collector
*
* @brief A helper class to configure a Query Builder to get a collection of announcements
*/
namespace PKP\announcement;
use APP\core\Application;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\Core;
use PKP\core\interfaces\CollectorInterface;
use PKP\plugins\Hook;
/**
* @template T of Announcement
*/
class Collector implements CollectorInterface
{
public const ORDERBY_DATE_POSTED = 'date_posted';
public const ORDERBY_DATE_EXPIRE = 'date_expire';
public const ORDER_DIR_ASC = 'ASC';
public const ORDER_DIR_DESC = 'DESC';
public const SITE_ONLY = 'site';
public const SITE_AND_CONTEXTS = 'all';
public DAO $dao;
public ?array $contextIds = null;
public ?string $isActive = null;
public ?string $searchPhrase = null;
public ?array $typeIds = null;
public ?string $includeSite = null;
public ?int $count = null;
public ?int $offset = null;
public string $orderBy = self::ORDERBY_DATE_POSTED;
public string $orderDirection = self::ORDER_DIR_DESC;
public function __construct(DAO $dao)
{
$this->dao = $dao;
}
/** @copydoc DAO::getCount() */
public function getCount(): int
{
return $this->dao->getCount($this);
}
/**
* @copydoc DAO::getIds()
*
* @return Collection<int,int>
*/
public function getIds(): Collection
{
return $this->dao->getIds($this);
}
/**
* @copydoc DAO::getMany()
*
* @return LazyCollection<int,T>
*/
public function getMany(): LazyCollection
{
return $this->dao->getMany($this);
}
/**
* Filter announcements by one or more contexts
*/
public function filterByContextIds(?array $contextIds): self
{
$this->contextIds = $contextIds;
return $this;
}
/**
* Filter announcements by those that have not expired
*
* @param string $date Optionally filter announcements by those
* not expired until $date (YYYY-MM-DD).
*/
public function filterByActive(string $date = ''): self
{
$this->isActive = empty($date)
? Core::getCurrentDate()
: $date;
return $this;
}
/**
* Filter announcements by one or more announcement types
*/
public function filterByTypeIds(array $typeIds): self
{
$this->typeIds = $typeIds;
return $this;
}
/**
* Include site-level announcements in the results
*/
public function withSiteAnnouncements(?string $includeMethod = self::SITE_AND_CONTEXTS): self
{
$this->includeSite = $includeMethod;
return $this;
}
/**
* Filter announcements by those matching a search query
*/
public function searchPhrase(?string $phrase): self
{
$this->searchPhrase = $phrase;
return $this;
}
/**
* Limit the number of objects retrieved
*/
public function limit(?int $count): self
{
$this->count = $count;
return $this;
}
/**
* Offset the number of objects retrieved, for example to
* retrieve the second page of contents
*/
public function offset(?int $offset): self
{
$this->offset = $offset;
return $this;
}
/**
* Order the results
*
* Results are ordered by the date posted by default.
*
* @param string $sorter One of the self::ORDERBY_ constants
* @param string $direction One of the self::ORDER_DIR_ constants
*/
public function orderBy(?string $sorter, string $direction = self::ORDER_DIR_DESC): self
{
$this->orderBy = $sorter;
$this->orderDirection = $direction;
return $this;
}
/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
public function getQueryBuilder(): Builder
{
$qb = DB::table($this->dao->table . ' as a')
->select(['a.*']);
if (isset($this->contextIds) && $this->includeSite !== self::SITE_ONLY) {
$qb->where('a.assoc_type', Application::get()->getContextAssocType());
$qb->whereIn('a.assoc_id', $this->contextIds);
if ($this->includeSite === self::SITE_AND_CONTEXTS) {
$qb->orWhereNull('a.assoc_id');
}
} elseif ($this->includeSite === self::SITE_ONLY) {
$qb->where('a.assoc_type', Application::get()->getContextAssocType());
$qb->whereNull('a.assoc_id');
}
if (isset($this->typeIds)) {
$qb->whereIn('a.type_id', $this->typeIds);
}
$qb->when($this->isActive, fn ($qb) => $qb->where(function ($qb) {
$qb->where('a.date_expire', '>', $this->isActive)
->orWhereNull('a.date_expire');
}));
if ($this->searchPhrase !== null) {
$words = explode(' ', $this->searchPhrase);
if (count($words)) {
$qb->whereIn('a.announcement_id', function ($query) use ($words) {
$query->select('announcement_id')->from($this->dao->settingsTable);
foreach ($words as $word) {
$word = strtolower(addcslashes($word, '%_'));
$query->where(function ($query) use ($word) {
$query->where(function ($query) use ($word) {
$query->where('setting_name', 'title');
$query->where(DB::raw('lower(setting_value)'), 'LIKE', "%{$word}%");
})
->orWhere(function ($query) use ($word) {
$query->where('setting_name', 'descriptionShort');
$query->where(DB::raw('lower(setting_value)'), 'LIKE', "%{$word}%");
})
->orWhere(function ($query) use ($word) {
$query->where('setting_name', 'description');
$query->where(DB::raw('lower(setting_value)'), 'LIKE', "%{$word}%");
});
});
}
});
}
}
$qb->orderByDesc('a.date_posted');
if (isset($this->count)) {
$qb->limit($this->count);
}
if (isset($this->offset)) {
$qb->offset($this->offset);
}
if (isset($this->orderBy)) {
$qb->orderBy('a.' . $this->orderBy, $this->orderDirection);
// Add a secondary sort by id to catch cases where two
// announcements share the same date
if (in_array($this->orderBy, [SELF::ORDERBY_DATE_EXPIRE, SELF::ORDERBY_DATE_POSTED])) {
$qb->orderBy('a.announcement_id', $this->orderDirection);
}
}
Hook::call('Announcement::Collector', [&$qb, $this]);
return $qb;
}
}
+142
View File
@@ -0,0 +1,142 @@
<?php
/**
* @file classes/announcement/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @brief Read and write announcements to the database.
*/
namespace PKP\announcement;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\EntityDAO;
/**
* @template T of Announcement
* @extends EntityDAO<T>
*/
class DAO extends EntityDAO
{
/** @copydoc EntityDAO::$schema */
public $schema = \PKP\services\PKPSchemaService::SCHEMA_ANNOUNCEMENT;
/** @copydoc EntityDAO::$table */
public $table = 'announcements';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = 'announcement_settings';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'announcement_id';
/** @copydoc EntityDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'announcement_id',
'assocId' => 'assoc_id',
'assocType' => 'assoc_type',
'typeId' => 'type_id',
'dateExpire' => 'date_expire',
'datePosted' => 'date_posted',
];
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Announcement
{
return app(Announcement::class);
}
/**
* Check if an announcement exists
*/
public function exists(int $id): bool
{
return DB::table($this->table)
->where($this->primaryKeyColumn, '=', $id)
->exists();
}
/**
* Get an announcement
*/
public function get(int $id): ?Announcement
{
$row = DB::table($this->table)
->where($this->primaryKeyColumn, $id)
->first();
return $row ? $this->fromRow($row) : null;
}
/**
* Get the number of announcements matching the configured query
*/
public function getCount(Collector $query): int
{
return $query
->getQueryBuilder()
->get('a.' . $this->primaryKeyColumn)
->count();
}
/**
* Get a list of ids matching the configured query
*
* @return Collection<int,int>
*/
public function getIds(Collector $query): Collection
{
return $query
->getQueryBuilder()
->pluck('a.' . $this->primaryKeyColumn);
}
/**
* Get a collection of announcements matching the configured query
*
* @return LazyCollection<int,T>
*/
public function getMany(Collector $query): LazyCollection
{
$rows = $query
->getQueryBuilder()
->get();
return LazyCollection::make(function () use ($rows) {
foreach ($rows as $row) {
yield $row->announcement_id => $this->fromRow($row);
}
});
}
/**
* @copydoc EntityDAO::insert()
*/
public function insert(Announcement $announcement): int
{
return parent::_insert($announcement);
}
/**
* @copydoc EntityDAO::update()
*/
public function update(Announcement $announcement)
{
parent::_update($announcement);
}
/**
* @copydoc EntityDAO::delete()
*/
public function delete(Announcement $announcement)
{
parent::_delete($announcement);
}
}
+352
View File
@@ -0,0 +1,352 @@
<?php
/**
* @file classes/announcement/Repository.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage announcements.
*/
namespace PKP\announcement;
use APP\core\Application;
use APP\core\Request;
use APP\file\PublicFileManager;
use PKP\context\Context;
use PKP\core\Core;
use PKP\core\exceptions\StoreTemporaryFileException;
use PKP\core\PKPString;
use PKP\file\FileManager;
use PKP\file\TemporaryFile;
use PKP\file\TemporaryFileManager;
use PKP\plugins\Hook;
use PKP\services\PKPSchemaService;
use PKP\user\User;
use PKP\validation\ValidatorFactory;
class Repository
{
/** @var DAO $dao */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schema */
public $schemaMap = maps\Schema::class;
/** @var Request $request */
protected $request;
/** @var PKPSchemaService<Announcement> $schemaService */
protected $schemaService;
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->dao = $dao;
$this->request = $request;
$this->schemaService = $schemaService;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): Announcement
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::get() */
public function get(int $id): ?Announcement
{
return $this->dao->get($id);
}
/** @copydoc DAO::exists() */
public function exists(int $id): bool
{
return $this->dao->exists($id);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* announcements to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for an announcement
*
* Perform validation checks on data used to add or edit an announcement.
*
* @param array $props A key/value array with the new data to validate
* @param array $allowedLocales The context's supported locales
* @param string $primaryLocale The context's primary locale
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(?Announcement $object, array $props, array $allowedLocales, string $primaryLocale): array
{
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules($this->dao->schema, $allowedLocales),
[
'dateExpire.date_format' => __('stats.dateRange.invalidDate'),
]
);
// Check required fields
ValidatorFactory::required(
$validator,
$object,
$this->schemaService->getRequiredProps($this->dao->schema),
$this->schemaService->getMultilingualProps($this->dao->schema),
$allowedLocales,
$primaryLocale
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);
$errors = [];
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Announcement::validate', [&$errors, $object, $props, $allowedLocales, $primaryLocale]);
return $errors;
}
/** @copydoc DAO::insert() */
public function add(Announcement $announcement): int
{
$announcement->setData('datePosted', Core::getCurrentDate());
$id = $this->dao->insert($announcement);
$announcement = $this->get($id);
if ($announcement->getImage()) {
$this->handleImageUpload($announcement);
}
Hook::call('Announcement::add', [$announcement]);
return $id;
}
/**
* Update an object in the database
*
* Deletes the old image if it has been removed, or a new image has
* been uploaded.
*/
public function edit(Announcement $announcement, array $params)
{
$newAnnouncement = clone $announcement;
$newAnnouncement->setAllData(array_merge($newAnnouncement->_data, $params));
Hook::call('Announcement::edit', [$newAnnouncement, $announcement, $params]);
$this->dao->update($newAnnouncement);
$image = $newAnnouncement->getImage();
$hasNewImage = $image && $image['temporaryFileId'];
if ((!$image || $hasNewImage) && $announcement->getImage()) {
$this->deleteImage($announcement);
}
if ($hasNewImage) {
$this->handleImageUpload($newAnnouncement);
}
}
/** @copydoc DAO::delete() */
public function delete(Announcement $announcement)
{
Hook::call('Announcement::delete::before', [$announcement]);
if ($announcement->getImage()) {
$this->deleteImage($announcement);
}
$this->dao->delete($announcement);
Hook::call('Announcement::delete', [$announcement]);
}
/**
* Delete a collection of announcements
*/
public function deleteMany(Collector $collector)
{
foreach ($collector->getMany() as $announcement) {
$this->delete($announcement);
}
}
/**
* The subdirectory where announcement images are stored
*/
public function getImageSubdirectory(): string
{
return 'announcements';
}
/**
* Get the base URL for announcement file uploads
*/
public function getFileUploadBaseUrl(?Context $context = null): string
{
return join('/', [
Application::get()->getRequest()->getPublicFilesUrl($context),
$this->getImageSubdirectory(),
]);
}
/**
* Handle image uploads
*
* @throws StoreTemporaryFileException Unable to store temporary file upload
*/
protected function handleImageUpload(Announcement $announcement): void
{
$image = $announcement->getImage();
if ($image && $image['temporaryFileId']) {
$user = Application::get()->getRequest()->getUser();
$image = $announcement->getImage();
$temporaryFileManager = new TemporaryFileManager();
$temporaryFile = $temporaryFileManager->getFile((int) $image['temporaryFileId'], $user?->getId());
$filePath = $this->getImageSubdirectory() . '/' . $this->getImageFilename($announcement, $temporaryFile);
if (!$this->isValidImage($temporaryFile, $filePath, $user, $announcement)) {
throw new StoreTemporaryFileException($temporaryFile, $filePath, $user, $announcement);
}
if ($this->storeTemporaryFile($temporaryFile, $filePath, $user->getId(), $announcement)) {
$announcement->setImage(
$this->getImageData($announcement, $temporaryFile)
);
$this->dao->update($announcement);
} else {
$this->delete($announcement);
throw new StoreTemporaryFileException($temporaryFile, $filePath, $user, $announcement);
}
}
}
/**
* Store a temporary file upload in the public files directory
*
* @param string $newPath The new filename with the path relative to the public files directoruy
* @return bool Whether or not the operation was successful
*/
protected function storeTemporaryFile(TemporaryFile $temporaryFile, string $newPath, int $userId, Announcement $announcement): bool
{
$publicFileManager = new PublicFileManager();
$temporaryFileManager = new TemporaryFileManager();
if ($announcement->getAssocId()) {
$result = $publicFileManager->copyContextFile(
$announcement->getAssocId(),
$temporaryFile->getFilePath(),
$newPath
);
} else {
$result = $publicFileManager->copySiteFile(
$temporaryFile->getFilePath(),
$newPath
);
}
if (!$result) {
return false;
}
$temporaryFileManager->deleteById($temporaryFile->getId(), $userId);
return $result;
}
/**
* Get the data array for a temporary file that has just been stored
*
* @return array Data about the image, like the upload name, alt text, and date uploaded
*/
protected function getImageData(Announcement $announcement, TemporaryFile $temporaryFile): array
{
$image = $announcement->getImage();
return [
'name' => $temporaryFile->getOriginalFileName(),
'uploadName' => $this->getImageFilename($announcement, $temporaryFile),
'dateUploaded' => Core::getCurrentDate(),
'altText' => !empty($image['altText']) ? $image['altText'] : '',
];
}
/**
* Get the filename of the image upload
*/
protected function getImageFilename(Announcement $announcement, TemporaryFile $temporaryFile): string
{
$fileManager = new FileManager();
return $announcement->getId()
. $fileManager->getImageExtension($temporaryFile->getFileType());
}
/**
* Delete the image related to announcement
*/
protected function deleteImage(Announcement $announcement): void
{
$image = $announcement->getImage();
if ($image && $image['uploadName']) {
$publicFileManager = new PublicFileManager();
$filesPath = $announcement->getAssocId()
? $publicFileManager->getContextFilesPath($announcement->getAssocId())
: $publicFileManager->getSiteFilesPath();
$publicFileManager->deleteByPath(
join('/', [
$filesPath,
$this->getImageSubdirectory(),
$image['uploadName'],
])
);
}
}
/**
* Check that temporary file is an image
*/
protected function isValidImage(TemporaryFile $temporaryFile): bool
{
if (getimagesize($temporaryFile->getFilePath()) === false) {
return false;
}
$extension = pathinfo($temporaryFile->getOriginalFileName(), PATHINFO_EXTENSION);
$fileManager = new FileManager();
$extensionFromMimeType = $fileManager->getImageExtension(
PKPString::mime_content_type($temporaryFile->getFilePath())
);
if ($extensionFromMimeType !== '.' . $extension) {
return false;
}
return true;
}
}
@@ -0,0 +1,124 @@
<?php
/**
* @file classes/announcement/maps/Schema.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map announcements to the properties defined in the announcement schema
*/
namespace PKP\announcement\maps;
use APP\core\Application;
use APP\core\Request;
use Illuminate\Support\Enumerable;
use PKP\announcement\Announcement;
use PKP\core\PKPApplication;
use PKP\services\PKPSchemaService;
class Schema extends \PKP\core\maps\Schema
{
public Enumerable $collection;
public string $schema = PKPSchemaService::SCHEMA_ANNOUNCEMENT;
/**
* Map an announcement
*
* Includes all properties in the announcement schema.
*/
public function map(Announcement $item): array
{
return $this->mapByProperties($this->getProps(), $item);
}
/**
* Summarize an announcement
*
* Includes properties with the apiSummary flag in the announcement schema.
*/
public function summarize(Announcement $item): array
{
return $this->mapByProperties($this->getSummaryProps(), $item);
}
/**
* Map a collection of Announcements
*
* @see self::map
*/
public function mapMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->map($item);
});
}
/**
* Summarize a collection of Announcements
*
* @see self::summarize
*/
public function summarizeMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->summarize($item);
});
}
/**
* Map schema properties of an Announcement to an assoc array
*/
protected function mapByProperties(array $props, Announcement $item): array
{
$output = [];
foreach ($props as $prop) {
switch ($prop) {
case '_href':
$output[$prop] = $this->getApiUrl('announcements/' . $item->getId());
break;
case 'url':
$output[$prop] = $this->request->getDispatcher()->url(
$this->request,
PKPApplication::ROUTE_PAGE,
$this->getUrlPath(),
'announcement',
'view',
$item->getId()
);
break;
default:
$output[$prop] = $item->getData($prop);
break;
}
}
$output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->getSupportedLocales());
ksort($output);
return $this->withExtensions($output, $item);
}
protected function getUrlPath(): string
{
if (isset($this->context)) {
return $this->context->getData('urlPath');
}
return 'index';
}
protected function getSupportedLocales(): array
{
if (isset($this->context)) {
return $this->context->getSupportedFormLocales();
}
return Application::get()->getRequest()->getSite()->getSupportedLocales();
}
}
+257
View File
@@ -0,0 +1,257 @@
<?php
/**
* @file classes/author/Author.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class \PKP\author\Author
*
* @ingroup author
*
* @see DAO
*
* @brief Author metadata class.
*/
namespace PKP\author;
use APP\facades\Repo;
use PKP\facades\Locale;
use PKP\identity\Identity;
class Author extends Identity
{
/**
* Get the default/fall back locale the values should exist for
*/
public function getDefaultLocale(): ?string
{
return $this->getSubmissionLocale();
}
/**
* @copydoc Identity::getLocalizedGivenName()
*/
public function getLocalizedGivenName()
{
return $this->getLocalizedData(self::IDENTITY_SETTING_GIVENNAME);
}
/**
* @copydoc Identity::getLocalizedFamilyName()
*/
public function getLocalizedFamilyName()
{
// Prioritize the current locale, then the default locale.
$locale = Locale::getLocale();
$givenName = $this->getGivenName($locale);
// Only use the family name if a given name exists (to avoid mixing locale data)
if (!empty($givenName)) {
return $this->getFamilyName($locale);
}
// Fall back on the submission locale.
return $this->getFamilyName($this->getSubmissionLocale());
}
//
// Get/set methods
//
/**
* Get ID of submission.
*
* @return int
*/
public function getSubmissionId()
{
return $this->getData('submissionId');
}
/**
* Set ID of submission.
*
* @param int $submissionId
*/
public function setSubmissionId($submissionId)
{
$this->setData('submissionId', $submissionId);
}
/**
* Get submission locale.
*
* @return string
*/
public function getSubmissionLocale()
{
return $this->getData('locale');
}
/**
* Set submission locale.
*
* @param string $locale
*/
public function setSubmissionLocale($locale)
{
return $this->setData('locale', $locale);
}
/**
* Set the user group id
*
* @param int $userGroupId
*/
public function setUserGroupId($userGroupId)
{
$this->setData('userGroupId', $userGroupId);
}
/**
* Get the user group id
*
* @return int
*/
public function getUserGroupId()
{
return $this->getData('userGroupId');
}
/**
* Set whether or not to include in browse lists.
*
* @param bool $include
*/
public function setIncludeInBrowse($include)
{
$this->setData('includeInBrowse', $include);
}
/**
* Get whether or not to include in browse lists.
*
* @return bool
*/
public function getIncludeInBrowse()
{
return $this->getData('includeInBrowse');
}
/**
* Get the "show title" flag (whether or not the title of the role
* should be included in the list of submission contributor names).
* This is fetched from the user group for performance reasons.
*
* @return bool
*/
public function getShowTitle()
{
return $this->getData('showTitle');
}
/**
* Set the "show title" flag. This attribute belongs to the user group,
* NOT the author; fetched for performance reasons only.
*
* @param bool $showTitle
*/
public function _setShowTitle($showTitle)
{
$this->setData('showTitle', $showTitle);
}
/**
* Get primary contact.
*
* @return bool
*/
public function getPrimaryContact()
{
return $this->getData('primaryContact');
}
/**
* Set primary contact.
*
* @param bool $primaryContact
*/
public function setPrimaryContact($primaryContact)
{
$this->setData('primaryContact', $primaryContact);
}
/**
* Get sequence of author in submissions' author list.
*
* @return float
*/
public function getSequence()
{
return $this->getData('seq');
}
/**
* Set sequence of author in submissions' author list.
*
* @param float $sequence
*/
public function setSequence($sequence)
{
$this->setData('seq', $sequence);
}
/**
* Get the user group for this contributor.
*
* @return \PKP\userGroup\UserGroup
*/
public function getUserGroup()
{
//FIXME: should this be queried when fetching Author from DB? - see #5231.
static $userGroup; // Frequently we'll fetch the same one repeatedly
if (!$userGroup || $this->getUserGroupId() != $userGroup->getId()) {
$userGroup = Repo::userGroup()->get($this->getUserGroupId());
}
return $userGroup;
}
/**
* Get a localized version of the User Group
*
* @return string
*/
public function getLocalizedUserGroupName()
{
$userGroup = $this->getUserGroup();
return $userGroup->getLocalizedName();
}
/**
* Get competing interests.
* @return string|array|null
*/
function getCompetingInterests(?string $locale)
{
return $this->getData('competingInterests', $locale);
}
/**
* Set competing interests.
* @param $competingInterests string|array|null
*/
function setCompetingInterests($competingInterests, ?string $locale)
{
$this->setData('competingInterests', $competingInterests, $locale);
}
/**
* Get a localized version competing interest statement
*/
function getLocalizedCompetingInterests(): ?string
{
return $this->getLocalizedData('competingInterests');
}
}
+258
View File
@@ -0,0 +1,258 @@
<?php
/**
* @file classes/author/Collector.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Collector
*
* @brief A helper class to configure a Query Builder to get a collection of announcements
*/
namespace PKP\author;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\interfaces\CollectorInterface;
use PKP\plugins\Hook;
/**
* @template T of Author
*/
class Collector implements CollectorInterface
{
public const ORDERBY_SEQUENCE = 'sequence';
public const ORDERBY_ID = 'id';
/** @var string The default orderBy value for authors collector */
public $orderBy = self::ORDERBY_SEQUENCE;
/** @var DAO */
public $dao;
/** @var int[]|null */
public $contextIds = null;
/** @var int[]|null */
public $publicationIds = null;
/** Get authors with a family name */
protected ?string $familyName = null;
/** Get authors with a given name */
protected ?string $givenName = null;
/** Get authors with a specified country code */
protected ?string $country = null;
/** Get authors with a specified affiliation */
protected ?string $affiliation = null;
public ?int $count = null;
public ?int $offset = null;
public ?bool $includeInBrowse = null;
public function __construct(DAO $dao)
{
$this->dao = $dao;
}
public function getCount(): int
{
return $this->dao->getCount($this);
}
/**
* @return Collection<int,int>
*/
public function getIds(): Collection
{
return $this->dao->getIds($this);
}
/**
* @copydoc DAO::getMany()
* @return LazyCollection<int,T>
*/
public function getMany(): LazyCollection
{
return $this->dao->getMany($this);
}
/**
* Filter by contexts
*/
public function filterByContextIds(?array $contextIds): self
{
$this->contextIds = $contextIds;
return $this;
}
/**
* Filter by publications
*/
public function filterByPublicationIds(?array $publicationIds): self
{
$this->publicationIds = $publicationIds;
return $this;
}
/**
* Filter by include in browse
*/
public function filterByIncludeInBrowse(?bool $includeInBrowse): self
{
$this->includeInBrowse = $includeInBrowse;
return $this;
}
/**
* Include orderBy columns to the collector query
*/
public function orderBy(?string $orderBy): self
{
$this->orderBy = $orderBy;
return $this;
}
/**
* Filter by the given and family name
*
*
*/
public function filterByName(?string $givenName, ?string $familyName): self
{
$this->givenName = $givenName;
$this->familyName = $familyName;
return $this;
}
/**
* Filter by the specified country code
*
* @param string $country Country code (2-letter)
*
* */
public function filterByCountry(?string $country): self
{
$this->country = $country;
return $this;
}
/**
* Filter by the specified affiliation code
*
* */
public function filterByAffiliation(?string $affiliation): self
{
$this->affiliation = $affiliation;
return $this;
}
/**
* Limit the number of objects retrieved
*/
public function limit(?int $count): self
{
$this->count = $count;
return $this;
}
/**
* Offset the number of objects retrieved, for example to
* retrieve the second page of contents
*/
public function offset(?int $offset): self
{
$this->offset = $offset;
return $this;
}
/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
public function getQueryBuilder(): Builder
{
$q = DB::table('authors as a')
->select(['a.*', 's.locale AS submission_locale'])
->join('publications as p', 'a.publication_id', '=', 'p.publication_id')
->join('submissions as s', 'p.submission_id', '=', 's.submission_id');
if (isset($this->contextIds)) {
$q->whereIn('s.context_id', $this->contextIds);
}
$q->when($this->familyName !== null, function (Builder $q) {
$q->whereIn('a.author_id', function (Builder $q) {
$q->select('author_id')
->from($this->dao->settingsTable)
->where('setting_name', '=', 'familyName')
->where('setting_value', $this->familyName);
});
});
$q->when($this->givenName !== null, function (Builder $q) {
$q->whereIn('a.author_id', function (Builder $q) {
$q->select('author_id')
->from($this->dao->settingsTable)
->where('setting_name', '=', 'givenName')
->where('setting_value', $this->givenName);
});
});
if (isset($this->publicationIds)) {
$q->whereIn('a.publication_id', $this->publicationIds);
}
$q->when($this->country !== null, function (Builder $q) {
$q->whereIn('a.author_id', function (Builder $q) {
$q->select('author_id')
->from($this->dao->settingsTable)
->where('setting_name', '=', 'country')
->where('setting_value', $this->country);
});
});
$q->when($this->affiliation !== null, function (Builder $q) {
$q->whereIn('a.author_id', function (Builder $q) {
$q->select('author_id')
->from($this->dao->settingsTable)
->where('setting_name', '=', 'affiliation')
->where('setting_value', $this->affiliation);
});
});
if ($this->includeInBrowse) {
$q->where('a.include_in_browse', $this->includeInBrowse);
}
if (isset($this->count)) {
$q->limit($this->count);
}
if (isset($this->offset)) {
$q->offset($this->offset);
}
switch ($this->orderBy) {
case self::ORDERBY_SEQUENCE:
$q->orderBy('a.seq', 'asc');
break;
case self::ORDERBY_ID:
default:
$q->orderBy('a.author_id', 'asc');
break;
}
// Add app-specific query statements
Hook::call('Author::Collector', [&$q, $this]);
return $q;
}
}
+228
View File
@@ -0,0 +1,228 @@
<?php
/**
* @file classes/author/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @ingroup author
*
* @see \PKP\author\Author
*
* @brief Operations for retrieving and modifying Author objects.
*/
namespace PKP\author;
use APP\author\Author;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\EntityDAO;
use PKP\facades\Repo;
use PKP\services\PKPSchemaService;
/**
* @template T of Author
* @extends EntityDAO<T>
*/
class DAO extends EntityDAO
{
/** @copydoc EntityDAO::$schema */
public $schema = PKPSchemaService::SCHEMA_AUTHOR;
/** @copydoc EntityDAO::$table */
public $table = 'authors';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = 'author_settings';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'author_id';
/** @copydoc EntityDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'author_id',
'email' => 'email',
'includeInBrowse' => 'include_in_browse',
'publicationId' => 'publication_id',
'seq' => 'seq',
'userGroupId' => 'user_group_id',
];
/**
* Get the parent object ID column name
*/
public function getParentColumn(): string
{
return 'publication_id';
}
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Author
{
return app(Author::class);
}
/**
* Get an author.
*
* Optionally, pass the publication ID to only get an author
* if it exists and is assigned to that publication.
*/
public function get(int $id, int $publicationId = null): ?Author
{
// This is overridden due to the need to include submission_locale
// to the fromRow function
$row = DB::table('authors as a')
->join('publications as p', 'a.publication_id', '=', 'p.publication_id')
->join('submissions as s', 'p.submission_id', '=', 's.submission_id')
->where('a.author_id', '=', $id)
->when($publicationId !== null, fn (Builder $query) => $query->where('a.publication_id', '=', $publicationId))
->select(['a.*', 's.locale AS submission_locale'])
->first();
return $row ? $this->fromRow($row) : null;
}
/**
* Check if an author exists.
*
* Optionally, pass the publication ID to check if the author
* exists and is assigned to that publication.
*/
public function exists(int $id, int $publicationId = null): bool
{
return DB::table($this->table)
->where($this->primaryKeyColumn, '=', $id)
->when($publicationId !== null, fn (Builder $query) => $query->where($this->getParentColumn(), $publicationId))
->exists();
}
/**
* Get the total count of rows matching the configured query
*/
public function getCount(Collector $query): int
{
return $query
->getQueryBuilder()
->count();
}
/**
* Get a list of ids matching the configured query
*
* @return Collection<int,int>
*/
public function getIds(Collector $query): Collection
{
return $query
->getQueryBuilder()
->select('a.' . $this->primaryKeyColumn)
->pluck('a.' . $this->primaryKeyColumn);
}
/**
* Get a collection of publications matching the configured query
*
* @return LazyCollection<int,T>
*/
public function getMany(Collector $query): LazyCollection
{
$rows = $query
->getQueryBuilder()
->get();
return LazyCollection::make(function () use ($rows) {
foreach ($rows as $row) {
yield $row->author_id => $this->fromRow($row);
}
});
}
/**
* @copydoc EntityDAO::fromRow()
*/
public function fromRow(object $row): Author
{
$author = parent::fromRow($row);
// Set the primary locale from the submission
$author->setData('locale', $row->submission_locale);
return $author;
}
/**
* @copydoc EntityDAO::insert()
*/
public function insert(Author $author): int
{
return parent::_insert($author);
}
/**
* @copydoc EntityDAO::update()
*/
public function update(Author $author)
{
parent::_update($author);
}
/**
* @copydoc EntityDAO::delete()
*/
public function delete(Author $author)
{
DB::table('publications')
->where('primary_contact_id', $author->getId())
->update(['primary_contact_id' => null]);
parent::_delete($author);
}
/**
* Get the next sequence that should be used when adding a contributor to a publication
*/
public function getNextSeq(int $publicationId): int
{
$nextSeq = 0;
$seq = DB::table('authors as a')
->join('publications as p', 'a.publication_id', '=', 'p.publication_id')
->where('p.publication_id', '=', $publicationId)
->max('a.seq');
if ($seq) {
$nextSeq = $seq + 1;
}
return $nextSeq;
}
/**
* Reset the order of contributors in a publication
*
* This method resets the seq property for each contributor in a publication
* so that they are numbered sequentially without any gaps.
*
* eg - 1, 3, 4, 6 will become 1, 2, 3, 4
*/
public function resetContributorsOrder(int $publicationId)
{
$authorIds = Repo::author()
->getCollector()
->filterByPublicationIds([$publicationId])
->orderBy(Repo::author()->getCollector()::ORDERBY_SEQUENCE)
->getIds();
foreach ($authorIds as $seq => $authorId) {
DB::table('authors')->where('author_id', '=', $authorId)->update(['seq' => $seq]);
}
}
}
+262
View File
@@ -0,0 +1,262 @@
<?php
/**
* @file classes/author/Repository.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage authors.
*/
namespace PKP\author;
use APP\author\Author;
use APP\author\DAO;
use APP\core\Request;
use APP\core\Services;
use APP\facades\Repo;
use APP\submission\Submission;
use PKP\context\Context;
use PKP\plugins\Hook;
use PKP\services\PKPSchemaService;
use PKP\submission\PKPSubmission;
use PKP\user\User;
use PKP\validation\ValidatorFactory;
class Repository
{
/** @var DAO */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schema */
public $schemaMap = maps\Schema::class;
/** @var Request */
protected $request;
/** @var PKPSchemaService<Author> */
protected $schemaService;
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->dao = $dao;
$this->request = $request;
$this->schemaService = $schemaService;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): Author
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::get() */
public function get(int $id, int $publicationId = null): ?Author
{
return $this->dao->get($id, $publicationId);
}
/** @copydoc DAO::exists() */
public function exists(int $id, int $publicationId = null): bool
{
return $this->dao->exists($id, $publicationId);
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* authors to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for an author
*
* Perform validation checks on data used to add or edit an author.
*
* @param Author|null $author The author being edited. Pass `null` if creating a new author
* @param array $props A key/value array with the new data to validate
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate($author, $props, Submission $submission, Context $context)
{
$schemaService = Services::get('schema');
$allowedLocales = $context->getSupportedSubmissionLocales();
$primaryLocale = $submission->getData('locale');
$validator = ValidatorFactory::make(
$props,
$schemaService->getValidationRules(PKPSchemaService::SCHEMA_AUTHOR, $allowedLocales),
[
'country.regex' => __('validator.country.regex'),
]
);
// Check required fields
ValidatorFactory::required(
$validator,
$author,
$schemaService->getRequiredProps(PKPSchemaService::SCHEMA_AUTHOR),
$schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_AUTHOR),
$allowedLocales,
$primaryLocale
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $schemaService->getMultilingualProps(PKPSchemaService::SCHEMA_AUTHOR), $allowedLocales);
// The publicationId must match an existing publication that is not yet published
$validator->after(function ($validator) use ($props) {
if (isset($props['publicationId']) && !$validator->errors()->get('publicationId')) {
$publication = Repo::publication()->get($props['publicationId']);
if (!$publication) {
$validator->errors()->add('publicationId', __('author.publicationNotFound'));
} elseif ($publication->getData('status') === PKPSubmission::STATUS_PUBLISHED) {
$validator->errors()->add('publicationId', __('author.editPublishedDisabled'));
}
}
});
$errors = [];
if ($validator->fails()) {
$errors = $schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Author::validate', [$errors, $author, $props, $allowedLocales, $primaryLocale]);
return $errors;
}
/**
* @copydoc \PKP\services\entityProperties\EntityWriteInterface::add()
*/
public function add(Author $author): int
{
$existingSeq = $author->getData('seq');
if (!isset($existingSeq)) {
$nextSeq = $this->dao->getNextSeq($author->getData('publicationId'));
$author->setData('seq', $nextSeq);
}
$authorId = $this->dao->insert($author);
$author = Repo::author()->get($authorId);
Hook::call('Author::add', [$author]);
return $author->getId();
}
/**
* @copydoc \PKP\services\entityProperties\EntityWriteInterface::edit()
*/
public function edit(Author $author, array $params)
{
$newAuthor = Repo::author()->newDataObject(array_merge($author->_data, $params));
Hook::call('Author::edit', [$newAuthor, $author, $params]);
$this->dao->update($newAuthor);
Repo::author()->get($newAuthor->getId());
}
/**
* @copydoc \PKP\services\entityProperties\EntityWriteInterface::delete()
*/
public function delete(Author $author)
{
Hook::call('Author::delete::before', [$author]);
$this->dao->delete($author);
$this->dao->resetContributorsOrder($author->getData('publicationId'));
Hook::call('Author::delete', [$author]);
}
/**
* Create an Author object from a User object
*
* This does not save the author in the database.
*/
public function newAuthorFromUser(User $user): Author
{
$author = Repo::author()->newDataObject();
$author->setGivenName($user->getGivenName(null), null);
$author->setFamilyName($user->getFamilyName(null), null);
$author->setAffiliation($user->getAffiliation(null), null);
$author->setCountry($user->getCountry());
$author->setEmail($user->getEmail());
$author->setUrl($user->getUrl());
$author->setBiography($user->getBiography(null), null);
$author->setIncludeInBrowse(1);
$author->setOrcid($user->getOrcid());
Hook::call('Author::newAuthorFromUser', [$author, $user]);
return $author;
}
/**
* Update author names when publication locale changes.
*
* @param int $publicationId
* @param string $oldLocale
* @param string $newLocale
*/
public function changePublicationLocale($publicationId, $oldLocale, $newLocale)
{
$authors = $this->getCollector()
->filterByPublicationIds([$publicationId])
->getMany();
foreach ($authors as $author) {
if (empty($author->getGivenName($newLocale))) {
if (empty($author->getFamilyName($newLocale)) && empty($author->getPreferredPublicName($newLocale))) {
// if no name exists for the new locale
// copy all names with the old locale to the new locale
$author->setGivenName($author->getGivenName($oldLocale), $newLocale);
$author->setFamilyName($author->getFamilyName($oldLocale), $newLocale);
$author->setPreferredPublicName($author->getPreferredPublicName($oldLocale), $newLocale);
} else {
// if the given name does not exist, but one of the other names do exist
// copy only the given name with the old locale to the new locale, because the given name is required
$author->setGivenName($author->getGivenName($oldLocale), $newLocale);
}
$this->dao->update($author);
}
}
}
/**
* Reorders the authors of a publication according to the given order of the authors in the provided author array
*/
public function setAuthorsOrder(int $publicationId, array $authors)
{
$seq = 0;
foreach ($authors as $author) {
$author->setData('seq', $seq);
$this->dao->update($author);
$seq++;
}
}
}
+115
View File
@@ -0,0 +1,115 @@
<?php
/**
* @file classes/author/maps/Schema.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map authors to the properties defined in the announcement schema
*/
namespace PKP\author\maps;
use APP\author\Author;
use APP\facades\Repo;
use Illuminate\Support\Enumerable;
use Illuminate\Support\LazyCollection;
use PKP\core\PKPRequest;
use PKP\security\Role;
use PKP\services\PKPSchemaService;
use PKP\userGroup\UserGroup;
use stdClass;
class Schema extends \PKP\core\maps\Schema
{
public Enumerable $collection;
public string $schema = PKPSchemaService::SCHEMA_AUTHOR;
protected LazyCollection $authorUserGroups;
public function __construct(PKPRequest $request, \PKP\context\Context $context, PKPSchemaService $schemaService)
{
parent::__construct($request, $context, $schemaService);
$this->authorUserGroups = Repo::userGroup()->getByRoleIds([Role::ROLE_ID_AUTHOR], $this->context->getId());
}
/**
* Map an author
*
* Includes all properties in the announcement schema.
*/
public function map(Author $item): array
{
return $this->mapByProperties($this->getProps(), $item);
}
/**
* Summarize an author
*
* Includes properties with the apiSummary flag in the author schema.
*/
public function summarize(Author $item): array
{
return $this->mapByProperties($this->getSummaryProps(), $item);
}
/**
* Map a collection of Authors
*
* @see self::map
*/
public function mapMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->map($item);
});
}
/**
* Summarize a collection of Authors
*
* @see self::summarize
*/
public function summarizeMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($item) {
return $this->summarize($item);
});
}
/**
* Map schema properties of an Author to an assoc array
*/
protected function mapByProperties(array $props, Author $item): array
{
$output = [];
foreach ($props as $prop) {
switch ($prop) {
case 'userGroupName':
/** @var UserGroup $userGroup */
$userGroup = $this->authorUserGroups->first(fn (UserGroup $userGroup) => $userGroup->getId() === $item->getData('userGroupId'));
$output[$prop] = $userGroup ? $userGroup->getName(null) : new stdClass();
break;
case 'fullName':
$output[$prop] = $item->getFullName();
break;
default:
$output[$prop] = $item->getData($prop);
break;
}
}
$output = $this->schemaService->addMissingMultilingualValues($this->schema, $output, $this->context->getSupportedSubmissionLocales());
ksort($output);
return $this->withExtensions($output, $item);
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
/**
* @file classes/cache/APCCache.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class apc_false
*
* @ingroup cache
*
* @see GenericCache
*
* @brief Provides caching based on APC's variable store.
*/
namespace PKP\cache;
class apc_false
{
};
class APCCache extends GenericCache
{
/**
* Flush the cache.
*/
public function flush()
{
$prefix = INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId();
$info = apc_cache_info('user');
foreach ($info['cache_list'] as $entry) {
if (substr($entry['info'], 0, strlen($prefix)) == $prefix) {
apc_delete($entry['info']);
}
}
}
/**
* Get an object from the cache.
*
*/
public function getCache($id)
{
$key = INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId() . ':' . $id;
$returner = unserialize(apc_fetch($key));
if ($returner === false) {
return $this->cacheMiss;
}
if ($returner instanceof apc_false) {
$returner = false;
}
return $returner;
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*
*/
public function setCache($id, $value)
{
$key = INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId() . ':' . $id;
if ($value === false) {
$value = new apc_false();
}
apc_store($key, serialize($value));
}
/**
* Get the time at which the data was cached.
* Not implemented in this type of cache.
*/
public function getCacheTime()
{
return null;
}
/**
* Set the entire contents of the cache.
* WARNING: THIS DOES NOT FLUSH THE CACHE FIRST!
*
* @param array $contents Complete cache contents.
*/
public function setEntireCache($contents)
{
foreach ($contents as $id => $value) {
$this->setCache($id, $value);
}
}
}
+171
View File
@@ -0,0 +1,171 @@
<?php
/**
* @file classes/cache/CacheManager.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @ingroup cache
*
* @see GenericCache
*
* @brief Provides cache management functions.
*
*/
namespace PKP\cache;
use PKP\config\Config;
use PKP\core\Core;
use PKP\core\Registry;
define('CACHE_TYPE_FILE', 1);
define('CACHE_TYPE_OBJECT', 2);
class CacheManager
{
/**
* Get the static instance of the cache manager.
*
* @return CacheManager
*/
public static function getManager()
{
$manager = & Registry::get('cacheManager', true, null);
if ($manager === null) {
$manager = new CacheManager();
}
return $manager;
}
/**
* Get a file cache.
*
* @param string $context
* @param string $cacheId
* @param callable $fallback
*
* @return FileCache
*/
public function getFileCache($context, $cacheId, $fallback)
{
return new FileCache(
$context,
$cacheId,
$fallback,
$this->getFileCachePath()
);
}
public function getObjectCache($context, $cacheId, $fallback)
{
return $this->getCache($context, $cacheId, $fallback, CACHE_TYPE_OBJECT);
}
public function getCacheImplementation($type)
{
switch ($type) {
case CACHE_TYPE_FILE: return 'file';
case CACHE_TYPE_OBJECT: return Config::getVar('cache', 'object_cache');
default: return null;
}
}
/**
* Get a cache.
*
* @param string $context
* @param string $cacheId
* @param ?callable $fallback
* @param string $type Type of cache: CACHE_TYPE_...
*
* @return GenericCache
*/
public function getCache($context, $cacheId, $fallback, $type = CACHE_TYPE_FILE)
{
switch ($this->getCacheImplementation($type)) {
case 'xcache':
$cache = new \PKP\cache\XCacheCache(
$context,
$cacheId,
$fallback
);
break;
case 'apc':
$cache = new \PKP\cache\APCCache(
$context,
$cacheId,
$fallback
);
break;
case 'memcache':
$cache = new \PKP\cache\MemcacheCache(
$context,
$cacheId,
$fallback,
Config::getVar('cache', 'memcache_hostname'),
Config::getVar('cache', 'memcache_port')
);
break;
case '': // Provide a default if not specified
case 'file':
$cache = $this->getFileCache($context, $cacheId, $fallback);
break;
case 'none':
$cache = new \PKP\cache\GenericCache(
$context,
$cacheId,
$fallback
);
break;
default:
exit("Unknown cache type \"{$type}\"!\n");
}
return $cache;
}
/**
* Get the path in which file caches will be stored.
*
* @return string The full path to the file cache directory
*/
public static function getFileCachePath()
{
return Core::getBaseDir() . '/cache';
}
/**
* Flush an entire context, if specified, or
* the whole cache.
*
* @param string $context The context to flush, if only one is to be flushed
* @param string $type The type of cache to flush
*/
public function flush($context = null, $type = CACHE_TYPE_FILE)
{
$cacheImplementation = $this->getCacheImplementation($type);
switch ($cacheImplementation) {
case 'xcache':
case 'apc':
case 'memcache':
$junkCache = $this->getCache($context, null, null);
$junkCache->flush();
break;
case 'file':
$filePath = $this->getFileCachePath();
$files = glob("{$filePath}/fc-" . (isset($context) ? $context . '-' : '') . '*.php');
foreach ($files as $file) {
@unlink($file);
}
break;
case '':
case 'none':
// Nothing necessary.
break;
default:
exit("Unknown cache type \"{$type}\"!\n");
}
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
* @defgroup cache Cache
* Implements various forms of caching, i.e. object caches, file caches, etc.
*/
/**
* @file classes/cache/FileCache.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FileCache
*
* @ingroup cache
*
* @brief Provides caching based on machine-generated PHP code on the filesystem.
*/
namespace PKP\cache;
use Exception;
use PKP\config\Config;
use PKP\file\FileManager;
class FileCache extends GenericCache
{
/**
* Connection to use for caching.
*/
public $filename;
/**
* @var ?array The cached data
*/
public $cache;
/**
* Instantiate a cache.
*/
public function __construct($context, $cacheId, $fallback, $path)
{
parent::__construct($context, $cacheId, $fallback);
$this->filename = "{$path}/fc-{$context}-" . str_replace('/', '.', $cacheId) . '.php';
// If the file couldn't be opened or if a lock couldn't be acquired, quit
if (!($fp = @fopen($this->filename, 'r')) || !flock($fp, LOCK_SH)) {
$this->cache = null;
return;
}
// Reasoning: When the include below fails, it returns "false" and we have no way to determine if it's an error or a valid cache value
set_error_handler(static fn () => throw new Exception('Failed to include file'));
try {
$this->cache = include $this->filename;
} catch (Exception) {
$this->cache = null;
} finally {
restore_error_handler();
flock($fp, LOCK_UN);
fclose($fp);
}
}
/**
* Flush the cache
*/
public function flush()
{
unset($this->cache);
$this->cache = null;
if (function_exists('opcache_invalidate')) {
opcache_invalidate($this->filename, true);
}
@unlink($this->filename);
}
/**
* Get an object from the cache.
*
* @param string $id
*/
public function getCache($id)
{
if (!isset($this->cache)) {
return $this->cacheMiss;
}
return ($this->cache[$id] ?? null);
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*
* @param string $id
*/
public function setCache($id, $value)
{
// Flush the cache; it will be regenerated on demand.
$this->flush();
}
/**
* Set the entire contents of the cache.
*/
public function setEntireCache($contents)
{
if (@file_put_contents(
$this->filename,
'<?php return ' . var_export($contents, true) . ';',
LOCK_EX
) !== false) {
$umask = Config::getVar('files', 'umask');
if ($umask) {
@chmod($this->filename, FileManager::FILE_MODE_MASK & ~$umask);
}
}
$this->cache = $contents;
}
/**
* Get the time at which the data was cached.
* If the file does not exist or an error occurs, null is returned.
*
* @return int|null
*/
public function getCacheTime()
{
$result = @filemtime($this->filename);
if ($result === false) {
return null;
}
return ((int) $result);
}
/**
* Get the entire contents of the cache in an associative array.
*/
public function &getContents()
{
if (!isset($this->cache)) {
// Trigger a cache miss to load the cache.
$this->get(null);
}
return $this->cache;
}
}
+154
View File
@@ -0,0 +1,154 @@
<?php
/**
* @file classes/cache/GenericCache.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class generic_cache_miss
*
* @ingroup cache
*
* @brief Provides implementation-independent caching. Although this class is intended
* to be overridden with a more specific implementation, it can be used as the
* null cache.
*/
namespace PKP\cache;
// Pseudotype to represent a cache miss
class generic_cache_miss
{
}
class GenericCache
{
/**
* The unique string identifying the context of this cache.
* Must be suitable for a filename.
*/
public $context;
/**
* The ID of this particular cache within the context
*/
public $cacheId;
public $cacheMiss;
/**
* The getter fallback callback (for a cache miss)
* This function is called with two parameters:
* 1. The cache object that is suffering a miss
* 2. The id of the value to fetch
* The function is responsible for loading data into the
* cache, using setEntireCache or setCache.
*/
public $fallback;
/**
* Instantiate a cache.
*/
public function __construct($context, $cacheId, $fallback)
{
$this->context = $context;
$this->cacheId = $cacheId;
$this->fallback = $fallback;
$this->cacheMiss = new generic_cache_miss();
}
/**
* Get an object from cache, using the fallback if necessary.
*/
public function get($id)
{
$result = $this->getCache($id);
if (is_object($result) && $result instanceof generic_cache_miss) {
$result = call_user_func_array($this->fallback, [$this, $id]);
}
return $result;
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*/
public function set($id, $value)
{
return $this->setCache($id, $value);
}
/**
* Flush the cache.
*/
public function flush()
{
}
/**
* Set the entire contents of the cache. May (should) be overridden
* by subclasses.
*/
public function setEntireCache($contents)
{
$this->flush();
foreach ($contents as $id => $value) {
$this->setCache($id, $value);
}
}
/**
* Get an object from the cache. This function should be overridden
* by subclasses.
*
* @param string $id
*/
public function getCache($id)
{
return $this->cacheMiss;
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*
* @param string $id
*/
public function setCache($id, $value)
{
}
/**
* Close the cache. (Optionally overridden by subclasses.)
*/
public function close()
{
}
/**
* Get the context.
*/
public function getContext()
{
return $this->context;
}
/**
* Get the cache ID within its context
*/
public function getCacheId()
{
return $this->cacheId;
}
/**
* Get the time at which the data was cached.
*/
public function getCacheTime()
{
// Since it's not really cached, we'll consider it to have been cached just now.
return time();
}
}
+166
View File
@@ -0,0 +1,166 @@
<?php
/**
* @file classes/cache/MemcacheCache.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class memcache_null
*
* @ingroup cache
*
* @see GenericCache
*
* @brief Provides caching based on Memcache.
*/
namespace PKP\cache;
use Memcached;
// WARNING: This cache MUST be loaded in batch, or else many cache
// misses will result.
// Pseudotypes used to represent false and null values in the cache
class memcache_false
{
}
class memcache_null
{
}
class MemcacheCache extends GenericCache
{
/**
* Connection to use for caching.
*/
public $connection;
/**
* Flag (used by Memcache::set)
*/
public $flag;
/**
* Expiry (used by Memcache::set)
*/
public $expire;
/**
* Instantiate a cache.
*/
public function __construct($context, $cacheId, $fallback, $hostname, $port)
{
parent::__construct($context, $cacheId, $fallback);
$this->connection = new Memcached();
// FIXME This should use connection pooling
// XXX check whether memcached server is usable
if (!$this->connection->addServer($hostname, $port)) {
$this->connection = null;
}
$this->flag = null;
$this->expire = 3600; // 1 hour default expiry
}
/**
* Set the flag (used in Memcache::set)
*/
public function setFlag($flag)
{
$this->flag = $flag;
}
/**
* Set the expiry time (used in Memcache::set)
*/
public function setExpiry($expiry)
{
$this->expire = $expiry;
}
/**
* Flush the cache.
*/
public function flush()
{
$this->connection->flush();
}
/**
* Get an object from the cache.
*
* @param string $id
*/
public function getCache($id)
{
$result = $this->connection->get($this->getContext() . ':' . $this->getCacheId() . ':' . $id);
if ($this->connection->getResultCode() == Memcached::RES_NOTFOUND) {
return $this->cacheMiss;
}
if ($result instanceof memcache_false) {
$result = false;
}
if ($result instanceof memcache_null) {
$result = null;
}
return $result;
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*
* @param string $id
*/
public function setCache($id, $value)
{
if ($value === false) {
$value = new memcache_false();
} elseif ($value === null) {
$value = new memcache_null();
}
return ($this->connection->set($this->getContext() . ':' . $this->getCacheId() . ':' . $id, $value, $this->expire));
}
/**
* Close the cache and free resources.
*/
public function close()
{
$this->connection->quit();
unset($this->connection);
$this->contextChecked = false;
}
/**
* Get the time at which the data was cached.
* Note that keys expire in memcache, which means
* that it's possible that the date will disappear
* before the data -- in this case we'll have to
* assume the data is still good.
*/
public function getCacheTime()
{
return null;
}
/**
* Set the entire contents of the cache.
* WARNING: THIS DOES NOT FLUSH THE CACHE FIRST!
* This is because there is no "scope restriction"
* for flushing within memcache and therefore
* a flush here would flush the entire cache,
* resulting in more subsequent calls to this function,
* resulting in more flushes, etc.
*/
public function setEntireCache($contents)
{
foreach ($contents as $id => $value) {
$this->setCache($id, $value);
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
/**
* @file classes/cache/XCacheCache.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class XCacheCache
*
* @ingroup cache
*
* @see GenericCache
*
* @brief Provides caching based on XCache's variable store.
*/
namespace PKP\cache;
class XCacheCache extends GenericCache
{
/**
* Flush the cache.
*/
public function flush()
{
$prefix = INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId();
if (function_exists('xcache_unset_by_prefix')) {
// If possible, just flush the context
xcache_unset_by_prefix(prefix);
} else {
// Otherwise, we need to do this manually
for ($i = 0; $i < xcache_count(XC_TYPE_VAR); $i++) {
$cache = xcache_list(XC_TYPE_VAR, $i);
foreach ($cache['cache_list'] as $entry) {
if (substr($entry['name'], 0, strlen($prefix)) == $prefix) {
xcache_unset($entry['name']);
}
}
}
}
}
/**
* Get an object from the cache.
*
* @param string $id
*/
public function getCache($id)
{
$key = INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId() . ':' . $id;
if (!xcache_isset($key)) {
return $this->cacheMiss;
}
$returner = unserialize(xcache_get($key));
return $returner;
}
/**
* Set an object in the cache. This function should be overridden
* by subclasses.
*
* @param string $id
*/
public function setCache($id, $value)
{
return (xcache_set(INDEX_FILE_LOCATION . ':' . $this->getContext() . ':' . $this->getCacheId() . ':' . $id, serialize($value)));
}
/**
* Get the time at which the data was cached.
* Not implemented in this type of cache.
*/
public function getCacheTime()
{
return null;
}
/**
* Set the entire contents of the cache.
* WARNING: THIS DOES NOT FLUSH THE CACHE FIRST!
*/
public function setEntireCache($contents)
{
foreach ($contents as $id => $value) {
$this->setCache($id, $value);
}
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
/**
* @file classes/category/Category.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Category
*
* @brief Describes basic Category properties.
*/
namespace PKP\category;
class Category extends \PKP\core\DataObject
{
/**
* Get ID of context.
*/
public function getContextId(): int
{
return $this->getData('contextId');
}
/**
* Set ID of context.
*/
public function setContextId(int $contextId)
{
return $this->setData('contextId', $contextId);
}
/**
* Get ID of parent category.
*/
public function getParentId(): ?int
{
return $this->getData('parentId');
}
/**
* Set ID of parent category.
*/
public function setParentId(?int $parentId)
{
return $this->setData('parentId', $parentId);
}
/**
* Get sequence of category.
*/
public function getSequence(): float
{
return (float) $this->getData('sequence');
}
/**
* Set sequence of category.
*/
public function setSequence(float $sequence)
{
return $this->setData('sequence', $sequence);
}
/**
* Get category path.
*/
public function getPath(): string
{
return $this->getData('path');
}
/**
* Set category path.
*/
public function setPath(string $path)
{
return $this->setData('path', $path);
}
/**
* Get localized title of the category.
*/
public function getLocalizedTitle(): string
{
return $this->getLocalizedData('title');
}
/**
* Get title of category.
*/
public function getTitle(?string $locale = null)
{
return $this->getData('title', $locale);
}
/**
* Set title of category.
*/
public function setTitle($title, ?string $locale)
{
return $this->setData('title', $title, $locale);
}
/**
* Get localized description of the category.
*/
public function getLocalizedDescription(): ?string
{
return $this->getLocalizedData('description');
}
/**
* Get description of category.
*/
public function getDescription(?string $locale)
{
return $this->getData('description', $locale);
}
/**
* Set description of category.
*/
public function setDescription($description, ?string $locale)
{
return $this->setData('description', $description, $locale);
}
/**
* Get the image.
*/
public function getImage(): ?array
{
return $this->getData('image');
}
/**
* Set the image.
*/
public function setImage(?array $image)
{
return $this->setData('image', $image);
}
/**
* Get the option how the books in this category should be sorted,
* in the form: concat(sortBy, sortDir).
*/
public function getSortOption(): ?string
{
return $this->getData('sortOption');
}
/**
* Set the option how the books in this category should be sorted,
* in the form: concat(sortBy, sortDir).
*/
public function setSortOption(?string $sortOption)
{
return $this->setData('sortOption', $sortOption);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\category\Category', '\Category');
}
+170
View File
@@ -0,0 +1,170 @@
<?php
/**
* @file classes/category/Collector.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Collector
*
* @brief A helper class to configure a Query Builder to get a collection of categories
*/
namespace PKP\category;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\interfaces\CollectorInterface;
use PKP\plugins\Hook;
/**
* @template T of Category
*/
class Collector implements CollectorInterface
{
public DAO $dao;
public ?array $contextIds = null;
public ?array $parentIds = null;
public ?array $paths = null;
public ?array $publicationIds = null;
public ?int $count = null;
public ?int $offset = null;
public function __construct(DAO $dao)
{
$this->dao = $dao;
}
public function getCount(): int
{
return $this->dao->getCount($this);
}
/**
* @return Collection<int,int>
*/
public function getIds(): Collection
{
return $this->dao->getIds($this);
}
/**
* @copydoc DAO::getMany()
* @return LazyCollection<int,T>
*/
public function getMany(): LazyCollection
{
return $this->dao->getMany($this);
}
/**
* Filter categories by one or more contexts
*/
public function filterByContextIds(?array $contextIds): self
{
$this->contextIds = $contextIds;
return $this;
}
/**
* Filter categories by one or more parent category IDs
*/
public function filterByParentIds(?array $parentIds): self
{
$this->parentIds = $parentIds;
return $this;
}
/**
* Filter categories by one or more publication IDs
*/
public function filterByPublicationIds(?array $publicationIds): self
{
$this->publicationIds = $publicationIds;
return $this;
}
/**
* Filter categories by one or more paths
*/
public function filterByPaths(?array $paths): self
{
$this->paths = $paths;
return $this;
}
/**
* Limit the number of objects retrieved
*/
public function limit(?int $count): self
{
$this->count = $count;
return $this;
}
/**
* Offset the number of objects retrieved, for example to
* retrieve the second page of contents
*/
public function offset(?int $offset): self
{
$this->offset = $offset;
return $this;
}
/**
* @copydoc CollectorInterface::getQueryBuilder()
*/
public function getQueryBuilder(): Builder
{
$qb = DB::table($this->dao->table . ' as c')
->leftJoin('categories AS pc', 'c.parent_id', '=', 'pc.category_id')
->select(['c.*']);
$qb->when($this->contextIds !== null, function ($query) {
$query->whereIn('c.context_id', $this->contextIds);
});
$qb->when($this->paths !== null, function ($query) {
$query->whereIn('c.path', $this->paths);
});
$qb->when($this->publicationIds !== null, function ($query) {
$query->whereIn('c.category_id', function ($query) {
$query->select('category_id')->from('publication_categories')->whereIn('publication_id', $this->publicationIds);
});
});
$qb->when($this->parentIds !== null, function ($query) {
// parentIds may contain mixed values and nulls; make sure the mix translates into the query accurately
$nonNullParentIds = array_filter($this->parentIds);
if (count($nonNullParentIds)) {
$query->whereIn('c.parent_id', array_filter($this->parentIds));
}
if (in_array(null, $this->parentIds)) {
if (count($nonNullParentIds)) {
$query->orWhereNull('c.parent_id');
} else {
$query->whereNull('c.parent_id');
}
}
});
$qb->orderBy(DB::raw('(COALESCE((pc.seq * 8192) + pc.category_id, 0) * 8192) + CASE WHEN pc.category_id IS NULL THEN 8192 * ((c.seq * 8192) + c.category_id) ELSE c.seq END'));
if (isset($this->count)) {
$qb->limit($this->count);
}
if (isset($this->offset)) {
$qb->offset($this->offset);
}
Hook::call('Category::Collector', [&$qb, $this]);
return $qb;
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
/**
* @file classes/category/DAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class DAO
*
* @brief Read and write categories to the database.
*/
namespace PKP\category;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\LazyCollection;
use PKP\core\EntityDAO;
use PKP\core\traits\EntityWithParent;
/**
* @template T of Category
* @extends EntityDAO<T>
*/
class DAO extends EntityDAO
{
use EntityWithParent;
/** @copydoc EntityDAO::$schema */
public $schema = \PKP\services\PKPSchemaService::SCHEMA_CATEGORY;
/** @copydoc EntityDAO::$table */
public $table = 'categories';
/** @copydoc EntityDAO::$settingsTable */
public $settingsTable = 'category_settings';
/** @copydoc EntityDAO::$primaryKeyColumn */
public $primaryKeyColumn = 'category_id';
/** @copydoc EntityDAO::$primaryTableColumns */
public $primaryTableColumns = [
'id' => 'category_id',
'parentId' => 'parent_id',
'contextId' => 'context_id',
'sequence' => 'seq',
'path' => 'path',
'image' => 'image',
];
/**
* Get the parent object ID column name
*/
public function getParentColumn(): string
{
return 'context_id';
}
/**
* Instantiate a new DataObject
*/
public function newDataObject(): Category
{
return app(Category::class);
}
/**
* Get the number of categories matching the configured query
*/
public function getCount(Collector $query): int
{
return $query->getQueryBuilder()->count();
}
/**
* Get a list of ids matching the configured query
*
* @return Collection<int,int>
*/
public function getIds(Collector $query): Collection
{
return $query
->getQueryBuilder()
->pluck('c.' . $this->primaryKeyColumn);
}
/**
* Get a collection of categories matching the configured query
* @return LazyCollection<int,T>
*/
public function getMany(Collector $query): LazyCollection
{
$rows = $query
->getQueryBuilder()
->get();
return LazyCollection::make(function () use ($rows) {
foreach ($rows as $row) {
yield $row->category_id => $this->fromRow($row);
}
});
}
/**
* @copydoc EntityDAO::fromRow()
*/
public function fromRow(object $row): Category
{
return parent::fromRow($row);
}
/**
* @copydoc EntityDAO::insert()
*/
public function insert(Category $category): int
{
return parent::_insert($category);
}
/**
* @copydoc EntityDAO::update()
*/
public function update(Category $category)
{
parent::_update($category);
}
/**
* @copydoc EntityDAO::delete()
*/
public function delete(Category $category)
{
parent::_delete($category);
}
/**
* Sequentially renumber categories in their sequence order by context ID and optionally parent category ID.
*
* @param int $parentCategoryId Optional parent category ID
*/
public function resequenceCategories(int $contextId, ?int $parentCategoryId = null)
{
$categoryIds = DB::table('categories')
->where('context_id', '=', $contextId)
->when($parentCategoryId !== null, function ($query) use ($parentCategoryId) {
$query->where($parentCategoryId, '=', $parentCategoryId);
})->pluck('category_id');
$i = 0;
foreach ($categoryIds as $categoryId) {
DB::table('categories')->where('category_id', '=', $categoryId)->update(['seq' => ++$i]);
}
}
/**
* Assign a publication to a category
*/
public function insertPublicationAssignment(int $categoryId, int $publicationId)
{
DB::table('publication_categories')->insert([
'category_id' => $categoryId,
'publication_id' => $publicationId,
]);
}
/**
* Delete the assignment of a category to a publication
*/
public function deletePublicationAssignments(int $publicationId)
{
DB::table('publication_categories')->where('publication_id', '=', $publicationId)->delete();
}
}
+191
View File
@@ -0,0 +1,191 @@
<?php
/**
* @file classes/category/Repository.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Repository
*
* @brief A repository to find and manage categories.
*/
namespace PKP\category;
use APP\core\Request;
use Illuminate\Support\LazyCollection;
use PKP\plugins\Hook;
use PKP\services\PKPSchemaService;
use PKP\validation\ValidatorFactory;
class Repository
{
/** @var DAO $dao */
public $dao;
/** @var string $schemaMap The name of the class to map this entity to its schema */
public $schemaMap = maps\Schema::class;
/** @var Request $request */
protected $request;
/** @var PKPSchemaService<Category> $schemaService */
protected $schemaService;
public function __construct(DAO $dao, Request $request, PKPSchemaService $schemaService)
{
$this->dao = $dao;
$this->request = $request;
$this->schemaService = $schemaService;
}
/** @copydoc DAO::newDataObject() */
public function newDataObject(array $params = []): Category
{
$object = $this->dao->newDataObject();
if (!empty($params)) {
$object->setAllData($params);
}
return $object;
}
/** @copydoc DAO::get() */
public function get(int $id, int $contextId = null): ?Category
{
return $this->dao->get($id, $contextId);
}
/** @copydoc DAO::exists() */
public function exists(int $id, int $contextId = null): bool
{
return $this->dao->exists($id, $contextId);
}
/**
* Get the breadcrumb of a category
*
* @return string For example: Social Sciences > Anthropology
*/
public function getBreadcrumb(Category $category, ?Category $parent = null): string
{
return !$parent
? $category->getLocalizedTitle()
: __('common.categorySeparator', [
'parent' => $parent->getLocalizedTitle(),
'child' => $category->getLocalizedTitle()
]);
}
/**
* Get the breadcrumbs for a Collection of categories
*/
public function getBreadcrumbs(LazyCollection $categories): LazyCollection
{
return $categories->map(function (Category $category) use ($categories) {
/** @var ?Category $parent */
$parent = $categories->first(
fn (Category $c) => $c->getId() === $category->getParentId()
);
return $this->getBreadcrumb($category, $parent);
});
}
/** @copydoc DAO::getCollector() */
public function getCollector(): Collector
{
return app(Collector::class);
}
/**
* Get an instance of the map class for mapping
* announcements to their schema
*/
public function getSchemaMap(): maps\Schema
{
return app('maps')->withExtensions($this->schemaMap);
}
/**
* Validate properties for a category
*
* Perform validation checks on data used to add or edit a category.
*
* @param array $props A key/value array with the new data to validate
* @param array $allowedLocales The context's supported locales
* @param string $primaryLocale The context's primary locale
*
* @return array A key/value array with validation errors. Empty if no errors
*/
public function validate(?Category $object, array $props, array $allowedLocales, string $primaryLocale): array
{
$validator = ValidatorFactory::make(
$props,
$this->schemaService->getValidationRules($this->dao->schema, $allowedLocales),
[]
);
// Check required fields
ValidatorFactory::required(
$validator,
$object,
$this->schemaService->getRequiredProps($this->dao->schema),
$this->schemaService->getMultilingualProps($this->dao->schema),
$allowedLocales,
$primaryLocale
);
// Check for input from disallowed locales
ValidatorFactory::allowedLocales($validator, $this->schemaService->getMultilingualProps($this->dao->schema), $allowedLocales);
$errors = [];
if ($validator->fails()) {
$errors = $this->schemaService->formatValidationErrors($validator->errors());
}
Hook::call('Category::validate', [&$errors, $object, $props, $allowedLocales, $primaryLocale]);
return $errors;
}
/** @copydoc DAO::insert() */
public function add(Category $category): int
{
$id = $this->dao->insert($category);
Hook::call('Category::add', [$category]);
return $id;
}
/** @copydoc DAO::update() */
public function edit(Category $category, array $params)
{
$newCategory = clone $category;
$newCategory->setAllData(array_merge($newCategory->_data, $params));
Hook::call('Category::edit', [$newCategory, $category, $params]);
$this->dao->update($newCategory);
}
/** @copydoc DAO::delete() */
public function delete(Category $category)
{
Hook::call('Category::delete::before', [$category]);
$this->dao->delete($category);
Hook::call('Category::delete', [$category]);
}
/**
* Delete a collection of categories
*/
public function deleteMany(Collector $collector)
{
foreach ($collector->getMany() as $category) {
/** @var Category $category */
$this->delete($category);
}
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
/**
* @file classes/category/maps/Schema.php
*
* Copyright (c) 2014-2020 Simon Fraser University
* Copyright (c) 2000-2020 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Schema
*
* @brief Map categories to the properties defined in the category schema
*/
namespace PKP\category\maps;
use Illuminate\Support\Enumerable;
use PKP\category\Category;
use PKP\services\PKPSchemaService;
class Schema extends \PKP\core\maps\Schema
{
public string $schema = PKPSchemaService::SCHEMA_CATEGORY;
/**
* Map a category
*
* Includes all properties in the category schema.
*/
public function map(Category $category): array
{
return $this->mapByProperties($this->getProps(), $category);
}
/**
* Summarize a category
*
* Includes properties with the apiSummary flag in the category schema.
*/
public function summarize(Category $category): array
{
return $this->mapByProperties($this->getSummaryProps(), $category);
}
/**
* Map a collection of Categories
*
* @see self::map
*/
public function mapMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($category) {
return $this->map($category);
});
}
/**
* Summarize a collection of Categories
*
* @see self::summarize
*/
public function summarizeMany(Enumerable $collection): Enumerable
{
$this->collection = $collection;
return $collection->map(function ($category) {
return $this->summarize($category);
});
}
/**
* Map schema properties of a Category to an assoc array
*/
protected function mapByProperties(array $props, Category $category): array
{
$output = [];
foreach ($props as $prop) {
switch ($prop) {
default:
$output[$prop] = $category->getData($prop);
break;
}
}
return $output;
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* @defgroup citation Citation
*/
/**
* @file classes/citation/Citation.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Citation
*
* @ingroup citation
*
* @brief Class representing a citation (bibliographic reference)
*/
namespace PKP\citation;
use PKP\core\PKPString;
class Citation extends \PKP\core\DataObject
{
/**
* Constructor.
*
* @param string $rawCitation an unparsed citation string
*/
public function __construct($rawCitation = null)
{
parent::__construct();
$this->setRawCitation($rawCitation);
}
//
// Getters and Setters
//
/**
* Replace URLs through HTML links, if the citation does not already contain HTML links
*
* @return string
*/
public function getCitationWithLinks()
{
$citation = $this->getRawCitation();
if (stripos($citation, '<a href=') === false) {
$citation = preg_replace_callback(
'#(http|https|ftp)://[\d\w\.-]+\.[\w\.]{2,6}[^\s\]\[\<\>]*/?#',
function ($matches) {
$trailingDot = in_array($char = substr($matches[0], -1), ['.', ',']);
$url = rtrim($matches[0], '.,');
return "<a href=\"{$url}\">{$url}</a>" . ($trailingDot ? $char : '');
},
$citation
);
}
return $citation;
}
/**
* Get the rawCitation
*
* @return string
*/
public function getRawCitation()
{
return $this->getData('rawCitation');
}
/**
* Set the rawCitation
*
* @param string $rawCitation
*/
public function setRawCitation($rawCitation)
{
$rawCitation = $this->_cleanCitationString($rawCitation);
$this->setData('rawCitation', $rawCitation);
}
/**
* Get the sequence number
*
* @return int
*/
public function getSequence()
{
return $this->getData('seq');
}
/**
* Set the sequence number
*
* @param int $seq
*/
public function setSequence($seq)
{
$this->setData('seq', $seq);
}
//
// Private methods
//
/**
* Take a citation string and clean/normalize it
*
* @param string $citationString
*
* @return string
*/
public function _cleanCitationString($citationString)
{
// 1) Strip slashes and whitespace
$citationString = trim(stripslashes($citationString));
// 2) Normalize whitespace
$citationString = PKPString::regexp_replace('/[\s]+/', ' ', $citationString);
return $citationString;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\citation\Citation', '\Citation');
}
+253
View File
@@ -0,0 +1,253 @@
<?php
/**
* @file classes/citation/CitationDAO.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CitationDAO
*
* @ingroup citation
*
* @see Citation
*
* @brief Operations for retrieving and modifying Citation objects
*/
namespace PKP\citation;
use PKP\db\DAOResultFactory;
use PKP\plugins\Hook;
class CitationDAO extends \PKP\db\DAO
{
/**
* Insert a new citation.
*
* @param Citation $citation
*
* @return int the new citation id
*/
public function insertObject($citation)
{
$seq = $citation->getSequence();
if (!(is_numeric($seq) && $seq > 0)) {
// Find the latest sequence number
$result = $this->retrieve(
'SELECT MAX(seq) AS lastseq FROM citations
WHERE publication_id = ?',
[(int)$citation->getData('publicationId')]
);
$row = $result->current();
$citation->setSequence($row ? $row->lastseq + 1 : 1);
}
$this->update(
sprintf('INSERT INTO citations
(publication_id, raw_citation, seq)
VALUES
(?, ?, ?)'),
[
(int) $citation->getData('publicationId'),
$citation->getRawCitation(),
(int) $seq
]
);
$citation->setId($this->getInsertId());
$this->_updateObjectMetadata($citation);
return $citation->getId();
}
/**
* Retrieve a citation by id.
*
* @param int $citationId
*
* @return ?Citation
*/
public function getById($citationId)
{
$result = $this->retrieve(
'SELECT * FROM citations WHERE citation_id = ?',
[$citationId]
);
$row = $result->current();
return $row ? $this->_fromRow((array) $row) : null;
}
/**
* Import citations from a raw citation list of the particular publication.
*
* @param int $publicationId
* @param string $rawCitationList
*/
public function importCitations($publicationId, $rawCitationList)
{
assert(is_numeric($publicationId));
$publicationId = (int) $publicationId;
$existingCitations = $this->getByPublicationId($publicationId)->toAssociativeArray();
// Remove existing citations.
$this->deleteByPublicationId($publicationId);
// Tokenize raw citations
$citationTokenizer = new CitationListTokenizerFilter();
$citationStrings = $citationTokenizer->execute($rawCitationList);
// Instantiate and persist citations
$importedCitations = [];
if (is_array($citationStrings)) {
foreach ($citationStrings as $seq => $citationString) {
if (!empty(trim($citationString))) {
$citation = new Citation($citationString);
// Set the publication
$citation->setData('publicationId', $publicationId);
// Set the counter
$citation->setSequence($seq + 1);
$this->insertObject($citation);
$importedCitations[] = $citation;
}
}
}
Hook::call('CitationDAO::afterImportCitations', [$publicationId, $existingCitations, $importedCitations]);
}
/**
* Retrieve an array of citations matching a particular publication id.
*
* @param int $publicationId
* @param ?\PKP\db\DBResultRange $rangeInfo
*
* @return DAOResultFactory<Citation> containing matching Citations
*/
public function getByPublicationId($publicationId, $rangeInfo = null)
{
$result = $this->retrieveRange(
'SELECT *
FROM citations
WHERE publication_id = ?
ORDER BY seq, citation_id',
[(int)$publicationId],
$rangeInfo
);
return new DAOResultFactory($result, $this, '_fromRow', ['id']);
}
/**
* Update an existing citation.
*
* @param Citation $citation
*/
public function updateObject($citation)
{
$returner = $this->update(
'UPDATE citations
SET publication_id = ?,
raw_citation = ?,
seq = ?
WHERE citation_id = ?',
[
(int) $citation->getData('publicationId'),
$citation->getRawCitation(),
(int) $citation->getSequence(),
(int) $citation->getId()
]
);
$this->_updateObjectMetadata($citation);
}
/**
* Delete a citation.
*
* @param Citation $citation
*
* @return bool
*/
public function deleteObject($citation)
{
return $this->deleteById($citation->getId());
}
/**
* Delete a citation by id.
*
* @param int $citationId
*
* @return bool
*/
public function deleteById($citationId)
{
$this->update('DELETE FROM citation_settings WHERE citation_id = ?', [(int) $citationId]);
return $this->update('DELETE FROM citations WHERE citation_id = ?', [(int) $citationId]);
}
/**
* Delete all citations matching a particular publication id.
*
* @param int $publicationId
*
* @return bool
*/
public function deleteByPublicationId($publicationId)
{
$citations = $this->getByPublicationId($publicationId);
while ($citation = $citations->next()) {
$this->deleteById($citation->getId());
}
return true;
}
//
// Private helper methods
//
/**
* Construct a new citation object.
*
* @return Citation
*/
public function _newDataObject()
{
return new Citation();
}
/**
* Internal function to return a citation object from a
* row.
*
* @param array $row
*
* @return Citation
*/
public function _fromRow($row)
{
$citation = $this->_newDataObject();
$citation->setId((int)$row['citation_id']);
$citation->setData('publicationId', (int)$row['publication_id']);
$citation->setRawCitation($row['raw_citation']);
$citation->setSequence((int)$row['seq']);
$this->getDataObjectSettings('citation_settings', 'citation_id', $row['citation_id'], $citation);
return $citation;
}
/**
* Update the citation meta-data
*
* @param Citation $citation
*/
public function _updateObjectMetadata($citation)
{
$this->updateDataObjectSettings('citation_settings', $citation, ['citation_id' => $citation->getId()]);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\citation\CitationDAO', '\CitationDAO');
}
@@ -0,0 +1,70 @@
<?php
/**
* @file classes/citation/CitationListTokenizerFilter.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CitationListTokenizerFilter
*
* @ingroup classes_citation
*
* @brief Class that takes an unformatted list of citations
* and returns an array of raw citation strings.
*/
namespace PKP\citation;
use PKP\core\PKPString;
use PKP\filter\Filter;
class CitationListTokenizerFilter extends Filter
{
/**
* Constructor
*/
public function __construct()
{
$this->setDisplayName('Split a reference list into separate citations');
parent::__construct('primitive::string', 'primitive::string[]');
}
//
// Implement template methods from Filter
//
/**
* @see Filter::process()
*
* @param string $input
*
* @return mixed array
*/
public function &process(&$input)
{
// The default implementation assumes that raw citations are
// separated with line endings.
// 1) Remove empty lines and normalize line endings.
$input = PKPString::regexp_replace('/[\r\n]+/s', "\n", $input);
// 2) Remove trailing/leading line breaks.
$input = trim($input, "\n");
// 3) Break up at line endings.
if (empty($input)) {
$citations = [];
} else {
$citations = explode("\n", $input);
}
// 4) Remove numbers from the beginning of each citation.
foreach ($citations as $index => $citation) {
$citations[$index] = PKPString::regexp_replace('/^\s*[\[#]?[0-9]+[.)\]]?\s*/', '', $citation);
}
return $citations;
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\citation\CitationListTokenizerFilter', '\CitationListTokenizerFilter');
}
+168
View File
@@ -0,0 +1,168 @@
<?php
/**
* @defgroup tools Tools
* Implements command-line management tools for PKP software.
*/
/**
* @file classes/cliTool/CommandLineTool.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class CommandLineTool
*
* @ingroup tools
*
* @brief Initialization code for command-line scripts.
*
* FIXME: Write a PKPCliRequest and PKPCliRouter class and use the dispatcher
* to bootstrap and route tool requests.
*/
namespace PKP\cliTool;
use APP\core\Application;
use APP\core\PageRouter;
use APP\facades\Repo;
use PKP\core\Registry;
use PKP\plugins\PluginRegistry;
use PKP\security\Role;
use PKP\session\SessionManager;
use PKP\user\User;
use PKP\config\Config;
/** Initialization code */
define('PWD', getcwd());
chdir(dirname(INDEX_FILE_LOCATION)); /* Change to base directory */
if (!defined('STDIN')) {
define('STDIN', fopen('php://stdin', 'r'));
}
require_once './lib/pkp/includes/bootstrap.php';
SessionManager::disable();
class CommandLineTool
{
/** @var string the script being executed */
public $scriptName;
/** @vary array Command-line arguments */
public $argv;
/** @var string the username provided */
public ?string $username = null;
/** @var \PKP\user\User the user provided */
public ?User $user = null;
public function __construct($argv = [])
{
// Initialize the request object with a page router
$application = Application::get();
$request = $application->getRequest();
// FIXME: Write and use a CLIRouter here (see classdoc)
$router = new PageRouter();
$router->setApplication($application);
$request->setRouter($router);
// Initialize the locale and load generic plugins.
PluginRegistry::loadCategory('generic');
$this->argv = isset($argv) && is_array($argv) ? $argv : [];
if (isset($_SERVER['SERVER_NAME'])) {
exit('This script can only be executed from the command-line');
}
$this->scriptName = isset($this->argv[0]) ? array_shift($this->argv) : '';
if (Config::getVar('general', 'installed')) $this->checkArgsForUsername();
if (isset($this->argv[0]) && $this->argv[0] == '-h') {
$this->exitWithUsageMessage();
}
}
public function usage()
{
}
private function checkArgsForUsername()
{
$usernameKeyPos = array_search('--user_name', $this->argv);
if (!$usernameKeyPos) {
$usernameKeyPos = array_search('-u', $this->argv);
}
if ($usernameKeyPos) {
$usernamePos = $usernameKeyPos + 1;
if (count($this->argv) >= $usernamePos + 1) {
$this->username = $this->argv[$usernamePos];
unset($this->argv[$usernamePos]);
}
unset($this->argv[$usernameKeyPos]);
}
if ($this->username) {
$user = Repo::user()->getByUsername($this->username, true);
$this->setUser($user);
}
if (!$this->user) {
$adminGroups = Repo::userGroup()->getArrayIdByRoleId(Role::ROLE_ID_SITE_ADMIN);
if (count($adminGroups)) {
$groupUsers = Repo::user()->getCollector()
->filterByUserGroupIds([$adminGroups[0]])
->getMany();
if ($groupUsers->isNotEmpty()) {
$this->setUser($groupUsers->first());
} else {
$this->exitWithUsageMessage();
}
}
}
}
/**
* Sets the user for the CLI Tool
*
* @param \PKP\user\User $user The user to set as the execution user of this CLI command
*/
public function setUser($user)
{
$registeredUser = Registry::get('user', true, null);
if (!isset($registeredUser)) {
/**
* This is used in order to reconcile with possible $request->getUser()
* used inside import processes, when the import is done by CLI tool.
*/
if ($user) {
Registry::set('user', $user);
$this->user = $user;
}
} else {
$this->user = $registeredUser;
}
}
/**
* Exit the CLI tool if an error occurs
*/
public function exitWithUsageMessage()
{
$this->usage();
exit(0);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\cliTool\CommandLineTool', '\CommandLineTool');
}
@@ -0,0 +1,36 @@
<?php
/**
* @file lib/pkp/classes/cliTool/ConvertLogFileTool.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ConvertLogFileTool
*
* @ingroup tools
*
* @brief Tool to convert usage stats log file (used in releases < 3.4) into the new format.
*
*/
namespace PKP\cliTool;
use PKP\cliTool\traits\ConvertLogFile;
abstract class ConvertLogFileTool extends \PKP\cliTool\CommandLineTool
{
use ConvertLogFile;
/**
* Constructor.
*
* @param array $argv command-line arguments (see usage)
*/
public function __construct($argv = [])
{
parent::__construct($argv);
$this->__constructTrait();
}
}
+261
View File
@@ -0,0 +1,261 @@
<?php
/**
* @file classes/cliTool/InstallTool.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class installTool
*
* @ingroup tools
*
* @brief CLI tool for installing a PKP app.
*/
namespace PKP\cliTool;
use APP\install\Install;
use PKP\install\form\InstallForm;
class InstallTool extends \PKP\cliTool\CommandLineTool
{
/** @var array installation parameters */
public $params;
/**
* Print command usage information.
*/
public function usage()
{
echo "Install tool\n"
. "Usage: {$this->scriptName}\n";
}
/**
* Execute the script.
*/
public function execute()
{
if ($this->readParams()) {
$this->install();
}
}
/**
* Perform installation.
*/
public function install()
{
$installer = new Install($this->params);
$installer->setLogger($this);
if ($installer->execute()) {
if (count($installer->getNotes()) > 0) {
printf("\nRelease Notes\n");
printf("----------------------------------------\n");
foreach ($installer->getNotes() as $note) {
printf("%s\n\n", $note);
}
}
if (!$installer->wroteConfig()) {
printf("\nNew config.inc.php:\n");
printf("----------------------------------------\n");
echo $installer->getConfigContents();
printf("----------------------------------------\n");
}
$newVersion = $installer->getNewVersion();
printf("Successfully installed version %s\n", $newVersion->getVersionString(false));
} else {
printf("ERROR: Installation failed: %s\n", $installer->getErrorString());
}
}
/**
* Read installation parameters from stdin.
* FIXME: May want to implement an abstract "CLIForm" class handling input/validation.
* FIXME: Use readline if available?
*/
public function readParams()
{
$installForm = new InstallForm(null); // Request object not available to CLI
// Locale Settings
$this->printTitle('installer.localeSettings');
$this->readParamOptions('locale', 'locale.primary', $installForm->supportedLocales, 'en');
$this->readParamOptions('additionalLocales', 'installer.additionalLocales', $installForm->supportedLocales, '', true);
// File Settings
$this->printTitle('installer.fileSettings');
$this->readParam('filesDir', 'installer.filesDir');
// Administrator Account
$this->printTitle('installer.administratorAccount');
$this->readParam('adminUsername', 'user.username');
@`/bin/stty -echo`;
do {
$this->readParam('adminPassword', 'user.password');
printf("\n");
$this->readParam('adminPassword2', 'user.repeatPassword');
printf("\n");
} while ($this->params['adminPassword'] != $this->params['adminPassword2']);
@`/bin/stty echo`;
$this->readParam('adminEmail', 'user.email');
// Database Settings
$this->printTitle('installer.databaseSettings');
$this->readParamOptions('databaseDriver', 'installer.databaseDriver', $installForm->getDatabaseDriversOptions());
$this->readParam('databaseHost', 'installer.databaseHost', '');
$this->readParam('databaseUsername', 'installer.databaseUsername', '');
$this->readParam('databasePassword', 'installer.databasePassword', '');
$this->readParam('databaseName', 'installer.databaseName');
// Miscellaneous Settings
$this->printTitle('installer.miscSettings');
$this->readParam('oaiRepositoryId', 'installer.oaiRepositoryId');
$this->readParamBoolean('enableBeacon', 'installer.beacon.enable', 'Y');
printf("\n*** ");
}
/**
* Print input section title.
*
* @param string $title
*/
public function printTitle($title)
{
printf("\n%s\n%s\n%s\n", str_repeat('-', 80), __($title), str_repeat('-', 80));
}
/**
* Read a line of user input.
*
* @return string
*/
public function readInput()
{
$value = trim(fgets(STDIN));
if ($value === false || feof(STDIN)) {
printf("\n");
exit(0);
}
return $value;
}
/**
* Read a string parameter.
*
* @param string $name
* @param string $prompt
* @param string $defaultValue
*/
public function readParam($name, $prompt, $defaultValue = null)
{
do {
if (isset($defaultValue)) {
printf('%s (%s): ', __($prompt), $defaultValue !== '' ? $defaultValue : __('common.none'));
} else {
printf('%s: ', __($prompt));
}
$value = $this->readInput();
if ($value === '' && isset($defaultValue)) {
$value = $defaultValue;
}
} while ($value === '' && $defaultValue !== '');
$this->params[$name] = $value;
}
/**
* Prompt user for yes/no input.
*
* @param string $name
* @param string $prompt
* @param string $default default value, 'Y' or 'N'
*/
public function readParamBoolean($name, $prompt, $default = 'N')
{
if ($default == 'N') {
printf('%s [y/N] ', __($prompt));
$value = $this->readInput();
$this->params[$name] = (int)(strtolower(substr(trim($value), 0, 1)) == 'y');
} else {
printf('%s [Y/n] ', __($prompt));
$value = $this->readInput();
$this->params[$name] = (int)(strtolower(substr(trim($value), 0, 1)) != 'n');
}
}
/**
* Read a parameter from a set of options.
*
* @param string $name
* @param string $prompt
* @param array $options
* @param null|mixed $defaultValue
*/
public function readParamOptions($name, $prompt, $options, $defaultValue = null, $allowMultiple = false)
{
do {
printf("%s\n", __($prompt));
foreach ($options as $k => $v) {
printf(" %-10s %s\n", '[' . $k . ']', $v);
}
if ($allowMultiple) {
printf(" (%s)\n", __('installer.form.separateMultiple'));
}
if (isset($defaultValue)) {
printf('%s (%s): ', __('common.select'), $defaultValue !== '' ? $defaultValue : __('common.none'));
} else {
printf('%s: ', __('common.select'));
}
$value = $this->readInput();
if ($value === '' && isset($defaultValue)) {
$value = $defaultValue;
}
$values = [];
if ($value !== '') {
if ($allowMultiple) {
$values = ($value === '' ? [] : preg_split('/\s*,\s*/', $value));
} else {
$values = [$value];
}
foreach ($values as $k) {
if (!isset($options[$k])) {
$value = '';
break;
}
}
}
} while ($value === '' && $defaultValue !== '');
if ($allowMultiple) {
$this->params[$name] = $values;
} else {
$this->params[$name] = $value;
}
}
/**
* Log install message to stdout.
*
* @param string $message
*/
public function log($message)
{
printf("[%s]\n", $message);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\cliTool\InstallTool', '\InstallTool');
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* @file lib/pkp/classes/cliTool/MergeUsersTool.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2003-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class mergeUsers
*
* @ingroup tools
*
* @brief CLI tool for merging two user accounts.
*/
namespace PKP\cliTool;
use APP\facades\Repo;
class MergeUsersTool extends \PKP\cliTool\CommandLineTool
{
/** @var string $targetSpecifier */
public $targetSpecifier;
/** @var array $mergeSpecifier */
public $mergeSpecifiers;
/**
* Constructor.
*
* @param array $argv command-line arguments
*/
public function __construct($argv = [])
{
parent::__construct($argv);
if (!isset($this->argv[0]) || !isset($this->argv[1])) {
$this->usage();
exit(1);
}
$this->targetSpecifier = $this->argv[0];
$this->mergeSpecifiers = array_slice($this->argv, 1);
}
/**
* Print command usage information.
*/
public function usage()
{
echo "Merge users tool\n"
. "Use this tool to merge two or more user accounts.\n\n"
. "Usage: {$this->scriptName} targetUsername mergeUsername1 [mergeUsername2] [...]\n"
. "targetUsername: The target username for assets to be transferred to.\n"
. "mergeUsername1: The username for the account to be merged. All assets (e.g.\n"
. " submissions) associated with this user account will be\n"
. " transferred to the user account that corresponds to\n"
. " targetUsername. The user account that corresponds\n"
. " to mergeUsername1 will be deleted.\n\n"
. "Multiple users to merge can be specified in the same command, e.g.:\n\n"
. "{$this->scriptName} myUsername spamUser1 spamUser2 spamUser3\n\n"
. "This will merge users with username \"spamUser1\", \"spamUser2\", and\n"
. "\"spamUser3\" into the account with username \"myUsername\".\n\n"
. "Users can be specified by ID by entering usernames of the form \"id=x\"\n"
. "with the user ID in place of \"x\", e.g.:\n\n"
. "{$this->scriptName} myUsername id=234 id=456\n\n"
. "Usernames and IDs may be mixed as desired.\n";
}
/**
* Execute the merge users command.
*/
public function execute()
{
$targetUser = $this->_getUserBySpecifier($this->targetSpecifier);
if (!$targetUser) {
echo "Error: \"{$this->targetSpecifier}\" does not specify a valid user.\n";
exit(1);
}
// Build a list of usernames and IDs, checking for missing users before doing anything.
$mergeArray = [];
foreach ($this->mergeSpecifiers as $specifier) {
$mergeUser = $this->_getUserBySpecifier($specifier);
if (!$mergeUser) {
echo "Error: \"{$specifier}\" does not specify a valid user.\n";
exit(2);
}
if ($mergeUser->getId() == $targetUser->getId()) {
echo "Error: Cannot merge an account into itself.\n";
exit(3);
}
$mergeArray[$mergeUser->getId()] = $mergeUser->getUsername();
}
// Merge the accounts.
foreach ($mergeArray as $userId => $username) {
Repo::user()->mergeUsers((int) $userId, $targetUser->getId());
}
if (count($mergeArray) == 1) {
echo "Merge completed: \"{$username}\" merged into \"" . $targetUser->getUsername() . "\".\n";
} else {
echo 'Merge completed: ' . count($mergeArray) . ' users merged into "' . $targetUser->getUsername() . "\".\n";
}
}
/**
* Get a username by specifier, i.e. username or id=xyz.
*
* @param string $specifier The specifier
*
* @return \PKP\user\User|null
*/
protected function _getUserBySpecifier($specifier)
{
if (substr($specifier, 0, 3) == 'id=') {
$userId = substr($specifier, 3);
if (!ctype_digit($userId)) {
return null;
}
return Repo::user()->get((int) $userId, true);
}
return Repo::user()->getByUsername($specifier, true);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\cliTool\MergeUsersTool', '\MergeUsersTool');
}
@@ -0,0 +1,140 @@
<?php
/**
* @file classes/cliTool/ScheduledTaskTool.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ScheduledTaskTool
*
* @ingroup tools
*
* @brief CLI tool to execute a set of scheduled tasks.
*/
namespace PKP\cliTool;
use PKP\db\DAORegistry;
use PKP\config\Config;
use PKP\scheduledTask\ScheduledTaskDAO;
use PKP\scheduledTask\ScheduledTaskHelper;
use PKP\xml\PKPXMLParser;
/** Default XML tasks file to parse if none is specified */
define('TASKS_REGISTRY_FILE', 'registry/scheduledTasks.xml');
class ScheduledTaskTool extends \PKP\cliTool\CommandLineTool
{
/** @var string the XML file listing the tasks to be executed */
public $file;
/** @var ScheduledTaskDAO the DAO object */
public $taskDao;
/**
* Constructor.
*
* @param array $argv command-line arguments
* If specified, the first parameter should be the path to
* a tasks XML descriptor file (other than the default)
*/
public function __construct($argv = [])
{
parent::__construct($argv);
if (isset($this->argv[0])) {
$this->file = $this->argv[0];
} else {
$this->file = TASKS_REGISTRY_FILE;
}
if (!file_exists($this->file) || !is_readable($this->file)) {
printf("Tasks file \"%s\" does not exist or is not readable!\n", $this->file);
exit(1);
}
$this->taskDao = DAORegistry::getDAO('ScheduledTaskDAO');
}
/**
* Print command usage information.
*/
public function usage()
{
echo "Script to run a set of scheduled tasks\n"
. "Usage: {$this->scriptName} [tasks_file]\n";
}
/**
* Parse and execute the scheduled tasks.
*/
public function execute()
{
// Application is set to sandbox mode and will not run any schedule tasks
if (Config::getVar('general', 'sandbox', false)) {
error_log('Application is set to sandbox mode and will not run any schedule tasks');
return;
}
$this->parseTasks($this->file);
}
/**
* Parse and execute the scheduled tasks in the specified file.
*
* @param string $file
*/
public function parseTasks($file)
{
$xmlParser = new PKPXMLParser();
$tree = $xmlParser->parse($file);
if (!$tree) {
printf("Unable to parse file \"%s\"!\n", $file);
exit(1);
}
foreach ($tree->getChildren() as $task) {
$className = $task->getAttribute('class');
$frequency = $task->getChildByName('frequency');
if (isset($frequency)) {
$canExecute = ScheduledTaskHelper::checkFrequency($className, $frequency);
} else {
// Always execute if no frequency is specified
$canExecute = true;
}
if ($canExecute) {
$this->executeTask($className, ScheduledTaskHelper::getTaskArgs($task));
}
}
}
/**
* Execute the specified task.
*
* @param string $className the class name to execute
* @param array $args the array of arguments to pass to the class constructors
*/
public function executeTask($className, $args)
{
// Load and execute the task
if (preg_match('/^[a-zA-Z0-9_.]+$/', $className)) {
// DEPRECATED as of 3.4.0: Use old class.name.style and import() function (pre-PSR classloading) pkp/pkp-lib#8186
if (!is_object($task = instantiate($className, null, null, 'execute', $args))) {
fatalError('Cannot instantiate task class.');
}
} else {
$task = new $className($args);
}
$this->taskDao->updateLastRunTime($className);
$task->execute();
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\cliTool\ScheduledTaskTool', '\ScheduledTaskTool');
}
+237
View File
@@ -0,0 +1,237 @@
<?php
/**
* @file classes/cliTool/UpgradeTool.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class upgradeTool
*
* @ingroup tools
*
* @brief CLI tool for upgrading the system.
*
* Note: Some functions require fopen wrappers to be enabled.
*/
namespace PKP\cliTool;
use APP\core\Application;
use APP\install\Upgrade;
use PKP\site\VersionCheck;
Application::upgrade();
class UpgradeTool extends \PKP\cliTool\CommandLineTool
{
/** @var string command to execute (check|upgrade|download) */
public $command;
/**
* Constructor.
*
* @param array $argv command-line arguments
*/
public function __construct($argv = [])
{
parent::__construct($argv);
if (!isset($this->argv[0]) || !in_array($this->argv[0], ['check', 'latest', 'upgrade', 'download'])) {
$this->usage();
exit(1);
}
$this->command = $this->argv[0];
}
/**
* Print command usage information.
*/
public function usage()
{
echo "Upgrade tool\n"
. "Usage: {$this->scriptName} command\n"
. "Supported commands:\n"
. " check perform version check\n"
. " latest display latest version info\n"
. " upgrade execute upgrade script\n"
. " download download latest version (does not unpack/install)\n";
}
/**
* Execute the specified command.
*/
public function execute()
{
$command = $this->command;
$this->$command();
}
/**
* Perform version check against latest available version.
*/
public function check()
{
$this->checkVersion(VersionCheck::getLatestVersion());
}
/**
* Print information about the latest available version.
*/
public function latest()
{
$this->checkVersion(VersionCheck::getLatestVersion(), true);
}
/**
* Run upgrade script.
*/
public function upgrade()
{
$installer = new Upgrade([]);
$installer->setLogger($this);
if ($installer->execute()) {
if (count($installer->getNotes()) > 0) {
printf("\nRelease Notes\n");
printf("----------------------------------------\n");
foreach ($installer->getNotes() as $note) {
printf("%s\n\n", $note);
}
}
$newVersion = $installer->getNewVersion();
printf("Successfully upgraded to version %s\n", $newVersion->getVersionString(false));
} else {
printf("ERROR: Upgrade failed: %s\n", $installer->getErrorString());
exit(2);
}
}
/**
* Download latest package.
*/
public function download()
{
$versionInfo = VersionCheck::getLatestVersion();
if (!$versionInfo) {
$application = Application::get();
printf("Failed to load version info from %s\n", $application->getVersionDescriptorUrl());
exit(3);
}
$download = $versionInfo['package'];
$outFile = basename($download);
printf("Download: %s\n", $download);
printf("File will be saved to: %s\n", $outFile);
if (!$this->promptContinue()) {
exit(0);
}
$out = fopen($outFile, 'wb');
if (!$out) {
printf("Failed to open %s for writing\n", $outFile);
exit(5);
}
$in = fopen($download, 'rb');
if (!$in) {
printf("Failed to open %s for reading\n", $download);
fclose($out);
exit(6);
}
printf('Downloading file...');
while (($data = fread($in, 4096)) !== '') {
printf('.');
fwrite($out, $data);
}
printf("done\n");
fclose($in);
fclose($out);
}
/**
* Perform version check.
*
* @param array $versionInfo latest version info
* @param bool $displayInfo just display info, don't perform check
*/
public function checkVersion($versionInfo, $displayInfo = false)
{
if (!$versionInfo) {
$application = Application::get();
printf("Failed to load version info from %s\n", $application->getVersionDescriptorUrl());
exit(7);
}
$dbVersion = VersionCheck::getCurrentDBVersion();
$codeVersion = VersionCheck::getCurrentCodeVersion();
$latestVersion = $versionInfo['version'];
printf("Code version: %s\n", $codeVersion->getVersionString(false));
printf("Database version: %s\n", $dbVersion->getVersionString(false));
printf("Latest version: %s\n", $latestVersion->getVersionString(false));
$compare1 = $codeVersion->compare($latestVersion);
$compare2 = $dbVersion->compare($codeVersion);
if (!$displayInfo) {
if ($compare2 < 0) {
printf("Database version is older than code version\n");
printf("Run \"{$this->scriptName} upgrade\" to update\n");
} elseif ($compare2 > 0) {
printf("Database version is newer than code version!\n");
} elseif ($compare1 == 0) {
printf("Your system is up-to-date\n");
} elseif ($compare1 < 0) {
printf("A newer version is available:\n");
$displayInfo = true;
} else {
printf("Current version is newer than latest!\n");
}
}
if ($displayInfo) {
printf(" tag: %s\n", $versionInfo['tag']);
printf(" date: %s\n", $versionInfo['date']);
printf(" info: %s\n", $versionInfo['info']);
printf(" package: %s\n", $versionInfo['package']);
}
return $compare1;
}
/**
* Prompt user for yes/no input (default no).
*
* @param string $prompt
*/
public function promptContinue($prompt = 'Continue?')
{
printf('%s [y/N] ', $prompt);
$continue = fread(STDIN, 255);
return (strtolower(substr(trim($continue), 0, 1)) == 'y');
}
/**
* Log install message to stdout.
*
* @param string $message
*/
public function log($message)
{
printf("%s [%s]\n", date('Y-m-d H:i:s'), $message);
}
}
if (!PKP_STRICT_MODE) {
class_alias('\PKP\cliTool\UpgradeTool', '\UpgradeTool');
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
<?php
/**
* @file components/PKPStatsComponent.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsComponent
*
* @ingroup classes_components_stats
*
* @brief A class to prepare the data object for a statistics UI component
*/
namespace PKP\components;
use PKP\statistics\PKPStatisticsHelper;
class PKPStatsComponent
{
/** @var string The URL to the /stats API endpoint */
public $apiUrl = '';
/** @var array Configuration for the columns to display in the table */
public $tableColumns = [];
/** @var string Retrieve stats after this date */
public $dateStart = '';
/** @var string Retrieve stats before this date */
public $dateEnd = '';
/** @var array Quick options to provide for configuring the date range */
public $dateRangeOptions = [];
/** @var array|null Configuration assoc array for available filters */
public $filters = null;
/**
* Constructor
*
* @param string $apiUrl The URL to fetch stats from
* @param array $args Optional arguments
*/
public function __construct($apiUrl, $args = [])
{
$this->apiUrl = $apiUrl;
$this->init($args);
}
/**
* Initialize the handler with config parameters
*
* @param array $args Configuration params
*/
public function init($args = [])
{
foreach ($args as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}
}
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
$config = [
'apiUrl' => $this->apiUrl,
'tableColumns' => $this->tableColumns,
'dateStart' => $this->dateStart,
'dateStartMin' => PKPStatisticsHelper::STATISTICS_EARLIEST_DATE,
'dateEnd' => $this->dateEnd,
'dateEndMax' => date('Y-m-d', strtotime('yesterday')),
'dateRangeOptions' => $this->dateRangeOptions,
'activeFilters' => [],
'isLoadingItems' => false,
'isSidebarVisible' => false,
];
if ($this->filters) {
$config['filters'] = $this->filters;
}
return $config;
}
}
@@ -0,0 +1,60 @@
<?php
/**
* @file components/PKPStatsContextPage.php
*
* Copyright (c) 2022 Simon Fraser University
* Copyright (c) 2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsContextPage
*
* @ingroup classes_controllers_stats
*
* @brief A class to prepare the data object for the context statistics
* UI component
*/
namespace PKP\components;
use PKP\statistics\PKPStatisticsHelper;
class PKPStatsContextPage extends PKPStatsComponent
{
/** @var array A timeline of stats (eg - monthly) for a graph */
public $timeline = [];
/** @var string Which time segment (eg - month) is displayed in the graph */
public $timelineInterval = PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH;
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
$config = parent::getConfig();
$config = array_merge(
$config,
[
'timeline' => $this->timeline,
'timelineInterval' => $this->timelineInterval,
'dateRangeLabel' => __('stats.dateRange'),
'betweenDatesLabel' => __('stats.downloadReport.betweenDates'),
'allDatesLabel' => __('stats.dateRange.allDates'),
'contextLabel' => __('context.context'),
'timelineTypeLabel' => __('stats.timelineType'),
'timelineIntervalLabel' => __('stats.timelineInterval'),
'viewsLabel' => __('submission.views'),
'dayLabel' => __('common.day'),
'monthLabel' => __('common.month'),
'timelineDescriptionLabel' => __('stats.timeline.downloadReport.description'),
'isLoadingTimeline' => false,
]
);
return $config;
}
}
@@ -0,0 +1,55 @@
<?php
/**
* @file components/PKPStatsEditorialPage.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsEditorialPage
*
* @ingroup classes_controllers_stats
*
* @brief A class to prepare the data object for the editorial statistics
* UI component
*/
namespace PKP\components;
class PKPStatsEditorialPage extends PKPStatsComponent
{
/** @var array A key/value array of active submissions by stage */
public $activeByStage = [];
/** @var string The URL to get the averages from the API */
public $averagesApiUrl = [];
/** @var array List of stats that should be converted to percentages */
public $percentageStats = [];
/** @var array List of stats details to display in the table */
public $tableRows = [];
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
$config = parent::getConfig();
$config = array_merge(
$config,
[
'activeByStage' => $this->activeByStage,
'averagesApiUrl' => $this->averagesApiUrl,
'percentageStats' => $this->percentageStats,
'tableRows' => $this->tableRows,
]
);
return $config;
}
}
@@ -0,0 +1,93 @@
<?php
/**
* @file components/PKPStatsPublicationPage.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPStatsPublicationPage
*
* @ingroup classes_controllers_stats
*
* @brief A class to prepare the data object for the publication statistics
* UI component
*/
namespace PKP\components;
use PKP\statistics\PKPStatisticsHelper;
class PKPStatsPublicationPage extends PKPStatsComponent
{
/** @var array A timeline of stats (eg - monthly) for a graph */
public $timeline = [];
/** @var string Which time segment (eg - month) is displayed in the graph */
public $timelineInterval = PKPStatisticsHelper::STATISTICS_DIMENSION_MONTH;
/** @var string Which views to show in the graph. Supports `abstract` or `galley`. */
public $timelineType = '';
/** @var array List of items to display stats for */
public $items = [];
/** @var int The maximum number of items that stats can be shown for */
public $itemsMax = 0;
/** @var int How many items to show per page */
public $count = 30;
/** @var string Order items by this property */
public $orderBy = '';
/** @var string Order items in this direction: ASC or DESC*/
public $orderDirection = 'DESC';
/** @var string A search phrase to filter the list of items */
public $searchPhrase = null;
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
$config = parent::getConfig();
$config = array_merge(
$config,
[
'timeline' => $this->timeline,
'timelineInterval' => $this->timelineInterval,
'timelineType' => $this->timelineType,
'items' => $this->items,
'dateRangeLabel' => __('stats.dateRange'),
'searchPhraseLabel' => __('common.searchPhrase'),
'itemsOfTotalLabel' => __('stats.publications.countOfTotal'),
'betweenDatesLabel' => __('stats.downloadReport.betweenDates'),
'allDatesLabel' => __('stats.dateRange.allDates'),
'allFiltersLabel' => __('stats.downloadReport.allFilters'),
'commonSubmissionsLabel' => __('common.publications'),
'timelineTypeLabel' => __('stats.timelineType'),
'timelineIntervalLabel' => __('stats.timelineInterval'),
'viewsLabel' => __('submission.views'),
'downloadsLabel' => __('submission.downloads'),
'dayLabel' => __('common.day'),
'monthLabel' => __('common.month'),
'timelineDescriptionLabel' => __('stats.timeline.downloadReport.description'),
'itemsMax' => $this->itemsMax,
'count' => $this->count,
'offset' => 0,
'searchPhrase' => null,
'orderBy' => $this->orderBy,
'orderDirection' => $this->orderDirection,
'isLoadingTimeline' => false,
]
);
return $config;
}
}
@@ -0,0 +1,51 @@
<?php
/**
* @file classes/components/fileAttachers/Base.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Base
*
* @ingroup classes_controllers_form
*
* @brief A base class for FileAttacher components.
*/
namespace PKP\components\fileAttachers;
abstract class BaseAttacher
{
public string $component;
public string $label;
public string $description;
public string $button;
/**
* Initialize the file attacher
*
* @param string $label The label to display for this file attacher
* @param string $description A description of this file attacher
* @param string $button The label for the button to activate this file attacher
*/
public function __construct(string $label, string $description, string $button)
{
$this->label = $label;
$this->description = $description;
$this->button = $button;
}
/**
* Compile the initial state for this file attacher
*/
public function getState(): array
{
return [
'component' => $this->component,
'label' => $this->label,
'description' => $this->description,
'button' => $this->button,
];
}
}
@@ -0,0 +1,82 @@
<?php
/**
* @file classes/components/fileAttachers/FileStage.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FileStage
*
* @ingroup classes_controllers_form
*
* @brief A class to compile initial state for a FileAttacherFileStage component.
*/
namespace PKP\components\fileAttachers;
use APP\core\Application;
use APP\submission\Submission;
use PKP\context\Context;
use PKP\submission\reviewRound\ReviewRound;
class FileStage extends BaseAttacher
{
public string $component = 'FileAttacherFileStage';
public Context $context;
public Submission $submission;
public array $fileStages = [];
/**
* Initialize a file stage attacher
*
* @param string $label The label to display for this file attacher
* @param string $description A description of this file attacher
* @param string $button The label for the button to activate this file attacher
*/
public function __construct(Context $context, Submission $submission, string $label, string $description, string $button)
{
parent::__construct($label, $description, $button);
$this->context = $context;
$this->submission = $submission;
}
/**
* Add a submission file stage that can be used for attachments
*/
public function withFileStage(int $fileStage, string $label, ?ReviewRound $reviewRound = null): self
{
$queryParams = ['fileStages' => [$fileStage]];
if ($reviewRound) {
$queryParams['reviewRoundIds'] = [$reviewRound->getId()];
}
$this->fileStages[] = [
'label' => $label,
'queryParams' => $queryParams,
];
return $this;
}
/**
* Compile the props for this file attacher
*/
public function getState(): array
{
$props = parent::getState();
$request = Application::get()->getRequest();
$props['submissionFilesApiUrl'] = $request->getDispatcher()->url(
$request,
Application::ROUTE_API,
$this->context->getData('urlPath'),
'submissions/' . $this->submission->getId() . '/files'
);
$props['fileStages'] = $this->fileStages;
$props['attachSelectedLabel'] = __('common.attachSelected');
$props['downloadLabel'] = __('common.download');
$props['backLabel'] = __('common.back');
return $props;
}
}
@@ -0,0 +1,66 @@
<?php
/**
* @file classes/components/fileAttachers/Library.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Library
*
* @ingroup classes_controllers_form
*
* @brief A class to compile initial state for a FileAttacherLibrary component.
*/
namespace PKP\components\fileAttachers;
use APP\core\Application;
use APP\submission\Submission;
use PKP\context\Context;
class Library extends BaseAttacher
{
public string $component = 'FileAttacherLibrary';
public Context $context;
public Submission $submission;
/**
* Initialize this file attacher
*
*/
public function __construct(Context $context, ?Submission $submission = null)
{
parent::__construct(
__('email.addAttachment.libraryFiles'),
__('email.addAttachment.libraryFiles.description'),
__('email.addAttachment.libraryFiles.attach')
);
$this->context = $context;
$this->submission = $submission;
}
/**
* Compile the props for this file attacher
*/
public function getState(): array
{
$props = parent::getState();
$request = Application::get()->getRequest();
$props['libraryApiUrl'] = $request->getDispatcher()->url(
$request,
Application::ROUTE_API,
$this->context->getData('urlPath'),
'_library'
);
if ($this->submission) {
$props['includeSubmissionId'] = $this->submission->getId();
}
$props['attachSelectedLabel'] = __('common.attachSelected');
$props['backLabel'] = __('common.back');
$props['downloadLabel'] = __('common.download');
return $props;
}
}
@@ -0,0 +1,99 @@
<?php
/**
* @file classes/components/fileAttachers/ReviewFiles.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class ReviewFiles
*
* @ingroup classes_controllers_form
*
* @brief A class to compile initial state for a FileAttacherReviewFiles component.
*/
namespace PKP\components\fileAttachers;
use APP\core\Application;
use APP\core\Services;
use APP\facades\Repo;
use Exception;
use PKP\context\Context;
use PKP\submission\reviewAssignment\ReviewAssignment;
use PKP\submissionFile\SubmissionFile;
class ReviewFiles extends BaseAttacher
{
public string $component = 'FileAttacherReviewFiles';
public Context $context;
/** @var iterable<SubmissionFile> $files */
public iterable $files;
/** @var array<ReviewAssignment> $reviewAssignments */
public array $reviewAssignments;
/**
* Initialize this file attacher
*
* @param string $label The label to display for this file attacher
* @param string $description A description of this file attacher
* @param string $button The label for the button to activate this file attacher
*/
public function __construct(string $label, string $description, string $button, iterable $files, array $reviewAssignments, Context $context)
{
parent::__construct($label, $description, $button);
$this->files = $files;
$this->reviewAssignments = $reviewAssignments;
$this->context = $context;
}
/**
* Compile the props for this file attacher
*/
public function getState(): array
{
$props = parent::getState();
$props['attachSelectedLabel'] = __('common.attachSelected');
$props['backLabel'] = __('common.back');
$props['downloadLabel'] = __('common.download');
$props['files'] = $this->getFilesState();
return $props;
}
protected function getFilesState(): array
{
$request = Application::get()->getRequest();
$files = [];
/** @var SubmissionFile $file */
foreach ($this->files as $file) {
if (!isset($this->reviewAssignments[$file->getData('assocId')])) {
throw new Exception('Tried to add review file attachment from unknown review assignment.');
}
$files[] = [
'id' => $file->getId(),
'name' => $file->getData('name'),
'documentType' => Services::get('file')->getDocumentType($file->getData('documentType')),
'reviewerName' => $this->reviewAssignments[$file->getData('assocId')]->getReviewerFullName(),
'url' => $request->getDispatcher()->url(
$request,
Application::ROUTE_COMPONENT,
$this->context->getData('urlPath'),
'api.file.FileApiHandler',
'downloadFile',
null,
[
'submissionFileId' => $file->getId(),
'submissionId' => $file->getData('submissionId'),
'stageId' => Repo::submissionFile()->getWorkflowStageId($file),
]
),
];
}
return $files;
}
}
@@ -0,0 +1,77 @@
<?php
/**
* @file classes/components/fileAttachers/Upload.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Upload
*
* @ingroup classes_controllers_form
*
* @brief A class to compile initial state for a FileAttacherUpload component.
*/
namespace PKP\components\fileAttachers;
use APP\core\Application;
use PKP\context\Context;
class Upload extends BaseAttacher
{
public string $component = 'FileAttacherUpload';
public Context $context;
/**
* Initialize this file attacher
*
* @param string $label The label to display for this file attacher
* @param string $description A description of this file attacher
* @param string $button The label for the button to activate this file attacher
*/
public function __construct(Context $context, string $label, string $description, string $button)
{
parent::__construct($label, $description, $button);
$this->context = $context;
}
/**
* Compile the props for this file attacher
*/
public function getState(): array
{
$props = parent::getState();
$request = Application::get()->getRequest();
$props['temporaryFilesApiUrl'] = $request->getDispatcher()->url(
$request,
Application::ROUTE_API,
$this->context->getData('urlPath'),
'temporaryFiles'
);
$props['dropzoneOptions'] = [
'maxFilesize' => Application::getIntMaxFileMBs(),
'timeout' => ini_get('max_execution_time') ? ini_get('max_execution_time') * 1000 : 0,
'dropzoneDictDefaultMessage' => __('form.dropzone.dictDefaultMessage'),
'dropzoneDictFallbackMessage' => __('form.dropzone.dictFallbackMessage'),
'dropzoneDictFallbackText' => __('form.dropzone.dictFallbackText'),
'dropzoneDictFileTooBig' => __('form.dropzone.dictFileTooBig'),
'dropzoneDictInvalidFileType' => __('form.dropzone.dictInvalidFileType'),
'dropzoneDictResponseError' => __('form.dropzone.dictResponseError'),
'dropzoneDictCancelUpload' => __('form.dropzone.dictCancelUpload'),
'dropzoneDictUploadCanceled' => __('form.dropzone.dictUploadCanceled'),
'dropzoneDictCancelUploadConfirmation' => __('form.dropzone.dictCancelUploadConfirmation'),
'dropzoneDictRemoveFile' => __('form.dropzone.dictRemoveFile'),
'dropzoneDictMaxFilesExceeded' => __('form.dropzone.dictMaxFilesExceeded'),
];
$props['addFilesLabel'] = __('common.addFiles');
$props['attachFilesLabel'] = __('common.attachFiles');
$props['dragAndDropMessage'] = __('common.dragAndDropHere');
$props['dragAndDropOrUploadMessage'] = __('common.orUploadFile');
$props['backLabel'] = __('common.back');
$props['removeItemLabel'] = __('common.removeItem');
return $props;
}
}
+169
View File
@@ -0,0 +1,169 @@
<?php
/**
* @file classes/components/form/Field.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class Field
*
* @ingroup classes_controllers_form
*
* @brief A base class representing a single field in a form.
*/
namespace PKP\components\forms;
abstract class Field
{
/** @var string Which UI Library component this field represents */
public $component;
/** @var string The form input name for this field */
public $name;
/** @var string|object Field label or multilingual object matching locales to labels, eg ['en' => 'Label', 'fr_CA' => 'Étiquette'] */
public $label = '';
/** @var string Field description */
public $description;
/** @var string Field tooltip */
public $tooltip;
/** @var string Field help topic. Refers to the /dev/docs file name without .md */
public $helpTopic;
/** @var string Field help section. An optional anchor link to open to when loading the helpTopic. */
public $helpSection;
/** @var string Which group should this field be placed in? */
public $groupId;
/** @var bool Is this field required? */
public $isRequired = false;
/** @var bool Is this field multilingual? */
public $isMultilingual = false;
/** @var mixed The value of this field. If multilingual, expects a key/value array: ['en', => 'English value', 'fr_CA' => 'French value'] */
public $value;
/** @var mixed A default for this field when no value is specified. */
public $default;
/**
* Only show this field when the field named here is not empty. Match an exact
* value by passing an array:
*
* $this->showWhen = ['fieldName', 'expectedValue'];
*
* @var string|array
*/
public $showWhen;
/** @var array List of required properties for this field. */
private $_requiredProperties = ['name', 'component'];
/**
* Initialize the form field
*
* @param string $name
* @param array $args [
*
* @option label string|object
* @option groupId string
* @option isRequired boolean
* @option isMultilingual boolean
* ]
*/
public function __construct($name, $args = [])
{
$this->name = $name;
foreach ($args as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}
}
/**
* Get a configuration object representing this field to be passed to the UI
* Library
*
* @return array
*/
public function getConfig()
{
if (!$this->validate()) {
throw new \Exception('Form field configuration did not pass validation: ' . print_r($this, true));
}
$config = [
'name' => $this->name,
'component' => $this->component,
'label' => $this->label,
];
if (isset($this->description)) {
$config['description'] = $this->description;
}
if (isset($this->tooltip)) {
$config['tooltip'] = $this->tooltip;
}
if (isset($this->helpTopic)) {
$config['helpTopic'] = $this->helpTopic;
if ($this->helpSection) {
$config['helpSection'] = $this->helpSection;
}
}
if (isset($this->groupId)) {
$config['groupId'] = $this->groupId;
}
if (isset($this->isRequired)) {
$config['isRequired'] = $this->isRequired;
}
if (isset($this->isMultilingual)) {
$config['isMultilingual'] = $this->isMultilingual;
}
if (isset($this->showWhen)) {
$config['showWhen'] = $this->showWhen;
}
$config['value'] = $this->value ?? $this->default ?? null;
return $config;
}
/**
* Validate the field configuration
*
* Check that no required fields are missing
*
* @return bool
*/
public function validate()
{
foreach ($this->_requiredProperties as $property) {
if (!isset($this->{$property})) {
return false;
}
}
return true;
}
/**
* Get a default empty value for this field type
*
* The UI Library expects to receive a value property for each field. If it's
* a multilingual field, it expects the value property to contain keys for
* each locale in the form.
*
* This function will provide a default empty value so that a form can fill
* in the empty values automatically.
*
*/
public function getEmptyValue()
{
return '';
}
}
@@ -0,0 +1,81 @@
<?php
/**
* @file classes/components/form/FieldControlledVocab.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldAutosuggestPreset
*
* @ingroup classes_controllers_form
*
* @brief A type of autosuggest field that preloads all of its options.
*/
namespace PKP\components\forms;
class FieldAutosuggestPreset extends FieldBaseAutosuggest
{
/** @copydoc Field::$component */
public $component = 'field-autosuggest-preset';
/** @var array Key/value list of suggestions for this field */
public $options = [];
/** @var array Key/value list of languages this field should support. Key = locale code. Value = locale name */
public $locales = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['options'] = $this->options;
$config['selected'] = $this->getSelected();
return $config;
}
/**
* @copydoc Field::getConfig()
*/
protected function getSelected(): array
{
if ($this->isMultilingual) {
$selected = [];
foreach ($this->locales as $locale) {
if (array_key_exists($locale['key'], $this->value)) {
$config['selected'][$locale['key']] = array_map([$this, 'mapSelected'], (array) $this->value[$locale['key']]);
} else {
$config['selected'][$locale['key']] = [];
}
}
return $selected;
}
return array_map([$this, 'mapSelected'], $this->value);
}
/**
* Map the selected values to the format expected by an
* autosuggest field
*
* @param string $value
*
* @return array
*/
protected function mapSelected($value)
{
foreach ($this->options as $option) {
if ($option['value'] === $value) {
return $option;
}
}
return [
'value' => $value,
'label' => $value,
];
}
}
@@ -0,0 +1,49 @@
<?php
/**
* @file classes/components/form/FieldBaseAutosuggest.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldBaseAutosuggest
*
* @ingroup classes_controllers_form
*
* @brief A base class for text fields that provide suggested values while typing.
*/
namespace PKP\components\forms;
define('AUTOSUGGEST_POSITION_INLINE', 'inline');
define('AUTOSUGGEST_POSITION_BELOW', 'below');
abstract class FieldBaseAutosuggest extends Field
{
/** @copydoc Field::$component */
public $component = 'field-base-autosuggest';
/** @var string A URL to retrieve suggestions. */
public $apiUrl;
/** @var array Query params when getting suggestions. */
public $getParams = [];
/** @var array List of selected items. */
public $selected = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['apiUrl'] = $this->apiUrl;
$config['deselectLabel'] = __('common.removeItem');
$config['getParams'] = empty($this->getParams) ? new \stdClass() : $this->getParams;
$config['selectedLabel'] = __('common.selectedPrefix');
$config['selected'] = $this->selected;
return $config;
}
}
@@ -0,0 +1,22 @@
<?php
/**
* @file classes/components/form/FieldColor.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldColor
*
* @ingroup classes_controllers_form
*
* @brief A color picker field in a form.
*/
namespace PKP\components\forms;
class FieldColor extends Field
{
/** @copydoc Field::$component */
public $component = 'field-color';
}
@@ -0,0 +1,64 @@
<?php
/**
* @file classes/components/form/FieldControlledVocab.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldControlledVocab
*
* @ingroup classes_controllers_form
*
* @brief A type of autosuggest field for controlled vocabulary like keywords.
*/
namespace PKP\components\forms;
class FieldControlledVocab extends FieldBaseAutosuggest
{
/** @copydoc Field::$component */
public $component = 'field-controlled-vocab';
/** @var array Key/value list of languages this field should support. Key = locale code. Value = locale name */
public $locales = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
if ($this->isMultilingual) {
$config['selected'] = [];
foreach ($this->locales as $locale) {
if (array_key_exists($locale['key'], $this->value)) {
$config['selected'][$locale['key']] = array_map([$this, 'mapSelected'], (array) $this->value[$locale['key']]);
} else {
$config['selected'][$locale['key']] = [];
}
}
} else {
$config['selected'] = array_map([$this, 'mapSelected'], $this->value);
}
return $config;
}
/**
* Map the selected values to the format expected by an
* autosuggest field
*
* @param string $value
*
* @return array
*/
public function mapSelected($value)
{
return [
'value' => $value,
'label' => $value,
];
}
}
@@ -0,0 +1,23 @@
<?php
/**
* @file classes/components/form/FieldHTML.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldHTML
*
* @ingroup classes_controllers_form
*
* @brief A component for inserting HTML into a form, when you don't need any
* input fields or values stored.
*/
namespace PKP\components\forms;
class FieldHTML extends Field
{
/** @copydoc Field::$component */
public $component = 'field-html';
}
@@ -0,0 +1,50 @@
<?php
/**
* @file classes/components/form/FieldMetadataSetting.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldMetadataSetting
*
* @ingroup classes_controllers_form
*
* @brief A field to enable a type of metadata and determine when it should be
* requested or required.
*/
namespace PKP\components\forms;
use PKP\context\Context;
class FieldMetadataSetting extends FieldOptions
{
/** @copydoc Field::$component */
public $component = 'field-metadata-setting';
/** @var int What is the value that represents metadata that is disabled */
public $disabledValue = Context::METADATA_DISABLE;
/**
* @var int What is the value that represents metadata that is enabled,
* but which is not requested or required during submission?
*/
public $enabledOnlyValue = Context::METADATA_ENABLE;
/** @var array The options for what to request/require from the author during submission */
public $submissionOptions = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['disabledValue'] = $this->disabledValue;
$config['enabledOnlyValue'] = $this->enabledOnlyValue;
$config['submissionOptions'] = $this->submissionOptions;
return $config;
}
}
@@ -0,0 +1,44 @@
<?php
/**
* @file classes/components/form/FieldOptions.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldOptions
*
* @ingroup classes_controllers_form
*
* @brief A field to select from a set of checkbox or radio options.
*/
namespace PKP\components\forms;
class FieldOptions extends Field
{
/** @copydoc Field::$component */
public $component = 'field-options';
/** @var string Use a checkbox or radio button input type */
public $type = 'checkbox';
/** @var bool Should the user be able to re-order the options? */
public $isOrderable = false;
/** @var array The options which can be selected */
public $options = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['type'] = $this->type;
$config['isOrderable'] = $this->isOrderable;
$config['options'] = $this->options;
return $config;
}
}
@@ -0,0 +1,40 @@
<?php
/**
* @file classes/components/form/FieldPreparedContent.php
*
* Copyright (c) 2014-2022 Simon Fraser University
* Copyright (c) 2000-2022 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldPreparedContent
*
* @ingroup classes_controllers_form
*
* @brief A rich text editor that can insert prepared content snippets
*/
namespace PKP\components\forms;
class FieldPreparedContent extends FieldRichTextarea
{
public $component = 'field-prepared-content';
/**
* A list of content that can be inserted from a TinyMCE button.
*
* @see FieldPreparedContent in the UI Library for details on the expected format
*/
public array $preparedContent = [];
public function getConfig()
{
$config = parent::getConfig();
$config['preparedContentLabel'] = __('common.content');
$config['insertLabel'] = __('common.insert');
$config['insertModalLabel'] = __('common.insertContent');
$config['searchLabel'] = __('common.insertContentSearch');
$config['preparedContent'] = $this->preparedContent;
return $config;
}
}
@@ -0,0 +1,112 @@
<?php
/**
* @file classes/components/form/FieldPubId.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldPubId
*
* @ingroup classes_controllers_form
*
* @brief A field for generating a pub id, like a DOI.
*/
namespace PKP\components\forms;
class FieldPubId extends Field
{
/** @copydoc Field::$component */
public $component = 'field-pub-id';
/** @var string A localized label for the button to assign the pubid */
public $assignIdLabel;
/** @var string A localized label for the button to clear the pubid */
public $clearIdLabel;
/** @var string The journal/press initials to use when generating a pub id */
public $contextInitials;
/** @var bool If a %p in the pattern should stand for press (OMP). Otherwise it means pages (OJS). */
public $isPForPress = false;
/** @var string The issue number to use when generating a pub id */
public $issueNumber;
/** @var string The issue volume to use when generating a pub id */
public $issueVolume;
/** @var string A localized message when the pub id can not be generated due to missing information */
public $missingPartsLabel;
/** @var string The page numbers use when generating a pub id */
public $pages;
/** @var string The pattern to use when generating a pub id */
public $pattern;
/** @var string The pub id prefix for this context */
public $prefix;
/** @var string The publisher id to use when generating a pub id */
public $publisherId;
/** @var string Optional separator to add between prefix and suffix when generating pub id */
public $separator = '';
/** @var string The submission ID to use when generating a pub id */
public $submissionId;
/** @var string The publication ID to use when generating a pub id */
public $publicationId;
/** @var string The year of publication to use when generating a pub id */
public $year;
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['assignIdLabel'] = $this->assignIdLabel;
$config['clearIdLabel'] = $this->clearIdLabel;
$config['missingPartsLabel'] = $this->missingPartsLabel;
if (isset($this->contextInitials)) {
$config['contextInitials'] = $this->contextInitials;
}
if (isset($this->issueNumber)) {
$config['issueNumber'] = $this->issueNumber;
}
if (isset($this->issueVolume)) {
$config['issueVolume'] = $this->issueVolume;
}
if (isset($this->pages)) {
$config['pages'] = $this->pages;
}
if (isset($this->pattern)) {
$config['pattern'] = $this->pattern;
}
if (isset($this->prefix)) {
$config['prefix'] = $this->prefix;
}
if (isset($this->publisherId)) {
$config['publisherId'] = $this->publisherId;
}
if (isset($this->submissionId)) {
$config['submissionId'] = $this->submissionId;
}
if (isset($this->publicationId)) {
$config['publicationId'] = $this->publicationId;
}
if (isset($this->year)) {
$config['year'] = $this->year;
}
$config['isPForPress'] = $this->isPForPress;
$config['separator'] = $this->separator;
return $config;
}
}
@@ -0,0 +1,37 @@
<?php
/**
* @file classes/components/form/FieldRadioInput.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldRadioInput
*
* @ingroup classes_controllers_form
*
* @brief A field to select one of a set of options, and one option is a text
* field for entering a custom value.
*/
namespace PKP\components\forms;
class FieldRadioInput extends Field
{
/** @copydoc Field::$component */
public $component = 'field-radio-input';
/** @var array The options which can be selected */
public $options = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['options'] = $this->options;
return $config;
}
}
@@ -0,0 +1,54 @@
<?php
/**
* @file classes/components/form/FieldRichText.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldRichText
*
* @ingroup classes_controllers_form
*
* @brief A rich single line text editor field in a form.
*/
namespace PKP\components\forms;
class FieldRichText extends Field
{
/** @copydoc Field::$component */
public $component = 'field-rich-text';
/** @var array Optional. An assoc array of init properties to pass to TinyMCE */
public $init;
/** @var string Optional. A preset size option. */
public $size = 'oneline';
/** @var string Optional. A preset toolbar configuration. */
public $toolbar = 'formatgroup';
/** @var array Optional. A list of required plugins. */
public $plugins = 'paste';
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['i18nFormattingLabel'] = __('common.formatting');
$config['toolbar'] = $this->toolbar;
$config['plugins'] = $this->plugins;
$config['size'] = $this->size;
if (!empty($this->init)) {
$config['init'] = $this->init;
}
return $config;
}
}
@@ -0,0 +1,65 @@
<?php
/**
* @file classes/components/form/FieldRichTextarea.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldRichTextarea
*
* @ingroup classes_controllers_form
*
* @brief A rich text editor field in a form.
*/
namespace PKP\components\forms;
class FieldRichTextarea extends Field
{
/** @copydoc Field::$component */
public $component = 'field-rich-textarea';
/** @var array Optional. An assoc array of init properties to pass to TinyMCE */
public $init;
/** @var array Optional. A list of required plugins. */
public $plugins = 'paste,link,noneditable';
/** @var string Optional. A preset size option. */
public $size;
/** @var string Optional. A preset toolbar configuration. */
public $toolbar = 'bold italic superscript subscript | link';
/** @var string Optional. The API endpoint to upload images to. Only include if image uploads are supported here. */
public $uploadUrl;
/** @var int Optional. When a word limit is specified a word counter will be shown */
public $wordLimit = 0;
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
if (!empty($this->init)) {
$config['init'] = $this->init;
}
$config['plugins'] = $this->plugins;
if (!empty($this->size)) {
$config['size'] = $this->size;
}
$config['toolbar'] = $this->toolbar;
if (!empty($this->uploadUrl)) {
$config['uploadUrl'] = $this->uploadUrl;
}
if ($this->wordLimit) {
$config['wordLimit'] = $this->wordLimit;
$config['wordCountLabel'] = __('publication.wordCount');
}
return $config;
}
}
@@ -0,0 +1,36 @@
<?php
/**
* @file classes/components/form/FieldSelect.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldSelect
*
* @ingroup classes_controllers_form
*
* @brief A select field in a form.
*/
namespace PKP\components\forms;
class FieldSelect extends Field
{
/** @copydoc Field::$component */
public $component = 'field-select';
/** @var array The options which can be selected */
public $options = [];
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['options'] = $this->options;
return $config;
}
}
@@ -0,0 +1,22 @@
<?php
/**
* @file classes/components/form/FieldSelectSubmissions.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldSelectSubmissions
*
* @ingroup classes_controllers_form
*
* @brief A text field to search for and select submissions.
*/
namespace PKP\components\forms;
class FieldSelectSubmissions extends FieldBaseAutosuggest
{
/** @copydoc Field::$component */
public $component = 'field-select-submissions';
}
@@ -0,0 +1,22 @@
<?php
/**
* @file classes/components/form/FieldSelectUsers.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldSelectUsers
*
* @ingroup classes_controllers_form
*
* @brief A text field to search for and select users.
*/
namespace PKP\components\forms;
class FieldSelectUsers extends FieldBaseAutosuggest
{
/** @copydoc Field::$component */
public $component = 'field-select-users';
}
@@ -0,0 +1,39 @@
<?php
/**
* @file classes/components/form/FieldShowEnsuringLink.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldShowEnsuringLink
*
* @ingroup classes_controllers_form
*
* @brief An extension of the FieldOptions for the configuration setting which
* determines whether or not to show a link to reviewers about keeping reviews
* anonymous.
*/
namespace PKP\components\forms;
class FieldShowEnsuringLink extends FieldOptions
{
/** @copydoc Field::$component */
public $component = 'field-show-ensuring-link';
/** @var string The message to show in a modal when the link is clicked. */
public $message = '';
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['message'] = __('review.anonymousPeerReview');
$config['modalTitle'] = __('review.anonymousPeerReview.title');
return $config;
}
}
@@ -0,0 +1,52 @@
<?php
/**
* @file classes/components/form/FieldText.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldText
*
* @ingroup classes_controllers_form
*
* @brief A basic text field in a form.
*/
namespace PKP\components\forms;
class FieldText extends Field
{
/** @copydoc Field::$component */
public $component = 'field-text';
/** @var string What should the <input type=""> be? */
public $inputType = 'text';
/** @var bool Whether the user should have to click a button to edit the field */
public $optIntoEdit = false;
/** @var string The label of the button added by self::$optIntoEdit */
public $optIntoEditLabel = '';
/** @var string Accepts: `small`, `normal` or `large` */
public $size = 'normal';
/** @var string A prefix to display before the input value */
public $prefix = '';
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['inputType'] = $this->inputType;
$config['optIntoEdit'] = $this->optIntoEdit;
$config['optIntoEditLabel'] = $this->optIntoEditLabel;
$config['size'] = $this->size;
$config['prefix'] = $this->prefix;
return $config;
}
}
@@ -0,0 +1,38 @@
<?php
/**
* @file classes/components/form/FieldTextarea.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldTextarea
*
* @ingroup classes_controllers_form
*
* @brief A multiline textarea field in a form.
*/
namespace PKP\components\forms;
class FieldTextarea extends Field
{
/** @copydoc Field::$component */
public $component = 'field-textarea';
/** @var string Optional. A preset size option. */
public $size;
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
if (isset($this->size)) {
$config['size'] = $this->size;
}
return $config;
}
}
@@ -0,0 +1,86 @@
<?php
/**
* @file classes/components/form/FieldUpload.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldUpload
*
* @ingroup classes_controllers_form
*
* @brief A field for uploading a file.
*/
namespace PKP\components\forms;
use APP\core\Application;
class FieldUpload extends Field
{
/** @copydoc Field::$component */
public $component = 'field-upload';
/**
* @var array Options to pass to the dropzone.js instance.
*
* A `url` key must be included with the value of the API endpoint where files
* can be uploaded to: <api-path>/temporaryFiles.
*/
public $options = [];
/**
* @copydoc Field::__construct()
*/
public function __construct($name, $args = [])
{
parent::__construct($name, $args);
$this->options['maxFilesize'] = Application::getIntMaxFileMBs();
$this->options['timeout'] = ini_get('max_execution_time')
? ini_get('max_execution_time') * 1000
: 0;
$this->options = array_merge(
[
'dropzoneDictDefaultMessage' => __('form.dropzone.dictDefaultMessage'),
'dropzoneDictFallbackMessage' => __('form.dropzone.dictFallbackMessage'),
'dropzoneDictFallbackText' => __('form.dropzone.dictFallbackText'),
'dropzoneDictFileTooBig' => __('form.dropzone.dictFileTooBig'),
'dropzoneDictInvalidFileType' => __('form.dropzone.dictInvalidFileType'),
'dropzoneDictResponseError' => __('form.dropzone.dictResponseError'),
'dropzoneDictCancelUpload' => __('form.dropzone.dictCancelUpload'),
'dropzoneDictUploadCanceled' => __('form.dropzone.dictUploadCanceled'),
'dropzoneDictCancelUploadConfirmation' => __('form.dropzone.dictCancelUploadConfirmation'),
'dropzoneDictRemoveFile' => __('form.dropzone.dictRemoveFile'),
'dropzoneDictMaxFilesExceeded' => __('form.dropzone.dictMaxFilesExceeded'),
],
$this->options
);
}
/**
* @copydoc Field::validate()
*/
public function validate()
{
if (empty($this->options['url'])) {
return false;
}
return parent::validate();
}
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
$config = parent::getConfig();
$config['options'] = $this->options;
$config['uploadFileLabel'] = __('common.upload.addFile');
$config['restoreLabel'] = __('common.upload.restore');
return $config;
}
}
@@ -0,0 +1,60 @@
<?php
/**
* @file classes/components/form/FieldUploadImage.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FieldUploadImage
*
* @ingroup classes_controllers_form
*
* @brief A field for uploading a file.
*/
namespace PKP\components\forms;
class FieldUploadImage extends FieldUpload
{
/** @copydoc Field::$component */
public $component = 'field-upload-image';
/** @var string Base url for displaying the image */
public $baseUrl = '';
/** @var string Label for the alt text field */
public $altTextLabel = '';
/** @var string Description for the alt text field */
public $altTextDescription = '';
/** @var string Description for the image thumbnail */
public $thumbnailDescription = '';
/**
* @copydoc Field::getConfig()
*/
public function getConfig()
{
if (!array_key_exists('acceptedFiles', $this->options)) {
$this->options['acceptedFiles'] = 'image/*';
}
$config = parent::getConfig();
$config['baseUrl'] = $this->baseUrl;
$config['thumbnailDescription'] = __('common.upload.thumbnailPreview');
$config['altTextLabel'] = __('common.altText');
$config['altTextDescription'] = __('common.altTextInstructions');
return $config;
}
/**
* @copydoc Field::getEmptyValue()
*/
public function getEmptyValue()
{
return null;
}
}
@@ -0,0 +1,347 @@
<?php
/**
* @file classes/components/form/FormComponent.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class FormComponent
*
* @ingroup classes_controllers_form
*
* @brief A base class for building forms to be passed to the Form component
* in the UI Library.
*/
namespace PKP\components\forms;
use Exception;
use PKP\facades\Locale;
use PKP\plugins\Hook;
define('FIELD_POSITION_BEFORE', 'before');
define('FIELD_POSITION_AFTER', 'after');
class FormComponent
{
/**
* @var string An $action value that will emit an event
* when the form is submitted, instead of sending a
* HTTP request
*/
public const ACTION_EMIT = 'emit';
/** @var string A unique ID for this form */
public $id = '';
/** @var string Form method: POST or PUT */
public $method = '';
/** @var string Where the form should be submitted. */
public $action = '';
/** @var array Key/value list of languages this form should support. Key = locale code. Value = locale name */
public $locales = [];
/** @var array List of fields in this form. */
public $fields = [];
/** @var array List of groups in this form. */
public $groups = [];
/** @var array List of hidden fields in this form. */
public $hiddenFields = [];
/** @var array List of pages in this form. */
public $pages = [];
/** @var array List of error messages */
public $errors = [];
/**
* Initialize the form with config parameters
*
* @param string $id
* @param string $method
* @param string $action
* @param array $locales
*/
public function __construct($id, $method, $action, $locales)
{
$this->id = $id;
$this->action = $action;
$this->method = $method;
$this->locales = $locales;
}
/**
* Add a form field
*
* @param Field $field
* @param array $position [
*
* @option string One of FIELD_POSITION_BEFORE or FIELD_POSITION_AFTER
* @option string The field to position it before or after
* ]
*/
public function addField($field, $position = []): static
{
if (empty($position)) {
$this->fields[] = $field;
} else {
$this->fields = $this->addToPosition($position[1], $this->fields, $field, $position[0]);
}
return $this;
}
/**
* Remove a form field
*
* @param string $fieldName
*/
public function removeField($fieldName): static
{
$this->fields = array_values(array_filter($this->fields, function ($field) use ($fieldName) {
return $field->name !== $fieldName;
}));
return $this;
}
/**
* Get a form field
*
* @param string $fieldName
*
* @return ?Field
*/
public function getField($fieldName)
{
foreach ($this->fields as $field) {
if ($field->name === $fieldName) {
return $field;
}
}
return null;
}
/**
* Add a form group
*
* @param array $args [
*
* @option id string Required A unique ID for this form group
* @option label string A label to identify this group of fields. Will become the fieldset's <legend>
* @option description string A description of this group of fields.
* ]
*
* @param array $position [
*
* @option string One of FIELD_POSITION_BEFORE or FIELD_POSITION_AFTER
* @option string The group to position it before or after
* ]
*/
public function addGroup($args, $position = []): static
{
if (empty($args['id'])) {
throw new Exception('Tried to add a form group without an id.');
}
if (empty($position)) {
$this->groups[] = $args;
} else {
$this->groups = $this->addToPosition($position[1], $this->groups, $args, $position[0]);
}
return $this;
}
/**
* Remove a form group
*
* @param string $groupId
*/
public function removeGroup($groupId): static
{
$this->groups = array_filter($this->groups, function ($group) use ($groupId) {
return $group['id'] !== $groupId;
});
$this->fields = array_filter($this->fields, function ($field) use ($groupId) {
return $field['groupId'] !== $groupId;
});
return $this;
}
/**
* Add a form page
*
* @param array $args [
*
* @option id string Required A unique ID for this form page
* @option label string The name of the page to identify it in the page list
* @option submitButton array Required Assoc array defining submission/next button params. Supports any param of the Button component in the UI Library.
* @option previousButton array Assoc array defining button params to go back to the previous page. Supports any param of the Button component in the UI Library.
* ]
*
* @param array $position [
*
* @option string One of FIELD_POSITION_BEFORE or FIELD_POSITION_AFTER
* @option string The page to position it before or after
* ]
*/
public function addPage($args, $position = []): static
{
if (empty($args['id'])) {
fatalError('Tried to add a form page without an id.');
}
if (empty($position)) {
$this->pages[] = $args;
} else {
$this->pages = $this->addToPosition($position[1], $this->pages, $args, $position[0]);
}
return $this;
}
/**
* Remove a form page
*
* @param string $pageId
*/
public function removePage($pageId): static
{
$this->pages = array_filter($this->pages, function ($page) use ($pageId) {
return $page['id'] !== $pageId;
});
foreach ($this->groups as $group) {
if ($group['pageId'] === $pageId) {
$this->removeGroup($group['id']);
}
}
return $this;
}
/**
* Add an field, group or page to a specific position in its array
*
* @param string $id The id of the item to position before or after
* @param array $list The list of fields, groups or pages
* @param mixed $item The item to insert
* @param string $position FIELD_POSITION_BEFORE or FIELD_POSITION_AFTER
*
* @return array
*/
public function addToPosition($id, $list, $item, $position)
{
$index = count($list);
foreach ($list as $key => $val) {
if (($val instanceof \PKP\components\forms\Field && $id === $val->name) || (!$val instanceof \PKP\components\forms\Field && $id === $val['id'])) {
$index = $key;
break;
}
}
if (!$index && $position === FIELD_POSITION_BEFORE) {
array_unshift($list, $item);
return $list;
}
$slice = $position === FIELD_POSITION_BEFORE ? $index : $index + 1;
return array_merge(
array_slice($list, 0, $slice),
[$item],
array_slice($list, $slice)
);
}
/**
* Add a hidden field to this form
*/
public function addHiddenField(string $name, $value)
{
$this->hiddenFields[$name] = $value;
}
/**
* Retrieve the configuration data to be used when initializing this
* handler on the frontend
*
* @return array Configuration data
*/
public function getConfig()
{
if (empty($this->id) || empty($this->action) || ($this->action !== self::ACTION_EMIT && empty($this->method))) {
throw new Exception('FormComponent::getConfig() was called but one or more required property is missing: id, method, action.');
}
Hook::run('Form::config::before', [$this]);
// Add a default page/group if none exist
if (!$this->groups) {
$this->addGroup(['id' => 'default']);
$this->fields = array_map(function ($field) {
$field->groupId = 'default';
return $field;
}, $this->fields);
}
if (!$this->pages) {
$this->addPage(['id' => 'default', 'submitButton' => ['label' => __('common.save')]]);
$this->groups = array_map(function ($group) {
$group['pageId'] = 'default';
return $group;
}, $this->groups);
}
$fieldsConfig = array_map([$this, 'getFieldConfig'], $this->fields);
$visibleLocales = [Locale::getLocale()];
if (Locale::getLocale() !== Locale::getPrimaryLocale()) {
array_unshift($visibleLocales, Locale::getPrimaryLocale());
}
$config = [
'id' => $this->id,
'method' => $this->method,
'action' => $this->action,
'fields' => $fieldsConfig,
'groups' => $this->groups,
'hiddenFields' => (object) $this->hiddenFields,
'pages' => $this->pages,
'primaryLocale' => Locale::getPrimaryLocale(),
'visibleLocales' => $visibleLocales,
'supportedFormLocales' => array_values($this->locales), // See #5690
'errors' => (object) [],
];
Hook::call('Form::config::after', [&$config, $this]);
return $config;
}
/**
* Compile a configuration array for a single field
*
* @param Field $field
*
* @return array
*/
public function getFieldConfig($field)
{
$config = $field->getConfig();
// Add a value property if the field does not include one
if (!array_key_exists('value', $config)) {
$config['value'] = $field->isMultilingual ? [] : $field->getEmptyValue();
}
if ($field->isMultilingual) {
if (is_null($config['value'])) {
$config['value'] = [];
}
foreach ($this->locales as $locale) {
if (!array_key_exists($locale['key'], $config['value'])) {
$config['value'][$locale['key']] = $field->getEmptyValue();
}
}
}
return $config;
}
}
@@ -0,0 +1,123 @@
<?php
/**
* @file classes/components/form/announcement/PKPAnnouncementForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAnnouncementForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for creating a new announcement
*/
namespace PKP\components\forms\announcement;
use APP\core\Application;
use PKP\announcement\AnnouncementTypeDAO;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldRichTextarea;
use PKP\components\forms\FieldText;
use PKP\components\forms\FieldUploadImage;
use PKP\components\forms\FormComponent;
use PKP\config\Config;
use PKP\context\Context;
use PKP\db\DAORegistry;
define('FORM_ANNOUNCEMENT', 'announcement');
class PKPAnnouncementForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ANNOUNCEMENT;
/** @copydoc FormComponent::$method */
public $method = 'POST';
public ?Context $context;
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
*/
public function __construct($action, $locales, string $baseUrl, string $temporaryFileApiUrl, ?Context $context = null)
{
$this->action = $action;
$this->locales = $locales;
$this->context = $context;
$announcementTypeOptions = $this->getAnnouncementTypeOptions();
$this->addField(new FieldText('title', [
'label' => __('common.title'),
'size' => 'large',
'isMultilingual' => true,
]))
->addField(new FieldRichTextarea('descriptionShort', [
'label' => __('manager.announcements.form.descriptionShort'),
'description' => __('manager.announcements.form.descriptionShortInstructions'),
'isMultilingual' => true,
]))
->addField(new FieldRichTextarea('description', [
'label' => __('manager.announcements.form.description'),
'description' => __('manager.announcements.form.descriptionInstructions'),
'isMultilingual' => true,
'size' => 'large',
'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist',
'plugins' => 'paste,link,lists',
]));
if (Config::getVar('features', 'announcement_images')) {
$this->addField(new FieldUploadImage('image', [
'label' => __('manager.image'),
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
],
]));
}
$this->addField(new FieldText('dateExpire', [
'label' => __('manager.announcements.form.dateExpire'),
'description' => __('manager.announcements.form.dateExpireInstructions'),
'size' => 'small',
]));
if (!empty($announcementTypeOptions)) {
$this->addField(new FieldOptions('typeId', [
'label' => __('manager.announcementTypes.typeName'),
'type' => 'radio',
'options' => $announcementTypeOptions,
]));
}
$this->addField(new FieldOptions('sendEmail', [
'label' => __('common.sendEmail'),
'options' => [
[
'value' => true,
'label' => __('notification.sendNotificationConfirmation')
]
]
]));
}
protected function getAnnouncementTypeOptions(): array
{
/** @var AnnouncementTypeDAO */
$announcementTypeDao = DAORegistry::getDAO('AnnouncementTypeDAO');
$announcementTypes = $announcementTypeDao->getByContextId($this->context?->getId());
$announcementTypeOptions = [];
foreach ($announcementTypes as $announcementType) {
$announcementTypeOptions[] = [
'value' => (int) $announcementType->getId(),
'label' => $announcementType->getLocalizedTypeName(),
];
}
return $announcementTypeOptions;
}
}
@@ -0,0 +1,69 @@
<?php
/**
* @file classes/components/form/context/PKPAnnouncementSettingsForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAnnouncementSettingsForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for enabling and configuring announcements.
*/
namespace PKP\components\forms\context;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldRichTextarea;
use PKP\components\forms\FieldText;
use PKP\components\forms\FormComponent;
use PKP\context\Context;
use PKP\site\Site;
define('FORM_ANNOUNCEMENT_SETTINGS', 'announcementSettings');
class PKPAnnouncementSettingsForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_ANNOUNCEMENT_SETTINGS;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
*/
public function __construct($action, $locales, Context|Site $context)
{
$this->action = $action;
$this->locales = $locales;
$this->addField(new FieldOptions('enableAnnouncements', [
'label' => __('manager.setup.announcements'),
'description' => __('manager.setup.enableAnnouncements.description'),
'options' => [
['value' => true, 'label' => __('manager.setup.enableAnnouncements.enable')]
],
'value' => (bool) $context->getData('enableAnnouncements'),
]))
->addField(new FieldRichTextarea('announcementsIntroduction', [
'label' => __('manager.setup.announcementsIntroduction'),
'tooltip' => __('manager.setup.announcementsIntroduction.description'),
'isMultilingual' => true,
'value' => $context->getData('announcementsIntroduction'),
'showWhen' => 'enableAnnouncements',
]))
->addField(new FieldText('numAnnouncementsHomepage', [
'label' => __('manager.setup.numAnnouncementsHomepage'),
'description' => __('manager.setup.numAnnouncementsHomepage.description'),
'size' => 'small',
'value' => $context->getData('numAnnouncementsHomepage'),
'showWhen' => 'enableAnnouncements',
]));
}
}
@@ -0,0 +1,76 @@
<?php
/**
* @file classes/components/form/context/PKPAppearanceAdvancedForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAppearanceAdvancedForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for advanced settings under the website appearance tab.
*/
namespace PKP\components\forms\context;
use PKP\components\forms\FieldRichTextarea;
use PKP\components\forms\FieldUpload;
use PKP\components\forms\FieldUploadImage;
use PKP\components\forms\FormComponent;
define('FORM_APPEARANCE_ADVANCED', 'appearanceAdvanced');
class PKPAppearanceAdvancedForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_APPEARANCE_ADVANCED;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param \PKP\context\Context $context Journal or Press to change settings for
* @param string $baseUrl Site's base URL. Used for image previews.
* @param string $temporaryFileApiUrl URL to upload files to
* @param string $imageUploadUrl The API endpoint for images uploaded through the rich text field
*/
public function __construct($action, $locales, $context, $baseUrl, $temporaryFileApiUrl, $imageUploadUrl)
{
$this->action = $action;
$this->locales = $locales;
$this->addField(new FieldUpload('styleSheet', [
'label' => __('manager.setup.useStyleSheet'),
'value' => $context->getData('styleSheet'),
'options' => [
'url' => $temporaryFileApiUrl,
'acceptedFiles' => '.css',
],
]))
->addField(new FieldUploadImage('favicon', [
'label' => __('manager.setup.favicon'),
'value' => $context->getData('favicon'),
'isMultilingual' => true,
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
'acceptedFiles' => 'image/x-icon,image/png,image/gif',
],
]))
->addField(new FieldRichTextarea('additionalHomeContent', [
'label' => __('manager.setup.additionalContent'),
'description' => __('manager.setup.additionalContent.description'),
'isMultilingual' => true,
'value' => $context->getData('additionalHomeContent'),
'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist | image | code',
'plugins' => 'paste,link,lists,image,code',
'uploadUrl' => $imageUploadUrl,
]));
}
}
@@ -0,0 +1,112 @@
<?php
/**
* @file classes/components/form/context/PKPAppearanceSetupForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPAppearanceSetupForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for general website appearance setup, such as uploading
* a logo.
*/
namespace PKP\components\forms\context;
use PKP\components\forms\FieldOptions;
use PKP\components\forms\FieldRichTextarea;
use PKP\components\forms\FieldUploadImage;
use PKP\components\forms\FormComponent;
use PKP\plugins\PluginRegistry;
define('FORM_APPEARANCE_SETUP', 'appearanceSetup');
class PKPAppearanceSetupForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_APPEARANCE_SETUP;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param \PKP\context\Context $context Journal or Press to change settings for
* @param string $baseUrl Site's base URL. Used for image previews.
* @param string $temporaryFileApiUrl URL to upload files to
* @param string $imageUploadUrl The API endpoint for images uploaded through the rich text field
*/
public function __construct($action, $locales, $context, $baseUrl, $temporaryFileApiUrl, $imageUploadUrl)
{
$this->action = $action;
$this->locales = $locales;
$sidebarOptions = [];
$enabledOptions = [];
$disabledOptions = [];
$currentBlocks = (array) $context->getData('sidebar');
$plugins = PluginRegistry::loadCategory('blocks', true);
foreach ($currentBlocks as $plugin) {
if (isset($plugins[$plugin])) {
$enabledOptions[] = [
'value' => $plugin,
'label' => htmlspecialchars($plugins[$plugin]->getDisplayName()),
];
}
}
foreach ($plugins as $pluginName => $plugin) {
if (!in_array($pluginName, $currentBlocks)) {
$disabledOptions[] = [
'value' => $pluginName,
'label' => htmlspecialchars($plugin->getDisplayName()),
];
}
}
$sidebarOptions = array_merge($enabledOptions, $disabledOptions);
$this->addField(new FieldUploadImage('pageHeaderLogoImage', [
'label' => __('manager.setup.logo'),
'value' => $context->getData('pageHeaderLogoImage'),
'isMultilingual' => true,
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
],
]))
->addField(new FieldUploadImage('homepageImage', [
'label' => __('manager.setup.homepageImage'),
'tooltip' => __('manager.setup.homepageImage.description'),
'value' => $context->getData('homepageImage'),
'isMultilingual' => true,
'baseUrl' => $baseUrl,
'options' => [
'url' => $temporaryFileApiUrl,
],
]))
->addField(new FieldRichTextarea('pageFooter', [
'label' => __('manager.setup.pageFooter'),
'tooltip' => __('manager.setup.pageFooter.description'),
'isMultilingual' => true,
'value' => $context->getData('pageFooter'),
'toolbar' => 'bold italic superscript subscript | link | blockquote bullist numlist | image | code',
'plugins' => 'paste,link,lists,image,code',
'uploadUrl' => $imageUploadUrl,
]))
->addField(new FieldOptions('sidebar', [
'label' => __('manager.setup.layout.sidebar'),
'isOrderable' => true,
'value' => $currentBlocks,
'options' => $sidebarOptions,
]));
}
}
@@ -0,0 +1,102 @@
<?php
/**
* @file classes/components/form/context/PKPContactForm.php
*
* Copyright (c) 2014-2021 Simon Fraser University
* Copyright (c) 2000-2021 John Willinsky
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
*
* @class PKPContactForm
*
* @ingroup classes_controllers_form
*
* @brief A preset form for configuring a context's contact details.
*/
namespace PKP\components\forms\context;
use PKP\components\forms\FieldText;
use PKP\components\forms\FieldTextarea;
use PKP\components\forms\FormComponent;
define('FORM_CONTACT', 'contact');
class PKPContactForm extends FormComponent
{
/** @copydoc FormComponent::$id */
public $id = FORM_CONTACT;
/** @copydoc FormComponent::$method */
public $method = 'PUT';
/**
* Constructor
*
* @param string $action URL to submit the form to
* @param array $locales Supported locales
* @param \PKP\context\Context $context Journal or Press to change settings for
*/
public function __construct($action, $locales, $context)
{
$this->action = $action;
$this->locales = $locales;
$this->addGroup([
'id' => 'principal',
'label' => __('manager.setup.principalContact'),
'description' => __('manager.setup.principalContactDescription'),
])
->addField(new FieldText('contactName', [
'label' => __('common.name'),
'isRequired' => true,
'groupId' => 'principal',
'value' => $context->getData('contactName'),
]))
->addField(new FieldText('contactEmail', [
'label' => __('user.email'),
'isRequired' => true,
'groupId' => 'principal',
'value' => $context->getData('contactEmail'),
]))
->addField(new FieldText('contactPhone', [
'label' => __('user.phone'),
'groupId' => 'principal',
'value' => $context->getData('contactPhone'),
]))
->addField(new FieldText('contactAffiliation', [
'label' => __('user.affiliation'),
'isMultilingual' => true,
'groupId' => 'principal',
'value' => $context->getData('contactAffiliation'),
]))
->addField(new FieldTextarea('mailingAddress', [
'label' => __('common.mailingAddress'),
'isRequired' => false,
'size' => 'small',
'groupId' => 'principal',
'value' => $context->getData('mailingAddress'),
]))
->addGroup([
'id' => 'technical',
'label' => __('manager.setup.technicalSupportContact'),
'description' => __('manager.setup.technicalSupportContactDescription'),
])
->addField(new FieldText('supportName', [
'label' => __('common.name'),
'isRequired' => true,
'groupId' => 'technical',
'value' => $context->getData('supportName'),
]))
->addField(new FieldText('supportEmail', [
'label' => __('user.email'),
'isRequired' => true,
'groupId' => 'technical',
'value' => $context->getData('supportEmail'),
]))
->addField(new FieldText('supportPhone', [
'label' => __('user.phone'),
'groupId' => 'technical',
'value' => $context->getData('supportPhone'),
]));
}
}

Some files were not shown because too many files have changed in this diff Show More