first commit
This commit is contained in:
+24
@@ -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
@@ -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.
|
||||
@@ -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);
|
||||
@@ -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
@@ -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>.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Vendored
+93
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+150
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user