包含:登录失败锁定、90天密码有效期、30分钟会话超时、 强制改密、登录审计日志、屏幕水印、企业背景图、 备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
# Minified JavaScript files shouldn't be changed
|
||||
[**.min.js]
|
||||
indent_style = ignore
|
||||
insert_final_newline = ignore
|
||||
|
||||
# iCalendar files must have CRLF line endings
|
||||
[**.ics]
|
||||
end_of_line = crlf
|
||||
@@ -0,0 +1,5 @@
|
||||
* text eol=lf
|
||||
*.ics text eol=crlf
|
||||
*.gif binary
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Browser details (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Server details (please complete the following information):**
|
||||
- MRBS version [e.g. 1.9.4]
|
||||
- Web server: [e.g. Apache 2.4.48]
|
||||
- OS: [e.g. CentOS 7.9]
|
||||
- PHP version: [e.g. 7.4.23]
|
||||
- Database used: [e.g. MySQL 8.0.26]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
name: Support request
|
||||
about: To raise support requests.
|
||||
title: ''
|
||||
labels: support request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the problem you're facing/question you have**
|
||||
A clear and concise description of the problem you're facing, or the question you have.
|
||||
|
||||
**Browser details (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Server details (please complete the following information):**
|
||||
- MRBS version [e.g. 1.9.4]
|
||||
- Web server: [e.g. Apache 2.4.48]
|
||||
- OS: [e.g. CentOS 7.9]
|
||||
- PHP version: [e.g. 7.4.23]
|
||||
- Database used: [e.g. MySQL 8.0.26]
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,53 @@
|
||||
name: Docker image
|
||||
|
||||
on:
|
||||
push:
|
||||
# Publish `main` as Docker `latest` image.
|
||||
branches:
|
||||
- main
|
||||
|
||||
# Publish `v1.2.3` tags as releases.
|
||||
tags:
|
||||
- v*
|
||||
|
||||
# Run tests for any PRs.
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
IMAGE_NAME: mrbs
|
||||
|
||||
jobs:
|
||||
# Push image to GitHub Packages.
|
||||
# See also https://docs.docker.com/docker-hub/builds/
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build image
|
||||
run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}"
|
||||
|
||||
- name: Log in to registry
|
||||
# This is where you will update the personal access token to GITHUB_TOKEN
|
||||
run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u $ --password-stdin
|
||||
|
||||
- name: Push image
|
||||
run: |
|
||||
IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME
|
||||
|
||||
# Change all uppercase to lowercase
|
||||
IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]')
|
||||
# Strip git ref prefix from version
|
||||
VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
|
||||
# Strip "v" prefix from tag name
|
||||
[[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//')
|
||||
# Use Docker `latest` tag convention
|
||||
[ "$VERSION" == "main" ] && VERSION=latest
|
||||
echo IMAGE_ID=$IMAGE_ID
|
||||
echo VERSION=$VERSION
|
||||
docker tag $IMAGE_NAME $IMAGE_ID:$VERSION
|
||||
docker push $IMAGE_ID:$VERSION
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.bak-*
|
||||
*.bak
|
||||
*.legacy-*
|
||||
*.smokebak*
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
vendor/
|
||||
node_modules/
|
||||
*.log
|
||||
*.tmp
|
||||
@@ -0,0 +1,18 @@
|
||||
# Glob syntax
|
||||
syntax: glob
|
||||
|
||||
*~
|
||||
*.bak
|
||||
web/config.inc.php.*
|
||||
web/css/custom.css
|
||||
web/config.inc.php
|
||||
|
||||
# Regexp syntax rules
|
||||
syntax: regexp
|
||||
|
||||
^\.settings$
|
||||
^\.project$
|
||||
^\.buildpath
|
||||
^\.idea
|
||||
^hg-code.iml
|
||||
^nbproject
|
||||
+1120
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,339 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
675 Mass Ave, Cambridge, MA 02139, USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) 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
|
||||
this service 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 make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. 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.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
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
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the 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 a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE 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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Appendix: 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
|
||||
convey 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) 19yy <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 2 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, write to the Free Software
|
||||
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) 19yy name of author
|
||||
Gnomovision 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, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This 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 Library General
|
||||
Public License instead of this License.
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM php:8.4-apache
|
||||
|
||||
RUN a2enmod rewrite
|
||||
RUN apt-get update && apt-get install -y libicu-dev locales-all \
|
||||
&& apt-get clean
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql intl
|
||||
|
||||
COPY web/ /var/www/html/
|
||||
COPY docker-config.inc.php /var/www/html/config.inc.php
|
||||
@@ -0,0 +1,564 @@
|
||||
MRBS Installation Instructions
|
||||
|
||||
|
||||
|
||||
REQUIREMENTS
|
||||
---------------------------------------------------------------------------
|
||||
MRBS works with both MySQL (Version 5.5.3 and above, though MySQL Version
|
||||
5.7.5 and above or MariaDB Version 10.0.2 and above are recommended) and
|
||||
PostgreSQL (Version 8.2 and above) systems. [The reason that if you are using
|
||||
MySQL you are recommended to use MySQL Version 5.7.5 and above or MariaDB
|
||||
Version 10.0.2 and above is that these versions support multiple locks and thus
|
||||
enable MRBS to use a custom database session handler. This is more secure than
|
||||
the standard file based session handler, easier to configure and enables MRBS
|
||||
to be used on clustered web servers.]
|
||||
|
||||
You must have at least PHP 7.2.5, with support for your chosen database system
|
||||
installed and working for this application. See the PHP (www.php.net), MySQL
|
||||
(www.mysql.com), and PostgreSQL (www.postgresql.org) sites for more info on
|
||||
setting these up. You need to know how to install, secure, run, maintain,
|
||||
and back up your chosen database system.
|
||||
|
||||
MRBS also requires that 'iconv' PHP extension, to provide internationalisation.
|
||||
The 'intl' and 'mbstring' extensions are recommended.
|
||||
|
||||
You can run PHP either as a CGI or with a direct module interface (also called
|
||||
SAPI). These servers include Apache, Microsoft Internet Information Server,
|
||||
Netscape and iPlanet servers. Many other servers have support for ISAPI, the
|
||||
Microsoft module interface.
|
||||
You'll get better performance with PHP setup as a module. Not only will you
|
||||
not have to deal with the CGI performance hit, but you'll be able to use PHP's
|
||||
database connection pooling. However, be careful that you don't exceed
|
||||
the maximum number of connections allowed to your database; with connection
|
||||
pooling PHP/Apache can potentially create a connection from each Apache
|
||||
child server to the database.
|
||||
Also many MRBS authentication schemes use basic HTTP authentication. These
|
||||
don't work if you run PHP as a CGI.
|
||||
|
||||
If you are using PHP as an Apache module, you probably want to ensure that the
|
||||
Apache MaxConnectionsPerChild (aka. MaxRequestsPerChild in Apache pre-v2.3.9)
|
||||
is not set to 0, in case of undetected memory leaks in PHP or MRBS. In Ubuntu
|
||||
25.10 this can be found in the mpm_prefork.conf file, located in
|
||||
/etc/apache2/mods-available/.
|
||||
|
||||
OVERVIEW
|
||||
---------------------------------------------------------------------------
|
||||
The steps involved in installing MRBS are:
|
||||
|
||||
1. Installing the MRBS files on your web server
|
||||
2. Creating the MRBS tables in your database
|
||||
3. Configuring MRBS
|
||||
|
||||
After that you can then point your browser at MRBS and start creating areas
|
||||
and rooms.
|
||||
|
||||
|
||||
INSTALLING THE MRBS FILES
|
||||
---------------------------------------------------------------------------
|
||||
To install MRBS, just unpack the distribution into a temporary directory,
|
||||
then copy the files in the "web" subdirectory into a new directory somewhere
|
||||
your web server can find them.
|
||||
|
||||
If you are using a remote webserver, unpack the files into a temporary
|
||||
directory on your local machine and then upload them to suitable directory
|
||||
on your webserver, for example mydomain.com/mrbs
|
||||
|
||||
If you are using a Unix/Linux webserver to which you have access, then
|
||||
an example of the installation might be:
|
||||
|
||||
Unpack the software into a new temporary directory, something like this:
|
||||
$ tar -xvzf ~/download/mrbs-1.7.1.tgz (or whatever version)
|
||||
$ cd mrbs-1.7.1 (or whatever version)
|
||||
|
||||
MRBS comes with a sample configuration file "web/config.inc.php-sample" -
|
||||
this should be copied to "web/config.inc.php" and configured for your
|
||||
site. The minimum you generally need to configure are the timezone
|
||||
(unless your PHP configuration already defines the timezone) and the
|
||||
database access details.
|
||||
|
||||
If you are upgrading from a previous version of MRBS, you should consider
|
||||
copying your changes to the "config.inc.php" file into a new copy
|
||||
of config.inc.php copied from the new MRBS version's config.inc.php-sample.
|
||||
|
||||
Now install MRBS by copying the contents of the "web" subdirectory of the
|
||||
distribution somewhere your web server can find it. For example:
|
||||
$ cp -r web /var/lib/apache/htdocs/mrbs
|
||||
|
||||
|
||||
CREATING THE MRBS TABLES IN YOUR DATABASE
|
||||
---------------------------------------------------------------------------
|
||||
For a new install:
|
||||
|
||||
If you are using a remote webserver you should use your database
|
||||
administration program (eg phpMyAdmin in your control panel) to create the
|
||||
MRBS tables, by executing the contents of tables.my.sql. For example, if
|
||||
you are using phpMyAdmin copy the contents of tables.my.sql into the SQL
|
||||
tab of phpMyAdmin and execute it as an SQL query. This assumes that you
|
||||
have already created a database - if not you should create a database
|
||||
first.
|
||||
|
||||
If you are using a Unix/Linux webserver to which you have access, then the
|
||||
procedure might be:
|
||||
|
||||
[MySQL] $ mysqladmin create mrbs
|
||||
[PostgreSQL] $ createdb -E UTF8 -T template0 mrbs
|
||||
|
||||
(This will create a database named "mrbs", but you can use any name.)
|
||||
|
||||
Create the MRBS tables using the supplied tables.*.sql file:
|
||||
|
||||
[MySQL] $ mysql mrbs < tables.my.sql
|
||||
[PostgreSQL] $ psql -a -f tables.pg.sql mrbs
|
||||
|
||||
where "mrbs" is the name of your database (mentioned above).
|
||||
This will create all the needed tables.
|
||||
You may need to set rights on the tables; for PostgreSQL see "grant.pg.sql".
|
||||
If you need to delete the tables, for PostgreSQL see "destroy.pg.sql".
|
||||
|
||||
The tables are now empty and ready for use.
|
||||
|
||||
For an upgrade:
|
||||
|
||||
If you are upgrading from MRBS 1.2-pre3 or later, your database will be
|
||||
upgraded automatically if necessary when you first run MRBS. You will
|
||||
be prompted for a database (not MRBS) username and password with rights
|
||||
to create and alter tables. Otherwise, please see the UPGRADE file.
|
||||
|
||||
For a second installation or to use different table names:
|
||||
|
||||
If you have table name conflicts or want to do a second installation
|
||||
and only have access to one database, then you can modify the 'mrbs_'
|
||||
prefix for the table names.
|
||||
|
||||
In tables.*.sql you will need to change the table names and then follow
|
||||
the instructions above for creating the tables in your database.
|
||||
|
||||
When editing config.inc.php, you need to change the table name prefix
|
||||
from "mrbs_" to the value you chose using the variable $db_tbl_prefix.
|
||||
|
||||
WARNING: All of the .sql files are set up to use the 'mrbs_' prefix
|
||||
therefore you will have to edit them before you use them if you
|
||||
change the prefix for your tables.
|
||||
|
||||
|
||||
Maintenance:
|
||||
|
||||
Be sure to back up your database regularly.
|
||||
For PostgreSQL, be sure to run the "vacuum" command regularly.
|
||||
You can clean out old entries from your database using the supplied SQL
|
||||
scripts purge.my.sql (for MySQL) and purge.pg.sql (for PostgreSQL). Read
|
||||
the comments at the top of the scripts before using them.
|
||||
|
||||
|
||||
ADDING EXTRA COLUMNS TO THE DATABASE TABLES
|
||||
---------------------------------------------------------------------------
|
||||
It is possible to add extra columns to the entry, repeat, room and users
|
||||
tables, if you need to hold extra information about bookings, rooms and users.
|
||||
For example you might want to add a column in the room table to record whether
|
||||
or not a room has a coffee machine and you might want to record the phone
|
||||
numbers of users. (Note that the users table is only used if you are using
|
||||
the 'db' authentication scheme). Similarly you might want to add a field
|
||||
to the entry and repeat tables to record the number of participants for
|
||||
a meeting.
|
||||
|
||||
To add extra columns, just go into your database administration tool, eg
|
||||
phpMyAdmin, and add the extra columns manually. MRBS will then recognise them
|
||||
and handle them automatically, displaying the information in the lists of rooms
|
||||
and users and allowing you to edit the data in the appropriate forms.
|
||||
|
||||
NOTES:
|
||||
(1) if you are adding a field to the entry table you must add an
|
||||
identical field to the repeat table. If you do not MRBS will fail with
|
||||
a fatal error when you try and run it.
|
||||
(2) names must consist of letters, numbers or underscores. If you are
|
||||
using PostgreSQL then the name must begin with a letter or an underscore.
|
||||
If you are using MySQL then there is no restriction on the first character
|
||||
as long as it is in the permitted set, ie a letter, number or underscore.
|
||||
(Although MySQL will allow other characters in column names, MRBS imposes
|
||||
restrictions on the characters allowed in order to simplify the code. For
|
||||
a technical explanation see below).
|
||||
|
||||
At the moment only text, varchar, date, decimal/numeric, int, smallint and
|
||||
tinyint columns are supported, displayed as textarea, text, date, number
|
||||
or checkbox fields as appropriate. Whether a varchar is displayed as a text
|
||||
or textarea input depends on its maximum length, with the breakpoint
|
||||
determined by a configuration variable. Ints are treated as integer types, as
|
||||
you would expect. However smallints and tinyints are assumed to be booleans
|
||||
and are displayed as checkboxes.
|
||||
|
||||
[Note: smallints are assumed to be booleans because the boolean type in
|
||||
PostgreSQL presents some problems in PHP when trying to process the results
|
||||
of a query in a database independent way, so it is more convenient to use a
|
||||
smallint instead of a boolean in PostgreSQL.]
|
||||
|
||||
Text descriptions are set in the config file using the $vocab_override variable
|
||||
using the appropriate language(s) and with the tag room.column_name,
|
||||
eg room.coffee_machine, or users.phone enabling translations to be provided.
|
||||
If not present, the column name will be used for labels etc. If you are adding
|
||||
columns to the entry and repeat tables then you only need to add the
|
||||
entry.column_name tags: you don't need to add a repeat.column_name tag.
|
||||
|
||||
As an example, to add a field to the room table recording whether or not
|
||||
there is a coffee machine you would, in MySQL, add the column
|
||||
|
||||
coffee_machine tinyint
|
||||
|
||||
to the room table and add the line
|
||||
|
||||
$vocab_override['en']['room.coffee_machine'] = "Coffee machine";
|
||||
|
||||
to the config file and similarly for other languages as required. MRBS
|
||||
should then do the rest and display your coffee machine field on the room
|
||||
pages.
|
||||
|
||||
Extra options for custom fields:
|
||||
|
||||
1)
|
||||
|
||||
You can create dropdown boxes for a custom field by defining an
|
||||
entry in the configuration array $select_options. For example:
|
||||
|
||||
$select_options['entry.conference_facilities'] = array('Video',
|
||||
'Telephone',
|
||||
'None');
|
||||
|
||||
would define the 'conference_facilities' custom field to have three
|
||||
possible values.
|
||||
|
||||
For custom fields only (will be extended later) it is also possible to use
|
||||
an associative array for $select_options, for example
|
||||
|
||||
$select_options['entry.catering'] = array('c' => 'Coffee',
|
||||
's' => 'Sandwiches',
|
||||
'h' => 'Hot Lunch');
|
||||
|
||||
In this case the key (eg 'c') is stored in the database, but the value
|
||||
(eg 'Coffee') is displayed and can be searched for using Search and Report.
|
||||
This allows you to alter the displayed values, for example changing 'Coffee'
|
||||
to 'Coffee, Tea and Biscuits', without having to alter the database. It can also
|
||||
be useful if the database table is being shared with another application.
|
||||
MRBS will auto-detect whether the array is associative.
|
||||
|
||||
2)
|
||||
|
||||
You can specify that a field is mandatory. This will ensure that the
|
||||
user specifies a value for a field that may be empty, like a text box
|
||||
or a selection, as in 1) above. For example:
|
||||
|
||||
$is_mandatory_field['entry.conference_facilities'] = true;
|
||||
|
||||
would define the 'conference_facilities' custom field to be mandatory.
|
||||
In the case of a select field, this adds an empty value to the dropdown
|
||||
list.
|
||||
|
||||
For the entry table only, you can also specify that the field should be mandatory
|
||||
only for specific areas, by setting the value to an array of area IDs. For example:
|
||||
|
||||
$is_mandatory_field['entry.conference_facilities'] = [1, 3];
|
||||
|
||||
would define the 'conference_facilities' custom field to be mandatory only
|
||||
for the areas with ID 1 and 3. For other areas the field will exist but not
|
||||
be mandatory.
|
||||
|
||||
Making a checkbox field mandatory is possible and requires the
|
||||
checkbox to be ticked before the form can be submitted. This can be
|
||||
useful for example for requiring users to accept terms of service or
|
||||
terms and conditions.
|
||||
|
||||
3)
|
||||
|
||||
You can also specify that a field is private, ensuring that the contents
|
||||
are only visible to yourself and the administrators. For example:
|
||||
|
||||
$is_private_field['entry.refreshments'] = true;
|
||||
$is_private_field['users.tel'] = true;
|
||||
|
||||
would prevent the details of your refreshments being visible to other
|
||||
users - provided that private bookings are enabled. See the section
|
||||
on private bookings in systemdefaults.inc.php for more information.
|
||||
Note that private fields are only supported in the entry and users tables.
|
||||
|
||||
4)
|
||||
|
||||
You can also enter regular expressions for validating text field input using
|
||||
the pattern attribute. At the moment this is limited to custom fields in the
|
||||
users table. For example the following could be used to ensure a valid US ZIP
|
||||
code (you might want to have a better regex - this is just for illustration):
|
||||
|
||||
$pattern['users.zip_code'] = "^[0-9]{5}(?:-[0-9]{4})?$";
|
||||
|
||||
You would probably also want to enter a custom error message by using
|
||||
$vocab_override, with the tag consisting of "table.field.oninvalid" eg
|
||||
|
||||
$vocab_override['users.zip_code.oninvalid']['en'] = "Please enter a valid " .
|
||||
"ZIP code, eg '12345' or '12345-6789'";
|
||||
|
||||
|
||||
Technical explanation of the restriction on column names for custom fields
|
||||
--------------------------------------------------------------------------
|
||||
// Column names for custom fields are used by MRBS in a number of ways:
|
||||
// - as the column name in the database
|
||||
// - as part of an HTML name attribute for a form input
|
||||
// - as part of a PHP variable name
|
||||
// - as part of a property name in an iCalendar file
|
||||
//
|
||||
// MySQL, PostgreSQL, HTML and PHP all have different rules for these tokens:
|
||||
// - MySQL: almost anything is allowed except that:
|
||||
// - "No identifier can contain ASCII NUL (0x00) or a byte with a value
|
||||
// of 255."
|
||||
// - "Database, table, and column names should not end with space
|
||||
// characters."
|
||||
// (http://dev.mysql.com/doc/refman/5.0/en/identifiers.html)
|
||||
//
|
||||
// - PostgreSQL: "SQL identifiers and key words must begin with a letter (a-z,
|
||||
// but also letters with diacritical marks and non-Latin letters) or an
|
||||
// underscore (_). Subsequent characters in an identifier or key word can
|
||||
// be letters, underscores, digits (0-9), or dollar signs ($). Note that
|
||||
// dollar signs are not allowed in identifiers according to the letter of the
|
||||
// SQL standard, so their use may render applications less portable. The SQL
|
||||
// standard will not define a key word that contains digits or starts or ends
|
||||
// with an underscore, so identifiers of this form are safe against possible
|
||||
// conflict with future extensions of the standard."
|
||||
// (http://www.postgresql.org/docs/8.1/interactive/sql-syntax.html#SQL-SYNTAX-IDENTIFIERS)
|
||||
//
|
||||
// - PHP: "Variable names follow the same rules as other labels in PHP. A
|
||||
// valid variable name starts with a letter or underscore, followed by any
|
||||
// number of letters, numbers, or underscores. As a regular expression, it
|
||||
// would be expressed thus: '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*' "
|
||||
// (http://php.net/manual/en/language.variables.basics.php)
|
||||
//
|
||||
// - HTML: "ID and NAME tokens must begin with a letter ([A-Za-z]) and may be
|
||||
// followed by any number of letters, digits ([0-9]), hyphens ("-"),
|
||||
// underscores ("_"), colons (":"), and periods (".")."
|
||||
// (http://www.w3.org/TR/html401/types.html#type-cdata)
|
||||
//
|
||||
// - iCalendar: A name consists of ALPHA / DIGIT / "-" characters.
|
||||
//
|
||||
// In order to avoid complications with using names in each of these contexts,
|
||||
// we restrict custom field names to the set of names which conforms to all
|
||||
// five rules, taking into account the fact that when MRBS uses column names
|
||||
// in PHP and HTML it always prefixes them with a string beginning with a letter.
|
||||
// This gives us the rule that custom field names must consist of letters,
|
||||
// numbers or underscores (which are converted to hyphens when exported to an
|
||||
// iCalendar file).
|
||||
|
||||
|
||||
CONFIGURING MRBS
|
||||
---------------------------------------------------------------------------
|
||||
Next, you will need to create and customize the file "config.inc.php"...
|
||||
|
||||
First of all copy the file config.inc.php-sample to config.inc.php. Then you
|
||||
need to edit the settings in that file and add other settings as required.
|
||||
|
||||
As a minimum you will need to set the timezone and the database variables.
|
||||
Other settings can be changed by copying lines from systemdefaults.inc.php and
|
||||
areadefaults.inc.php and pasting them into config.inc.php. Do not edit
|
||||
systemdefaults.inc.php or areadefaults.inc.php as it will make it harder for
|
||||
you to upgrade when new versions are released.
|
||||
|
||||
Note that there are two different defaults files (systemdefaults.inc.php and
|
||||
areadefaults.inc.php) to draw attention to the fact that the settings in
|
||||
areadefaults just determine the settings for NEW areas. Settings for existing
|
||||
areas are set using a web browser by following the "Rooms" link in MRBS. (It
|
||||
can be a little frustrating editing the area settings to find that they have no
|
||||
effect on existing areas!)
|
||||
|
||||
1. Timezone
|
||||
|
||||
You must set the timezone to a valid value, a list of which can be found at
|
||||
http://php.net/manual/timezones.php. Don't forget to uncomment the line
|
||||
by removing the '//' at the beginning. Note that the timezone to use is the
|
||||
timezone in which your meeting rooms are located, not the timezone of your
|
||||
server in case they are different.
|
||||
|
||||
However, if you already have bookings in your system which were made under
|
||||
a different timezone (perhaps if you are upgrading from a previous version
|
||||
of MRBS where the timezone wasn't set explicitly and the timezone defaulted to
|
||||
that of the server) then you have two choices:
|
||||
|
||||
(a) to set the timezone to be the same as the previous timezone. This will
|
||||
ensure that all your existing bookings still appear correctly, but you will
|
||||
have to continue to put up with some minor inconveniences. For example
|
||||
"Go to Today" will not always go to the today for your rooms, if you happen
|
||||
to be using MRBS at a time of day when the rooms are on one day but the
|
||||
timezone you have selected is on the day before or after. Also if you are
|
||||
using the min and max book ahead facility then you will find that this is out
|
||||
by the difference between the timezone of your rooms and the timezone you have
|
||||
chosen.
|
||||
|
||||
(b) to set the timezone to the timezone of your rooms, having first corrected
|
||||
the start and end times of all your existing bookings, in both the entry and
|
||||
repeat tables. This is not a trivial exercise and you should back up your
|
||||
database before starting. Note also that it is not necessarily as simple as
|
||||
adding or subtracting a fixed number of hours to existing bookings since the
|
||||
dates at which your rooms change between summer and winter time may be different
|
||||
to the dates at which your previous timezone made the DST change. This can
|
||||
happen for example if your rooms are in Europe and your server is in the USA,
|
||||
as there is usually a week when Europe has changed but the USA has not.
|
||||
|
||||
|
||||
2. Database Settings:
|
||||
|
||||
First, select your database system. Define one of the following:
|
||||
|
||||
$dbsys = "mysql";
|
||||
$dbsys = "pgsql";
|
||||
|
||||
Then define your database connection parameters. Set the values for:
|
||||
|
||||
$db_host = The hostname that the database server is running on.
|
||||
$db_database = The name of the database containing the MRBS tables.
|
||||
$db_login = The database login username
|
||||
$db_password = The database login password for the above login username
|
||||
|
||||
If you are using cPanel on your web server, make sure you include the prefix,
|
||||
typically 8 characters followed by an underscore, in your database name and
|
||||
database username. For example $db_database = "abcdefgh_mrbs". (Note: this
|
||||
prefix is not the same as the table prefix below.)
|
||||
|
||||
If the database server and web server are the same machine, use
|
||||
$db_host="localhost". Or, with PostgreSQL only, you can use $db_host="" to
|
||||
use Unix Domain Sockets to connect to the database server on the same machine.
|
||||
|
||||
By default, MRBS will not use PHP persistent (pooled) database connections.
|
||||
Persistent connections can sometimes give better performance, but they can
|
||||
also cause problems with transactions and locks. For more details see
|
||||
http://php.net/manual/en/features.persistent-connections.php Although
|
||||
MRBS is designed to work with persistent connections we recommend that you
|
||||
don't use them unless they give a significant performance boost. To use
|
||||
persistent connections set
|
||||
|
||||
$db_persist = TRUE;
|
||||
|
||||
If you want to install multiple sets of mrbs tables when only one
|
||||
SQL database is available, or resolve table name conflicts, you have
|
||||
to change the prefix of "mrbs_" for the tables in your database,
|
||||
then you will need to set the value of:
|
||||
|
||||
$db_tbl_prefix = The table name prefix
|
||||
|
||||
|
||||
3. Other Settings
|
||||
|
||||
Now go through systemdefaults.inc.php and areadefaults.inc.php and see which other
|
||||
configuration settings you would like to change. Do this by copying them to
|
||||
config.inc.php and changing them there. This should make the task of upgrading to
|
||||
new releases easier as all your site-specific configuration changes will be confined
|
||||
to config.inc.php.
|
||||
|
||||
There is a wide variety of settings that can be changed, including
|
||||
|
||||
- site identification information
|
||||
- themes
|
||||
- calendar settings
|
||||
- booking policies
|
||||
- display and time format settings
|
||||
- private bookings settings
|
||||
- provisional bookings settings
|
||||
- authentication settings
|
||||
- email settings
|
||||
- language settings
|
||||
- report settings
|
||||
- entry types
|
||||
|
||||
The comments in the systemdefaults.inc.php and areadefaults.inc.php files should
|
||||
explain the purpose of the various configuration variables and how to change them.
|
||||
(Note that some of the settings can be set on a per-area basis through the area
|
||||
administration page in MRBS. In this case the setting in the areadefaults.inc.php
|
||||
and config.inc.php files defines the default settings for new areas.)
|
||||
|
||||
The Help information is held in the site_faq files, one per language. You may well
|
||||
want to customise it by editing the files.
|
||||
|
||||
|
||||
CHANGING TEXT STRINGS
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
All the text strings used by MRBS, except those used on the Help page, are held in a
|
||||
series of lang.* files, for example lang.en for English, or lang.fr for French. They
|
||||
are overridden by the $vocab_override array in the config file. If you want to change
|
||||
some of the text strings used by MRBS, for example change "Room" to "Computer", then
|
||||
look for the appropriate string in the lang files, remember its tag and set the
|
||||
$vocab_override variable in the config file. For example
|
||||
|
||||
$vocab_override['en']['ctrl_click'] = "Use Control-Click to select more than one computer";
|
||||
|
||||
Doing it this way, rather than editing the lang files, will mean that when you upgrade to
|
||||
the next version of MRBS you will not have to re-edit the lang files.
|
||||
|
||||
|
||||
INTERNATIONALISATION
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
From MRBS 1.4.6, MRBS is internationalised and uses UTF-8 throughout.
|
||||
MRBS will serve all of its pages in UTF-8 and stores everything in the
|
||||
database in UTF-8. This means that all languages work together.
|
||||
|
||||
For MRBS to use Unicode, PHP must be built with 'iconv' support
|
||||
('--with-iconv' directive), or have the iconv extension installed
|
||||
and enabled. On Windows, if you are using PHP 5, iconv support is built-in.
|
||||
|
||||
|
||||
PERIODS
|
||||
---------------------------------------------------------------------------
|
||||
When using periods, MRBS stores bookings internally as one minute slots
|
||||
starting at 1200. If once you have had your system up and running for
|
||||
a while find that you need to add a new period then you will need to
|
||||
adjust the times of your existing bookings unless your new period is at
|
||||
the end of the day.
|
||||
|
||||
The simplest example is when you want to add a new period at the start of
|
||||
the day. In this case you will need to run some SQL using phpMyAdmin or
|
||||
a similar program to move all bookings one minute later. In this example
|
||||
the SQL required would be
|
||||
|
||||
UPDATE mrbs_entry SET start_time=start_time+60, end_time=end_time+60;
|
||||
UPDATE mrbs_repeat SET start_time=start_time+60, end_time=end_time+60,
|
||||
end_date=end_date+60;
|
||||
|
||||
having changed "mrbs_" to your table prefix if necessary.
|
||||
|
||||
Before running this SQL you should of course backup your database.
|
||||
|
||||
|
||||
MULTISITE
|
||||
---------------------------------------------------------------------------
|
||||
MRBS can be run in multisite mode by setting in the config file
|
||||
|
||||
$multisite = true;
|
||||
|
||||
In multisite mode a single instance of the MRBS code can serve a number of
|
||||
sites. Each site has its own config file in the sites/<sitename> directory
|
||||
which will override any settings in the global config file. At a minimum
|
||||
the local config file will contain a table prefix for the site, but can also
|
||||
contain any other config settings including a theme and custom CSS. The sites
|
||||
are reached by specifying the sitename in the query string, eg
|
||||
|
||||
index.php?site=sitename
|
||||
|
||||
Individual sites can even have their own authentication methods, but note that
|
||||
authentication against WordPress multisite is not supported.
|
||||
|
||||
|
||||
SECURITY NOTES!
|
||||
---------------------------------------------------------------------------
|
||||
You can configure your web server so that users can not obtain the ".inc"
|
||||
files but this is not essential, since critical files containing your
|
||||
database login and password use a ".php" extension like config.inc.php.
|
||||
See your web server documentation on how to do this.
|
||||
|
||||
There are example Apache .htaccess files included, for different versions of
|
||||
Apache, but Apache might ignore a .htaccess file in your MRBS directory
|
||||
due to the setting of the "AllowOverride" directive in your web server
|
||||
configuration. Either change "AllowOverride None" to "AllowOverride Limit",
|
||||
or create a new <Directory> entry with the contents of the .htaccess example
|
||||
file in it for your MRBS installation. Then read the Apache docs five or six
|
||||
times, until you know what you just did.
|
||||
|
||||
You may protect "config.inc.php" to only allow the web server to read it.
|
||||
For example: # chown httpd config.inc.php; chmod 400 config.inc.php
|
||||
|
||||
The script "testdata.php" is for testing only. Do not leave it in a
|
||||
directory accessible to your web server. Anyone running this will add a
|
||||
large number of test entries to your database, regardless of
|
||||
authentication, and book all your rooms to people you've never heard of.
|
||||
@@ -0,0 +1,36 @@
|
||||
MRBS Language Support
|
||||
|
||||
Each "lang.*" file (in the web/lang/ directory) contains the text strings
|
||||
for a language. Not all of these files are complete. Note that MRBS always
|
||||
reads a reference file (which defaults to "lang.en") first, before reading
|
||||
another language file.
|
||||
|
||||
Porting MRBS to another language is relatively easy. From the Admin page,
|
||||
you will see "Your browser is set to use "xx" language". If no "lang.xx"
|
||||
file is available, copy "lang.en" to "lang.xx" (replace xx with the code
|
||||
you found in Admin), and then edit the file to change the English strings
|
||||
to your language. For example, you might
|
||||
change: $vocab["gototoday"] = "Go To Today";
|
||||
to: $vocab["gototoday"] = "gehe zum heutigen Tag";
|
||||
|
||||
You will then find that MRBS starts to use your language file
|
||||
automatically. If you are having trouble getting this to work, see the
|
||||
language section of config.inc.php.
|
||||
|
||||
If you would like to complete or improve an existing language file, the
|
||||
"checklang.php" script included in the distribution can help. Copy this
|
||||
to your web server area.
|
||||
Now use your browser to access:
|
||||
http://your-host-and-dir/checklang.php?lang=xx
|
||||
(where "xx" is the code of the language file you want to check). This
|
||||
will report missing or untranslated strings in "lang.xx" compared to
|
||||
"lang.en". If you omit the parameter and access:
|
||||
http://your-host-and-dir/checklang.php
|
||||
you will see the results of checking all available language files.
|
||||
|
||||
If you do create or improve a language file, please post a message about
|
||||
it on the mailing list, so it can be included in future releases. Thanks!
|
||||
|
||||
Important note: You should not use any HTML entities in the translation
|
||||
other than . If you need to use any other characters you should
|
||||
just encode those characters in utf-8.
|
||||
@@ -0,0 +1,158 @@
|
||||
MRBS is supplied under the GNU GENERAL PUBLIC LICENSE, Version 2, June 1991.
|
||||
See COPYING for details.
|
||||
|
||||
In addition, MRBS uses a number of third-party components and their license
|
||||
terms are reproduced here.
|
||||
|
||||
|
||||
CAS
|
||||
===
|
||||
Copyright 2007-2015, JA-SIG, Inc.
|
||||
This project includes software developed by Jasig.
|
||||
http://www.jasig.org/
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this software except in compliance with the License.
|
||||
You may obtain a copy of the License at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
DATATABLES
|
||||
==========
|
||||
|
||||
MIT license
|
||||
|
||||
Copyright (C) 2008-2018, SpryMedia Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
FLATPICKR
|
||||
=========
|
||||
|
||||
Released under the MIT license. See web/js/flatpickr/LICENSE.md.
|
||||
|
||||
|
||||
JQUERY
|
||||
======
|
||||
|
||||
Released under the MIT license
|
||||
Copyright JS Foundation and other contributors
|
||||
https://jquery.org/license
|
||||
|
||||
|
||||
JQUERY UI
|
||||
=========
|
||||
|
||||
Released under the MIT license. See web/jquery/ui/LICENSE.txt.
|
||||
|
||||
|
||||
JQUERY-VISIBLE
|
||||
==============
|
||||
|
||||
A small plugin that checks whether elements are within the user visible
|
||||
viewport of a web browser. Only accounts for vertical position, not
|
||||
horizontal.
|
||||
|
||||
Copyright 2012, Digital Fusion
|
||||
Licensed under the MIT license.
|
||||
http://teamdf.com/jquery-plugins/license/
|
||||
|
||||
|
||||
JQUERY.REDIRECT
|
||||
===============
|
||||
A simple HTTP POST and GET Redirection Plugin for jQuery
|
||||
https://github.com/mgalante/jquery.redirect
|
||||
|
||||
Copyright (c) 2013-2022 Miguel Galante
|
||||
Copyright (c) 2011-2013 Nemanja Avramovic, www.avramovic.info
|
||||
|
||||
Licensed under CC BY-SA 4.0 License: http://creativecommons.org/licenses/by-sa/4.0/
|
||||
|
||||
|
||||
PHPMAILER
|
||||
=========
|
||||
|
||||
This is supplied under the GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1,
|
||||
February 1999. See web/PHPMailer/LICENSE.
|
||||
|
||||
|
||||
QR CODE
|
||||
=======
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Smiley <smiley@chillerlan.net>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
|
||||
SELECT2
|
||||
=======
|
||||
|
||||
Released under the MIT license. See web/jquery/select2/LICENSE.md
|
||||
|
||||
|
||||
Webklex/PHPIMAP
|
||||
===============
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2016 Webklex
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,56 @@
|
||||
Meeting Room Booking System
|
||||
http://mrbs.sourceforge.net/
|
||||
-------------------------------
|
||||
|
||||
The Meeting Room Booking System (MRBS) is a PHP-based application for
|
||||
booking meeting rooms (surprisingly!). I got annoyed with the piles of books
|
||||
which were being used to book meetings. They were slow, hard to edit and only
|
||||
at the reception desk. I thought that a nice web-based system would be much
|
||||
nicer.
|
||||
|
||||
Some parts of this are based on WebCalender 0.9.4 by Craig Knudsen
|
||||
(http://www.radix.net/~cknudsen/webcalendar/) but there is now very little
|
||||
which is similar. There are fundamental design goal differences between
|
||||
WebCalendar and MRBS - WC is for individuals, MRBS is for meeting rooms.
|
||||
|
||||
------
|
||||
To Use
|
||||
------
|
||||
See the INSTALL file for installation instructions.
|
||||
|
||||
Once it's installed try going to http://yourhost/mrbs/
|
||||
|
||||
If you're using the default authentication type ('db') the first thing you'll
|
||||
be prompted to do is to create an admin user. Once you've done that you'll
|
||||
need to login using the credentials you've just specified.
|
||||
|
||||
Once you have logged in as an administrator you can click on "Rooms" and
|
||||
create first an "Area", and then a "Room" within that area.
|
||||
|
||||
There are other ways to configure authentication in MRBS, see the
|
||||
file AUTHENTICATION for a more complete description.
|
||||
|
||||
It should be pretty easy to adjust it to your corporate colours - you can
|
||||
modify the themes under "Themes" or (preferably) copy an existing theme
|
||||
to a new directory and modify the new theme.
|
||||
|
||||
See LICENSE for licensing info.
|
||||
|
||||
See NEWS for a history of changes.
|
||||
|
||||
See AUTHENTICATION for information about user authentication/passwords.
|
||||
|
||||
-------------
|
||||
Requirements:
|
||||
-------------
|
||||
- PHP 7.2 or above with MySQL and/or PostgreSQL support
|
||||
- MySQL (5.5.3 and above) or PostgreSQL 8.2 or above.
|
||||
- Any web server that is supported by PHP
|
||||
|
||||
Recommended:
|
||||
- JavaScript-enabled browser
|
||||
- PHP module connection to the server (also called SAPI) if you want to use any
|
||||
of the basic http authentication schemes provided.
|
||||
|
||||
(If you are considering porting MRBS to another database, see README.sqlapi)
|
||||
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
README.sqlapi - Database abstraction class for MRBS
|
||||
-----------------------------------------------------------------------------
|
||||
MRBS utilises a database abstraction class, currently implemented for
|
||||
MySQL (>= 5.1) and PostgreSQL (>= 8.2). It was written for MRBS but may
|
||||
be useful in other applications.
|
||||
|
||||
The class supports multiple connections to arbitrary databases,
|
||||
but there is also a simple wrapper function to allow use of the default
|
||||
MRBS database without the user always passing a database object
|
||||
around. This is the function "db()" defined in dbsys.inc.
|
||||
|
||||
The class supports multiple pending results for each connection. It
|
||||
can be configured to use PHP persistent (pooled) database connections,
|
||||
or normal (single use) connections.
|
||||
|
||||
CAUTION: Before using PHP persistent database connections with PostgreSQL,
|
||||
be sure your PostgreSQL postmaster can support enough backends. In theory,
|
||||
and to be completely safe, it needs to be able to support at least as many
|
||||
concurrent connections as your Apache "MaxClients" setting times the number
|
||||
of unique persistent connection strings (PostgreSQL conninfo's, unique
|
||||
combinations of user/password/database) implemented on your site. Note that
|
||||
the default for PostgreSQL is a maximum of 32 connections, and the default
|
||||
for Apache MaxClients is 150. If you want to use persistent connections,
|
||||
see the $persist parameter to DBFactory::create() below.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
To use this package, include "dbsys.inc" after defining the following
|
||||
variables:
|
||||
$dbsys = The database abstraction to use, 'mysql' or 'pgsql'
|
||||
$db_host = The hostname of the database server, or "localhost"
|
||||
$db_login = The username to use when connecting to the database
|
||||
$db_password = The database account password
|
||||
$db_database = The database name
|
||||
Optionally, you can define:
|
||||
$db_persist = true;
|
||||
if you want to use persistent connections.
|
||||
|
||||
If using PostgreSQL, and the database server is on the same host as the web
|
||||
server, you can specify $db_host="localhost" to use TCP, or $db_host="" to
|
||||
use Unix Domain Sockets. Generally this won't make much difference, but if
|
||||
your server runs without the -i option, it will only accept Unix Domain
|
||||
Socket connections, so you must use $db_host="".
|
||||
|
||||
After your script includes the file, you can get the default database
|
||||
connection object by calling db().
|
||||
|
||||
If an error occurs while trying to connect, a message will be output
|
||||
followed by a PHP exit.
|
||||
|
||||
The way MRBS uses this is to define a configuration file config.inc.php with
|
||||
the above variables plus:
|
||||
$dbsys = "pgsql"; // or: $dbsys = "mysql";
|
||||
Then, each PHP script which wants to connect to the database starts with:
|
||||
include "config.inc.php";
|
||||
include "dbsys.inc";
|
||||
If you do this, be sure the web server will not serve config.inc.php to
|
||||
clients, for security reasons.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
Notes on improving SQL portability:
|
||||
|
||||
+ Use standard SQL-92 as much as possible.
|
||||
+ Where it is not possible to use SQL-92, use or implement an sql_syntax_*
|
||||
function which hides the database differences (see below).
|
||||
+ Don't use SQL-92 reserved words as column or table names.
|
||||
+ Use PHP functions rather than database functions where practical.
|
||||
+ Don't reply on specific formats for output of DATETIME types.
|
||||
+ Don't quote numeric type values in SQL statements.
|
||||
|
||||
SQL-92 standard things to avoid because they cause trouble in MySQL:
|
||||
+ Double quoted identifiers: SELECT "MY COLUMN" from "MY TABLE"...
|
||||
+ The string concatenation operator ||
|
||||
+ Subselects
|
||||
|
||||
SQL-92 standard things to avoid because they cause trouble in PostgreSQL:
|
||||
+ Outer joins.
|
||||
+ "table1 JOIN table2" syntax; use WHERE clause joins instead.
|
||||
|
||||
Non-standard features used, available in both PostgreSQL and MySQL (this
|
||||
information is provided for anyone attempting to port MRBS to another
|
||||
database system):
|
||||
+ MySQL implicitly assigns "DEFAULT current_timestamp" to a timestamp
|
||||
column; this must be done explicitly in other database systems.
|
||||
+ The column called TIMESTAMP is not legal in SQL-92. It would be legal
|
||||
if double-quoted in SQL statements, but MySQL doesn't like that.
|
||||
Changing the column name would break existing databases, and it turns
|
||||
out both PostgreSQL and MySQL accept this, so it has been kept.
|
||||
+ Auto-commit is assumed. The database wrappers have begin/end calls to
|
||||
bracket transactions, but MRBS generally uses them only to improve
|
||||
performance with grouped inserts/deletes/updates. It is assumed that
|
||||
a single insert/delete/update SQL statement commits right away. If
|
||||
a database doesn't implement this, it may be possible to incorporate
|
||||
this into sql_command(), which is used for all data modification.
|
||||
+ Portable use of auto-incrementing fields (PostgreSQL SERIAL, MySQL
|
||||
AUTO_INCREMENT) requires that:
|
||||
* Only one auto-increment field allowed per table; must be primary key.
|
||||
* Use sql_insert_id() to retrieve the value after INSERT.
|
||||
* Don't assume the value will either be MAX(field)+1, like MySQL,
|
||||
or always incremented, like PostgreSQL. These can be different
|
||||
when records have been deleted.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
The database class methods are documented here:
|
||||
|
||||
To make a new connection to a database, use the method DBFactory::create(), as:
|
||||
|
||||
DBFactory::create($db_system,
|
||||
$db_host,
|
||||
$db_username,
|
||||
$db_password,
|
||||
$db_name,
|
||||
$persist = 0,
|
||||
$db_port = null)
|
||||
Here $db_system is either 'mysql' or 'pgsql' and $db_name is the name of
|
||||
the database to access. This method returns an object of the class "DB".
|
||||
|
||||
The "DB" class has the following object methods:
|
||||
|
||||
->command($sql, $params)
|
||||
Execute a non-SELECT SQL command (for example: insert, update, delete).
|
||||
Returns the number of tuples affected if OK (a number >= 0).
|
||||
Raises a "DBException" exception on error.
|
||||
|
||||
->query($sql, $params)
|
||||
Execute an SQL query. Returns an object of class "DBStatement" (see methods further below).
|
||||
|
||||
->query1($sql, $params)
|
||||
Execute an SQL query which should return a single non-negative number value.
|
||||
Returns the value of the single column in the single row of the query |
|
||||
result or -1 if the query returns no result, or a single NULL value, such as from
|
||||
a MIN or MAX aggregate function applied over no rows.
|
||||
Raises a "DBException" exception on error.
|
||||
This is a short-cut alternative to ->query(), good for use with count(*)
|
||||
and similar queries.
|
||||
|
||||
->insert_id($table, $fieldname)
|
||||
Return the value of an autoincrement/serial field from the last insert.
|
||||
This must be called right after the insert on that table. The $fieldname
|
||||
is the name of the autoincrement or serial field in the table. The
|
||||
return result will be correct even if other processes are updating the
|
||||
database at the same time.
|
||||
NOTE: To make this work with different DBMS's, the field name must be
|
||||
specified, and it must name the only autoincrement/serial field in the
|
||||
row inserted by the most recent INSERT.
|
||||
|
||||
->error()
|
||||
Return the text of the last error message.
|
||||
|
||||
->begin()
|
||||
Begin a transaction, if the database supports it. This is used to
|
||||
improve performance for multiple insert/delete/updates on databases
|
||||
which support transactions, and using it is not required. Do
|
||||
not attempt to have both ->begin() and ->mutex_lock() active since
|
||||
then both may be implemented with a shared underlying mechanism.
|
||||
|
||||
->commit()
|
||||
Commit (end) a transaction. See ->begin().
|
||||
|
||||
->rollback()
|
||||
Rollback a transaction. See ->begin().
|
||||
|
||||
->mutex_lock($name)
|
||||
Acquire a mutual-exclusion lock on the named table. For portability:
|
||||
* This will not lock out SELECTs.
|
||||
* It may lock out DELETE/UPDATE/INSERT or it may not.
|
||||
* It will lock out other callers of this routine with the same name
|
||||
argument (which is the main reason for using it).
|
||||
* It may timeout in 20 seconds and return 0, or may wait forever.
|
||||
* It returns 1 when the lock has been acquired.
|
||||
* Caller must release the lock with sql_mutex_unlock().
|
||||
* Caller must not have more than one mutex lock at any time.
|
||||
You should be sure to release the lock with sql_mutex_unlock() before the
|
||||
script exits, although this function also establishes a shutdown handler to
|
||||
automatically release the lock if the script exits. (With persistent
|
||||
connections, the locks would not otherwise be released on exit, and a
|
||||
deadlock will occur.)
|
||||
This call effectively calls ->begin(), so do not use it inside an
|
||||
->begin()/->end() block, nor use ->begin() between calls to
|
||||
->mutex_lock() and ->mutex_unlock().
|
||||
|
||||
->mutex_unlock($name)
|
||||
Release a mutual-exclusion lock on the named table. See ->mutex_lock().
|
||||
This also effectively calls ->commit().
|
||||
|
||||
->version()
|
||||
Return a string identifying the database system and version.
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
The following ->syntax_* methods are intended to help you build up SQL
|
||||
statements using non-standard features. Each returns a portion of SQL (with
|
||||
leading and trailing spaces) which implements the named non-standard feature
|
||||
for the selected database. Some methods must also be passed (by reference) an
|
||||
array object for building the SQL parameters to pass to the query/command method.
|
||||
|
||||
->syntax_limit($count, $offset)
|
||||
Generate non-standard SQL for LIMIT clauses, to make the query return
|
||||
no more than $count records, starting at position $offset (basis 0).
|
||||
|
||||
->syntax_timestamp_to_unix($fieldname)
|
||||
Generate non-standard SQL to output a TIMESTAMP as a Unix time_t. The
|
||||
argument must be the name of a timestamp field.
|
||||
|
||||
->syntax_caseless_contains($fieldname, $s, &$params)
|
||||
Generate a non-standard SQL predicate clause which will be true if the
|
||||
string $s is contained anywhere in the named field, using case insensitive
|
||||
string compare. This uses LIKE or Regular Expression matching, depending
|
||||
on the database system. This method modifies the passed $params array
|
||||
to add the appropriate SQL parameters.
|
||||
|
||||
->syntax_casesensitive_equals($fieldname, $string, &$params)
|
||||
Generates a non-standard SQL predicate clause for a case-sensitive equals.
|
||||
This method modifies the passed $params array to add the appropriate
|
||||
SQL parameters.
|
||||
|
||||
->syntax_addcolumn_after($fieldname)
|
||||
Generate non-standard SQL to add a table column after another specified
|
||||
column.
|
||||
|
||||
->syntax_createtable_autoincrementcolumn()
|
||||
Generate non-standard SQL to specify a column as an auto-incrementing
|
||||
integer while doing a CREATE TABLE.
|
||||
|
||||
->syntax_bitwise_xor()
|
||||
Returns the syntax for a bitwise XOR operator.
|
||||
|
||||
Example usage:
|
||||
$sql = "SELECT * FROM mytable ORDER BY id" . $db_obj->syntax_limit(100,20);
|
||||
With PostgreSQL this gives you:
|
||||
$sql = "SELECT * FROM mytable ORDER BY id LIMIT 100 OFFSET 20";
|
||||
With MySQL this gives you:
|
||||
$sql = "SELECT * FROM mytable ORDER BY id LIMIT 20,100";
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
DBStatement methods:
|
||||
|
||||
->row($rownumber)
|
||||
Return a row from a result. The first row is row number 0.
|
||||
The row is returned as an array with index 0=first column, etc.
|
||||
When called with i >= number of rows in the result, returns 0 to signify
|
||||
the end of the result set. This is designed to be used in a loop
|
||||
like this to retrieve all the rows:
|
||||
|
||||
for ($i = 0; (($row = $stmt->row($r, $i)); $i++) { ... process the row ... }
|
||||
|
||||
->row_keyed($rownumber)
|
||||
Return a row from a result. The first row is row number 0.
|
||||
The row is returned as an associative array with column (field) names as
|
||||
the indexes. (PHP also makes numeric indexes for the same data.)
|
||||
When called with i >= number of rows in the result, returns 0 to signify
|
||||
the end of the result set. This is designed to be used in a loop
|
||||
like this to retrieve all the rows:
|
||||
|
||||
for ($i = 0; (($row = $stmt->row_keyed($i)); $i++) { ... }
|
||||
|
||||
NOTE: You should explicitly name each column in your SQL statement which
|
||||
is not a simple field name, because databases differ in how they assume
|
||||
a default name. For example, don't use ->row_keyed() on a query
|
||||
like: SELECT name, COUNT(*) FROM ...
|
||||
Instead use: SELECT name, COUNT(*) AS totals FROM ...
|
||||
so you can reliably refer to the count as row["totals"].
|
||||
|
||||
->all_rows_keyed
|
||||
Return all the rows from a statement object, as an array of arrays
|
||||
keyed on the column name.
|
||||
|
||||
->count()
|
||||
Returns the number of rows returned by the statement.
|
||||
|
||||
->num_fields()
|
||||
Returns the number of columns/fields returned by the statement.
|
||||
@@ -0,0 +1,150 @@
|
||||
Upgrade Information for previous releases of MRBS:
|
||||
--------------------------------------------------
|
||||
|
||||
If you are upgrading from MRBS 1.2-pre3 or later, then MRBS will
|
||||
automatically execute any necessary database upgrades when it is first
|
||||
run. It will prompt you for a database (not MRBS) username and password
|
||||
with rights to create and alter tables.
|
||||
|
||||
It would be a sensible precaution to take a backup of your database before
|
||||
the upgrade.
|
||||
|
||||
1. Take a backup of your database, just in case.
|
||||
2. Take a backup copy of your existing mrbs directory on your web server.
|
||||
3. Upload all the files and directories, except the config file, in the web
|
||||
directory of the release to a new directory on your server. Copy your
|
||||
config.inc.php file from your old directory to the new one. Note that
|
||||
if you are upgrading from MRBS 1.4.7 or earlier, the structure of the
|
||||
config file has changed and you should create a new config file based
|
||||
on config.inc.php-sample.
|
||||
4. Go to MRBS in your browser. If a database upgrade is required, you'll be
|
||||
prompted for a database (note database, not MRBS) username and password.
|
||||
5. Rename your directories so that the new one becomes the working one.
|
||||
|
||||
MRBS database upgrades are in general not backwards compatible, ie you won't
|
||||
be able to run an older version of MRBS against a later version of the
|
||||
database. You may therefore choose to make a copy of the database for test
|
||||
purposes and check that the upgrade process works before performing the
|
||||
upgrade on your production database.
|
||||
|
||||
See the advice in INSTALL about potentially creating a fresh "config.inc.php"
|
||||
when you upgrade MRBS, especially for a major version change.
|
||||
|
||||
Upgrading from prior to MRBS 1.8.0
|
||||
==================================
|
||||
|
||||
The following configuration settings have changed:
|
||||
$area_list_format Redundant. All area and room lists are now select elements.
|
||||
$display_calendar_bottom Redundant. The mini-calendars have moved.
|
||||
$max_slots Redundant. The code has been rewritten.
|
||||
$simple_trailer Redundant. There is no longer a trailer!
|
||||
|
||||
The following $strftime_format settings have changed:
|
||||
'day_month' redundant
|
||||
'dayname_cal' replaced by 'minical_dayname'
|
||||
'month_cal' replaced by 'minical_monthname'
|
||||
'monthyear' replaced by 'view_month'
|
||||
|
||||
|
||||
Upgrading from prior to MRBS 1.7.1
|
||||
==================================
|
||||
|
||||
The $year_range configuration setting has been abandoned. If you have
|
||||
it in your config file it won't do anything.
|
||||
|
||||
|
||||
Upgrading from prior to MRBS 1.7.0
|
||||
==================================
|
||||
|
||||
As a security measure, custom HTML for areas and rooms has been disabled by
|
||||
default, since it could be used to insert malicious JavaScript. However, if
|
||||
you trust your admins you can re-enable it by setting the following in the
|
||||
config file:
|
||||
|
||||
$auth['allow_custom_html'] = true;
|
||||
|
||||
|
||||
Upgrading from prior to MRBS 1.6.0
|
||||
==================================
|
||||
|
||||
If you upgrade to MRBS 1.6.0 and use your old config.inc.php file, you must
|
||||
add a line near to the top of the file, just after the <?php tag, to make
|
||||
the file read:
|
||||
|
||||
<?php
|
||||
namespace MRBS;
|
||||
|
||||
|
||||
Upgrading from prior to MRBS 1.5.0
|
||||
==================================
|
||||
|
||||
MRBS's default authentication scheme changed from 'config' to 'db' with
|
||||
the release of MRBS 1.5.0. If you had previously used the 'config' scheme
|
||||
without specifically stating this in your config.inc.php you will need
|
||||
to make a change to your config.inc.php after upgrading to MRBS 1.5.0. The
|
||||
change you need is:
|
||||
|
||||
$auth["type"] = "config";
|
||||
|
||||
|
||||
Upgrading from prior MRBS 1.4.9
|
||||
===============================
|
||||
|
||||
MRBS now supports the $vocab_override config variable. See
|
||||
systemdefaults.inc.php for more details. If you have customised your version
|
||||
of MRBS by editing the lang files, you are advised to use $vocab_override instead.
|
||||
This will make future upgrades easier.
|
||||
|
||||
|
||||
Upgrading from prior MRBS 1.4.6
|
||||
===============================
|
||||
|
||||
If you were previously using MRBS with $unicode_encoding set to 0, when
|
||||
you upgrade to 1.4.6 you _MUST_ upgrade the MySQL database from the
|
||||
previously used character set to Unicode. Note that it is extremely
|
||||
unlikely that you will need to do this as the default setting of
|
||||
$unicode_encoding is 1.
|
||||
|
||||
If you do need to convert text in the database you should run the
|
||||
convert_db_to_utf8.php script _BEFORE_ upgrading to the latest version
|
||||
of MRBS. The administrator should copy the file into the web directory,
|
||||
run it (choosing the encoding to convert from) ONCE, and then move it back
|
||||
out of the web directory. We recommend you backup your database before
|
||||
running this script if you are at all worried. Running it more than once
|
||||
will make a right mess of any non-ASCII text in the database.
|
||||
|
||||
Additionally, this script can correct an MRBS database that used to run on
|
||||
an old version of MySQL (earlier than 4.1), but that now runs on a newer
|
||||
version of MySQL. In this case, the database contains UTF-8 text, but the
|
||||
tables are considered to be in some other encoding by MySQL, generally
|
||||
Latin-1. The convert_db_to_utf8.php detects this condition, and offers
|
||||
the administrator the chance to correct the database 'collation'.
|
||||
|
||||
===
|
||||
|
||||
The following configuration variables are now deprecated. Their use is
|
||||
supported for the moment but you should change your config file now to
|
||||
use the new variables as support for the old variables may be dropped in the
|
||||
future:
|
||||
|
||||
$mail_settings['admin_all'] replaced by $mail_settings['on_new'] and
|
||||
$mail_settings['on_change']
|
||||
$mail_settings['admin_on_delete'] replaced by $mail_settings['on_delete']
|
||||
$dateformat replaced by $strftime_format['daymonth']
|
||||
|
||||
|
||||
Upgrading from prior MRBS 1.4.5
|
||||
===============================
|
||||
MRBS 1.4.5 introduces the concept of tentative bookings, or bookings that
|
||||
require confirmation. To avoid confusion, what were previously known as
|
||||
"provisional bookings" have now been renamed "bookings requiring approval"
|
||||
and the config variable $provisional_enabled has been renamed
|
||||
$approval_enabled. You should update your config file accordingly.
|
||||
|
||||
Please also see the note about database compatibility above.
|
||||
|
||||
|
||||
Upgrading from prior MRBS 1.2-pre3
|
||||
==================================
|
||||
Upgrade to MRBS 1.2-pre3 first by following the upgrade instructions in that
|
||||
release.
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/sperl5.6.0
|
||||
|
||||
# auth_pam.pl
|
||||
# uses Authen::PAM to validate a user - password pair
|
||||
# usage: auth_pam.pl [user] [password]
|
||||
# exit 0 on success, otherwise 1
|
||||
# script has to be SUID and use sperl if run as an unprivileged use
|
||||
# handle with care ...
|
||||
# Michael Redinger
|
||||
|
||||
use Authen::PAM;
|
||||
|
||||
exit 1 unless ( $ARGV[0] && $ARGV[1] );
|
||||
my $service = "passwd";
|
||||
my $username = $ARGV[0];
|
||||
my $password = $ARGV[1];
|
||||
|
||||
sub my_conv_func {
|
||||
my @res;
|
||||
while ( @_ ) {
|
||||
my $code = shift;
|
||||
my $msg = shift;
|
||||
my $ans = "";
|
||||
|
||||
$ans = $username if ($code == PAM_PROMPT_ECHO_ON() );
|
||||
$ans = $password if ($code == PAM_PROMPT_ECHO_OFF() );
|
||||
|
||||
push @res, (PAM_SUCCESS(),$ans);
|
||||
}
|
||||
push @res, PAM_SUCCESS();
|
||||
return @res;
|
||||
}
|
||||
|
||||
ref(my $pamh = new Authen::PAM($service, $username, \&my_conv_func)) ||
|
||||
die "Error code $pamh during PAM init!";
|
||||
|
||||
my $ret=$pamh->pam_authenticate;
|
||||
|
||||
exit 1 if ( $ret != 0 );
|
||||
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Language File Checker</title>
|
||||
<style>
|
||||
form {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
form div {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
label,
|
||||
input[type="checkbox"] {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid black;
|
||||
padding: 0.2em 0.5em;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
select {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Language File Checker</h1>
|
||||
<p>
|
||||
This will report missing or untranslated strings in the language files.
|
||||
</p>
|
||||
|
||||
<?php
|
||||
|
||||
// NOTE: You need to change this if you run checklang.php from anywhere but
|
||||
// the MRBS 'web' directory
|
||||
$path_to_mrbs = ".";
|
||||
|
||||
const PATTERN_START = '/^\$vocab\["([^"]+)"]/';
|
||||
const PATTERN_END = '/;\s*(?:(?:#|\/\/).*)?$/';
|
||||
|
||||
unset($lang);
|
||||
$lang = array();
|
||||
|
||||
if (!empty($_GET))
|
||||
{
|
||||
$lang = $_GET['lang'];
|
||||
$update = empty($_GET['update']) ? false : true;
|
||||
}
|
||||
|
||||
// Language file prefix
|
||||
$langs = "lang/lang.";
|
||||
|
||||
// Reference language:
|
||||
$ref_lang = "en";
|
||||
|
||||
// Make a list of language files to check. This is similar to glob() in
|
||||
// PEAR File/Find.
|
||||
$dh = opendir($path_to_mrbs.'/lang');
|
||||
while (($filename = readdir($dh)) !== false)
|
||||
{
|
||||
$files[] = $filename;
|
||||
}
|
||||
closedir($dh);
|
||||
|
||||
sort($files);
|
||||
|
||||
?>
|
||||
|
||||
<form method="get" action="checklang.php">
|
||||
<div>
|
||||
<label for="languages">Select one or more languages:</label>
|
||||
<select id="languages" multiple="multiple" size="5" name="lang[]">
|
||||
<?php
|
||||
foreach ($files as $filename)
|
||||
{
|
||||
if (preg_match('/^lang\.(.*)/', $filename, $name) && $name[1] != $ref_lang)
|
||||
{
|
||||
if (preg_match('/~|\.bak|\.swp\$/', $name[1]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
print "<option";
|
||||
if (array_search($name[1], $lang) !== FALSE)
|
||||
{
|
||||
print " selected=\"selected\"";
|
||||
}
|
||||
print ">$name[1]</option>\n";
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input id="update" type="checkbox" name="update">
|
||||
<label for="update">Update file(s) with new token lines (web server user requires write permission
|
||||
on files and directory)</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="submit" name="submit" value="Go">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<?php
|
||||
include "$path_to_mrbs/$langs$ref_lang";
|
||||
$ref = $vocab;
|
||||
|
||||
foreach ($lang as $l)
|
||||
{
|
||||
unset($vocab);
|
||||
include "$path_to_mrbs/$langs$l";
|
||||
if ($update)
|
||||
{
|
||||
$ref_statements = array();
|
||||
$in = fopen("$path_to_mrbs/$langs$ref_lang", "r")
|
||||
or die("Failed to open $path_to_mrbs/$langs$ref_lang for reading\n");
|
||||
while (!feof($in))
|
||||
{
|
||||
$line = fgets($in);
|
||||
// Handle multi-line statements.
|
||||
// If we've started a statement append this line to it.
|
||||
if (isset($statement))
|
||||
{
|
||||
$statement .= $line;
|
||||
}
|
||||
// Otherwise, if this is the start of a new statement, make this line the first line.
|
||||
elseif (preg_match(PATTERN_START, $line, $matches))
|
||||
{
|
||||
$statement = $line;
|
||||
$token = $matches[1];
|
||||
}
|
||||
|
||||
// And if this line is also the end of a statement, close it off and add it to the array.
|
||||
if (preg_match(PATTERN_END, $line))
|
||||
{
|
||||
$ref_statements[$token] = $statement;
|
||||
unset($statement);
|
||||
}
|
||||
}
|
||||
fclose($in);
|
||||
|
||||
$in = fopen("$path_to_mrbs/$langs$l", "r") or
|
||||
die("Failed to open $path_to_mrbs/$langs$l for reading");
|
||||
$out = fopen("$path_to_mrbs/$langs$l.new", "w") or
|
||||
die("Failed to open $path_to_mrbs/$langs$l.new for writing");
|
||||
|
||||
$seen = array();
|
||||
$added = array();
|
||||
// DEBUG print "<table>\n";
|
||||
while (!feof($in))
|
||||
{
|
||||
$line = fgets($in);
|
||||
$token_match = "";
|
||||
if (preg_match(PATTERN_START, $line, $matches))
|
||||
{
|
||||
// DEBUG print "<tr><td>$matches[1]</td><td>".key($ref_statements);
|
||||
$token_match = $matches[1];
|
||||
|
||||
if (!array_key_exists($token_match, $ref_statements))
|
||||
{
|
||||
fwrite($out, "// REMOVED - ".$line);
|
||||
continue;
|
||||
}
|
||||
while (($token_match != key($ref_statements)) &&
|
||||
(!array_key_exists(key($ref_statements), $vocab)))
|
||||
{
|
||||
if (array_key_exists(key($ref_statements), $seen))
|
||||
{
|
||||
break;
|
||||
}
|
||||
$seen[key($ref_statements)] = 1;
|
||||
fwrite($out, current($ref_statements));
|
||||
$added[] = htmlspecialchars(key($ref_statements));
|
||||
$ret = next($ref_statements);
|
||||
// DEBUG print " ".key($ref_statements);
|
||||
if (!$ret)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
next($ref_statements);
|
||||
}
|
||||
$seen[$token_match] = 1;
|
||||
fwrite($out, $line);
|
||||
// DEBUG print "</td></tr>\n";
|
||||
}
|
||||
fclose($in);
|
||||
fclose($out);
|
||||
// DEBUG print "</table>\n";
|
||||
|
||||
if (count($added))
|
||||
{
|
||||
print "Added the following tokens:\n<ul>\n<li>".
|
||||
implode("</li>\n<li>",$added)."</li>\n</ul>\n";
|
||||
rename("$path_to_mrbs/$langs$l", "$path_to_mrbs/$langs$l.old") or
|
||||
die("Failed to rename $path_to_mrbs/$langs$l to $path_to_mrbs/$langs$l.old");
|
||||
rename("$path_to_mrbs/$langs$l.new", "$path_to_mrbs/$langs$l") or
|
||||
die("Failed to rename $path_to_mrbs/$langs$l.new to $path_to_mrbs/$langs$l");
|
||||
|
||||
// Re-read the updated file
|
||||
unset($vocab);
|
||||
include "$path_to_mrbs/$langs$l";
|
||||
}
|
||||
else
|
||||
{
|
||||
print "No token lines added.";
|
||||
unlink("$path_to_mrbs/$langs$l.new") or
|
||||
print "<span style=\"color: red; font-weight: bold\">
|
||||
Failed to delete $path_to_mrbs/$langs".htmlspecialchars($l).".new</span>.\n";
|
||||
}
|
||||
}
|
||||
?>
|
||||
<h2>Language: <?php echo htmlspecialchars($l) ?></h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>Problem</th>
|
||||
<th>Key</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
<?php
|
||||
$ntotal = 0;
|
||||
$nmissing = 0;
|
||||
$nunxlate = 0;
|
||||
|
||||
foreach ($ref as $key => $val)
|
||||
{
|
||||
$ntotal++;
|
||||
$status = "";
|
||||
if (!isset($vocab[$key]))
|
||||
{
|
||||
$nmissing++;
|
||||
$status = "Missing";
|
||||
|
||||
} else if (($key != "charset") &&
|
||||
($vocab[$key] == $ref[$key]) &&
|
||||
($ref[$key] != "") &&
|
||||
(!preg_match('/^mail_/', $key)))
|
||||
{
|
||||
$status = "Untranslated";
|
||||
$nunxlate++;
|
||||
}
|
||||
if ($status != "")
|
||||
{
|
||||
echo " <tr><td>$status</td><td>" .
|
||||
htmlspecialchars($key) . "</td><td>" .
|
||||
htmlspecialchars($ref[$key]) . "</td></tr>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</table>\n";
|
||||
echo "<p>Total entries in reference language file: $ntotal\n";
|
||||
echo "<br>For language file $l: ";
|
||||
if ($nmissing + $nunxlate == 0)
|
||||
{
|
||||
echo "no missing or untranslated entries.\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "missing: $nmissing, untranslated: $nunxlate.\n";
|
||||
}
|
||||
print "<hr>\n";
|
||||
}
|
||||
|
||||
?>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
# SHA-512 crypted passwords, for better security
|
||||
user1:$6$FGcixVeC$J6Vb/7PKwcOn3M9ma2n4PtFx.83bA5p9s1Qk/GYOHgGZMw1rpPbZC6t1QbGETgyr.azZnAcJNJ/7Qdh9EasAf.
|
||||
user2:$6$zhZgcAMOe2$2CCvBRTPIsRaDA5Lt2gi3Nb6W1JQ2wklq3oZ/Cw8GNPYAtTO4CM7s/4ohNhJvzp1PpGD1WmsDf.bxBmAYTSxL0
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
# Authentication script to use with MRBS's "ext" authentication
|
||||
# scheme. config.inc.php should include something like:
|
||||
#
|
||||
# $auth["realm"] = "MRBS";
|
||||
# $auth["type"] = "ext";
|
||||
# $auth["prog"] = "../crypt_passwd.pl";
|
||||
# $auth["params"] = "/etc/httpd/mrbs_passwd #USERNAME# #PASSWORD#";
|
||||
#
|
||||
# The script takes 3 pararameters:
|
||||
#
|
||||
# PASSWDFILE USERNAME PASSWORD
|
||||
#
|
||||
# Where:
|
||||
#
|
||||
# PASSWDFILE - Filename of password file, which is the form
|
||||
# <username>:<crypted password>
|
||||
# [See crypt_passwd.example for an example]
|
||||
# You should make sure this is readable by the
|
||||
# user that PHP (most likely the web server)
|
||||
# runs as.
|
||||
# USERNAME - Username to check
|
||||
# PASSWORD - Password to check against crypted password in
|
||||
# password file
|
||||
#
|
||||
# Returns 0 on success, 1 on failure
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my $passwd_filename = shift || die "No passwd filename supplied\n";
|
||||
my $username = shift || die "No username supplied\n";
|
||||
my $password = shift || die "No password supplied\n";
|
||||
|
||||
my $retcode = 1;
|
||||
|
||||
open PASSWD,'<',$passwd_filename;
|
||||
|
||||
while (<PASSWD>)
|
||||
{
|
||||
if (m/^([^:]+):(.*)$/)
|
||||
{
|
||||
my $user = $1;
|
||||
my $crypt = $2;
|
||||
|
||||
if ($user eq $username)
|
||||
{
|
||||
if (crypt($password, $crypt) eq $crypt)
|
||||
{
|
||||
$retcode = 0;
|
||||
last;
|
||||
}
|
||||
else
|
||||
{
|
||||
last;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
close PASSWD;
|
||||
|
||||
exit $retcode;
|
||||
@@ -0,0 +1,23 @@
|
||||
--
|
||||
-- MRBS table destruction script for PostgreSQL 7.0 or higher
|
||||
-- This exists because I can never remember the sequence name magic.
|
||||
--
|
||||
-- If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
-- to something else then you must change each reference to 'mrbs_' in the
|
||||
-- lines below.
|
||||
--
|
||||
|
||||
DROP TABLE mrbs_area;
|
||||
DROP SEQUENCE mrbs_area_id_seq;
|
||||
DROP TABLE mrbs_room;
|
||||
DROP SEQUENCE mrbs_room_id_seq;
|
||||
DROP TABLE mrbs_entry;
|
||||
DROP SEQUENCE mrbs_entry_id_seq;
|
||||
DROP TABLE mrbs_repeat;
|
||||
DROP SEQUENCE mrbs_repeat_id_seq;
|
||||
DROP TABLE mrbs_users;
|
||||
DROP SEQUENCE mrbs_users_id_seq;
|
||||
DROP TABLE mrbs_variables;
|
||||
DROP SEQUENCE mrbs_variables_id_seq;
|
||||
DROP TABLE mrbs_zoneinfo;
|
||||
DROP SEQUENCE mrbs_zoneinfo_id_seq;
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php // -*-mode: PHP; coding:utf-8;-*-
|
||||
namespace MRBS;
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS Configuration File
|
||||
* Configure this file for your site.
|
||||
* You shouldn't have to modify anything outside this file.
|
||||
*
|
||||
* This file has already been populated with the minimum set of configuration
|
||||
* variables that you will need to change to get your system up and running.
|
||||
* If you want to change any of the other settings in systemdefaults.inc.php
|
||||
* or areadefaults.inc.php, then copy the relevant lines into this file
|
||||
* and edit them here. This file will override the default settings and
|
||||
* when you upgrade to a new version of MRBS the config file is preserved.
|
||||
*
|
||||
* NOTE: if you include or require other files from this file, for example
|
||||
* to store your database details in a separate location, then you should
|
||||
* use an absolute and not a relative pathname.
|
||||
**************************************************************************/
|
||||
|
||||
/**********
|
||||
* Timezone
|
||||
**********/
|
||||
|
||||
// The timezone your meeting rooms run in. It is especially important
|
||||
// to set this if you're using PHP 5 on Linux. In this configuration
|
||||
// if you don't, meetings in a different DST than you are currently
|
||||
// in are offset by the DST offset incorrectly.
|
||||
//
|
||||
// Note that timezones can be set on a per-area basis, so strictly speaking this
|
||||
// setting should be in areadefaults.inc.php, but as it is so important to set
|
||||
// the right timezone it is included here.
|
||||
//
|
||||
// When upgrading an existing installation, this should be set to the
|
||||
// timezone the web server runs in. See the INSTALL document for more information.
|
||||
//
|
||||
// A list of valid timezones can be found at http://php.net/manual/timezones.php
|
||||
// The following line must be uncommented by removing the '//' at the beginning
|
||||
$timezone = $_ENV['MRBS_TIMEZONE'] ?? "Etc/UTC";
|
||||
|
||||
/*******************
|
||||
* Database settings
|
||||
******************/
|
||||
// Which database system: "pgsql"=PostgreSQL, "mysql"=MySQL
|
||||
$dbsys = $_ENV['MRBS_DB_SYSTEM'] ?? 'mysql';
|
||||
// Hostname of database server. For pgsql, can use "" instead of localhost
|
||||
// to use Unix Domain Sockets instead of TCP/IP. For mysql "localhost"
|
||||
// tells the system to use Unix Domain Sockets, and $db_port will be ignored;
|
||||
// if you want to force TCP connection you can use "127.0.0.1".
|
||||
$db_host = $_ENV['MRBS_DB_HOST'] ?? '172.17.0.1';
|
||||
// If you need to use a non standard port for the database connection you
|
||||
// can uncomment the following line and specify the port number
|
||||
// $db_port = 1234;
|
||||
// Database name:
|
||||
$db_database = $_ENV['MRBS_DB_DATABASE'] ?? 'mrbs';
|
||||
// Schema name. This only applies to PostgreSQL and is only necessary if you have more
|
||||
// than one schema in your database and also you are using the same MRBS table names in
|
||||
// multiple schemas.
|
||||
//$db_schema = "public";
|
||||
// Database login user name:
|
||||
$db_login = $_ENV['MRBS_DB_USER'] ?? 'mrbs';
|
||||
// Database login password:
|
||||
$db_password = $_ENV['MRBS_DB_PASSWORD'] ?? 'mrbs-password';
|
||||
// Prefix for table names. This will allow multiple installations where only
|
||||
// one database is available
|
||||
$db_tbl_prefix = $_ENV['MRBS_DB_TBL_PREFIX'] ?? 'mrbs_';
|
||||
// Set $db_persist to TRUE to use PHP persistent (pooled) database connections. Note
|
||||
// that persistent connections are not recommended unless your system suffers significant
|
||||
// performance problems without them. They can cause problems with transactions and
|
||||
// locks (see http://php.net/manual/en/features.persistent-connections.php) and although
|
||||
// MRBS tries to avoid those problems, it is generally better not to use persistent
|
||||
// connections if you can.
|
||||
$db_persist = false;
|
||||
|
||||
|
||||
/* Add lines from systemdefaults.inc.php and areadefaults.inc.php below here
|
||||
to change the default configuration. Do _NOT_ modify systemdefaults.inc.php
|
||||
or areadefaults.inc.php. */
|
||||
@@ -0,0 +1,59 @@
|
||||
# Dev environment
|
||||
|
||||
This docker setup is supposed to get a running system for local development.
|
||||
|
||||
## Run locally
|
||||
|
||||
* Compose Docker containers:
|
||||
~~~
|
||||
cd docker_app
|
||||
docker compose up -d
|
||||
~~~
|
||||
(Linux users may need `sudo` or `docker login`)
|
||||
|
||||
* Open [localhost:8080](http://localhost:8080) in your browser \
|
||||
Logins are defined at first use. (There may be example data in the future)
|
||||
|
||||
## Additonal information
|
||||
|
||||
### Inspect database
|
||||
Open [localhost:8888](http://localhost:8888) in your browser for phpmyadmin \
|
||||
Login: `mrbs:mrbs`
|
||||
|
||||
Alternatively, you can connect to the database using the command like tool `mysql`:
|
||||
~~~
|
||||
docker-compose db mysql -u mrbs -pmrbs mrbs
|
||||
~~~
|
||||
|
||||
### View logs
|
||||
|
||||
View apache webserver logs:
|
||||
~~~
|
||||
docker compose logs www
|
||||
~~~
|
||||
View database logs:
|
||||
~~~
|
||||
docker compose logs db
|
||||
~~~
|
||||
|
||||
|
||||
### Live reloading
|
||||
The repository's source code is mounted into the docker containers. That means, changes of code take effect immediately after refreshing the browser.
|
||||
|
||||
However, when configuration of php and database is change, you have to reset the containers.
|
||||
|
||||
### Reset containers
|
||||
|
||||
* Stop Docker containers:
|
||||
~~~
|
||||
docker compose down
|
||||
~~~
|
||||
* Delete persisted volumes:
|
||||
~~~
|
||||
docker volume prune
|
||||
~~~
|
||||
* Rebuild container images:
|
||||
~~~
|
||||
docker compose build
|
||||
~~~
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
services:
|
||||
www:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker_app/php/Dockerfile
|
||||
ports:
|
||||
- "8080:80"
|
||||
command: apache2ctl -D FOREGROUND
|
||||
volumes:
|
||||
- ../web:/var/www/html/
|
||||
- ./php/config.inc.php:/var/www/html/config.inc.php
|
||||
links:
|
||||
- db
|
||||
networks:
|
||||
- default
|
||||
db:
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3306:3306"
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
environment:
|
||||
MYSQL_DATABASE: mrbs
|
||||
MYSQL_USER: mrbs
|
||||
MYSQL_PASSWORD: mrbs
|
||||
MYSQL_ROOT_PASSWORD: mrbs
|
||||
volumes:
|
||||
- ../tables.my.sql:/docker-entrypoint-initdb.d/010-tables.sql
|
||||
- persistent:/var/lib/mysql
|
||||
networks:
|
||||
- default
|
||||
phpmyadmin:
|
||||
image: phpmyadmin
|
||||
links:
|
||||
- db:db
|
||||
ports:
|
||||
- 8888:80
|
||||
volumes:
|
||||
persistent:
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM php:8.4-apache
|
||||
|
||||
RUN a2enmod rewrite
|
||||
RUN apt-get update && apt-get install -y libicu-dev locales-all \
|
||||
&& apt-get clean
|
||||
RUN docker-php-ext-install mysqli pdo pdo_mysql intl
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php // -*-mode: PHP; coding:utf-8;-*-
|
||||
namespace MRBS;
|
||||
|
||||
$timezone = "Europe/London";
|
||||
$dbsys = "mysql";
|
||||
$db_host = "db";
|
||||
$db_database = "mrbs";
|
||||
$db_login = "mrbs";
|
||||
$db_password = "mrbs";
|
||||
$db_tbl_prefix = "mrbs_";
|
||||
$db_persist = FALSE;
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace MRBS;
|
||||
|
||||
use IntlDateFormatter;
|
||||
use IntlDatePatternGenerator;
|
||||
|
||||
function write_line($fp, string $key, string $value) : void
|
||||
{
|
||||
$keywords = ['no', 'yes', 'true', 'false', 'on', 'off', 'null', 'none'];
|
||||
if (in_arrayi($key, $keywords))
|
||||
{
|
||||
// If it's a keyword we put a space before the key because it is a keyword and otherwise
|
||||
// parse_ini_file() will fail. The space is trimmed when it is parsed.
|
||||
// See https://stackoverflow.com/questions/6142980/using-a-keyword-as-variable-name-in-an-ini-file
|
||||
$key = " $key";
|
||||
$comment = "; The following line has a space before the key because it is a keyword\n";
|
||||
fwrite($fp, $comment);
|
||||
}
|
||||
fwrite($fp, "$key = \"$value\"\n");
|
||||
}
|
||||
|
||||
|
||||
if (!extension_loaded('intl'))
|
||||
{
|
||||
die("The 'intl' extension needs to be loaded.");
|
||||
}
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
$dir = 'tmp';
|
||||
|
||||
$locales = \ResourceBundle::getLocales('');
|
||||
|
||||
echo "<h2>Skeleton files</h2>\n";
|
||||
|
||||
$skeletons = array(
|
||||
'd',
|
||||
'dEMMM',
|
||||
'dMMM',
|
||||
'dMMMM',
|
||||
'MMMMy'
|
||||
);
|
||||
|
||||
foreach ($skeletons as $skeleton) {
|
||||
$filename = "$dir/skeletons/$skeleton.ini";
|
||||
echo "Generating $filename ...";
|
||||
$fp = fopen($filename, 'w');
|
||||
foreach ($locales as $locale)
|
||||
{
|
||||
$locale = Language::convertToBcp47($locale);
|
||||
$pattern_generator = new IntlDatePatternGenerator($locale);
|
||||
$pattern = $pattern_generator->getBestPattern($skeleton);
|
||||
write_line($fp, $locale, $pattern);
|
||||
// Fix up for some locales
|
||||
if (($locale == 'zh-Hans-CN') && !in_array('zh-CN', $locales)) {
|
||||
write_line($fp, 'zh-CN', $pattern);
|
||||
}
|
||||
if (($locale == 'zh-Hant-TW') && !in_array('zh-TW', $locales)) {
|
||||
write_line($fp, 'zh-TW', $pattern);
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
echo " done<br>\n";
|
||||
}
|
||||
|
||||
echo "<h2>Type files</h2>\n";
|
||||
|
||||
$types = array(
|
||||
'full' => IntlDateFormatter::FULL,
|
||||
'long' => IntlDateFormatter::LONG,
|
||||
'medium' => IntlDateFormatter::MEDIUM,
|
||||
'short' => IntlDateFormatter::SHORT,
|
||||
'none' => IntlDateFormatter::NONE
|
||||
);
|
||||
|
||||
foreach ($types as $date_key => $date_value)
|
||||
{
|
||||
foreach ($types as $time_key => $time_value)
|
||||
{
|
||||
if (($date_value === IntlDateFormatter::NONE) && ($time_value === IntlDateFormatter::NONE))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$filename = "$dir/types/{$date_key}_{$time_key}.ini";
|
||||
echo "Generating $filename ...";
|
||||
$fp = fopen($filename, 'w');
|
||||
foreach ($locales as $locale) {
|
||||
$locale = Language::convertToBcp47($locale);
|
||||
$formatter = new IntlDateFormatter($locale, $date_value, $time_value);
|
||||
$pattern = $formatter->getPattern();
|
||||
write_line($fp, $locale, $pattern);
|
||||
// Fix up for some locales
|
||||
if (($locale == 'zh-Hans-CN') && !in_array('zh-CN', $locales)) {
|
||||
write_line($fp, 'zh-CN', $pattern);
|
||||
}
|
||||
if (($locale == 'zh-Hant-TW') && !in_array('zh-TW', $locales)) {
|
||||
write_line($fp, 'zh-TW', $pattern);
|
||||
}
|
||||
}
|
||||
fclose($fp);
|
||||
echo " done<br>\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
--
|
||||
-- grant.pg.sql - Edit this to grant rights on PostgreSQL MRBS tables.
|
||||
-- You should not need to use this file if you create the tables (using the
|
||||
-- tables.pg.sql script) while connected to the database as the same user
|
||||
-- MRBS will use (the one in config.inc.php), because that account will own the
|
||||
-- tables and have all rights. However if you create the tables with another
|
||||
-- account, such as the superuser account, you need to use this script to
|
||||
-- grant rights to the user found in your config.inc.php file.
|
||||
--
|
||||
-- If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
-- to something else then you must change each reference to 'mrbs_' in the
|
||||
-- lines below.
|
||||
--
|
||||
-- Copy and edit this file as needed- Change the user name, then run it.
|
||||
|
||||
GRANT ALL ON
|
||||
mrbs_area,mrbs_area_id_seq,
|
||||
mrbs_entry,mrbs_entry_id_seq,
|
||||
mrbs_repeat,mrbs_repeat_id_seq,
|
||||
mrbs_room,mrbs_room_id_seq,
|
||||
mrbs_users,mrbs_users_id_seq,
|
||||
mrbs_variables,mrbs_variables_id_seq,
|
||||
mrbs_participants,mrbs_participants_id_seq,
|
||||
mrbs_zoneinfo,mrbs_zoneinfo_id_seq
|
||||
TO mrbs;
|
||||
@@ -0,0 +1,296 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<title>Help - report.php</title>
|
||||
<style type="text/css">
|
||||
body {
|
||||
font-size: small;
|
||||
font-family: Arial, Verdana, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
font-size: medium;
|
||||
margin-top: 2em;
|
||||
}
|
||||
body > table > tbody > tr:last-child > td {
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
td {
|
||||
padding: 0 1em 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
table table {
|
||||
margin-left: 2em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>NAME</h1>
|
||||
<p>
|
||||
report.php - produce MRBS reports
|
||||
</p>
|
||||
<h1>SYNOPSIS</h1>
|
||||
<p>
|
||||
<code>report.php [args]</code>
|
||||
</p>
|
||||
<p>
|
||||
where args is a space separated list of arguments of the form <code>param1=value1&param2=value2</code>. Spaces
|
||||
in the value string and ampersands can normally be escaped with the backslash character ('\'), depending
|
||||
on the shell. Array parameters can be sent by using the '[]' notation, eg <code>param1[]=valueA&param1[]=valueB</code>.
|
||||
Array parameters must all appear in the same argument.
|
||||
</p>
|
||||
<h1>DESCRIPTION</h1>
|
||||
<p>
|
||||
This page describes how to use MRBS reporting from the command line (CLI). Report.php can
|
||||
either be called directly or as a cron job. The script report.php should be called
|
||||
as a parameter to the PHP interpreter. The output of report.php is sent to STDOUT.
|
||||
</p>
|
||||
<p>
|
||||
The language and locale used for the reports is specified by the config variable
|
||||
$cli_language. The script can only be run from the command line if the MRBS config
|
||||
variable $allow_cli is set to TRUE.
|
||||
</p>
|
||||
<h1>OPTIONS</h1>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>output</code></td>
|
||||
<td>
|
||||
The type of output to produce. Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>0</code></td><td>A report<em> (default)</em></td></tr>
|
||||
<tr><td><code>1</code></td><td>A summary</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>output_format</code></td>
|
||||
<td>
|
||||
The format that the output should be produced in. Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>0</code></td><td>HTML<em> (default except when running from the CLI)</em></td></tr>
|
||||
<tr><td><code>1</code></td><td>CSV<em> (default when running from the CLI)</em></td></tr>
|
||||
<tr><td><code>2</code></td><td>iCalendar (.ics file) report - excluding periods<em> (cannot be used for summaries)</em></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<code>day</code>
|
||||
</td>
|
||||
<td>
|
||||
The day (1..31) to use as the base date for the reporting
|
||||
period. If any of day, week and month are not specified
|
||||
then today's date will be used as the base date.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>month</code>
|
||||
</td>
|
||||
<td>
|
||||
The month (1..12) to use as the base date for the reporting
|
||||
period. If any of day, week and month are not specified
|
||||
then today's date will be used as the base date.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>year</code>
|
||||
</td>
|
||||
<td>
|
||||
The year (4 digits) to use as the base date for the reporting
|
||||
period. If any of day, week and month are not specified
|
||||
then today's date will be used as the base date.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>from_date</code></td>
|
||||
<td>
|
||||
The start date of the reporting period in YYYY-MM-DD format. If
|
||||
from_date is not specified then the base date will be used.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>to_date</code></td>
|
||||
<td>
|
||||
The end date of the reporting period in YYYY-MM-DD format. If
|
||||
to_date is not specified then the base date +
|
||||
$default_report_days (specified in systemdefaults.inc.php and
|
||||
optionally over-ridden in config.in.php) will be used. Note that
|
||||
the reporting period ends at 0000 on the to_day, so if for example
|
||||
you want a report for all bookings in 2019 the to_date should be
|
||||
2020-01-01.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>creatormatch</code></td>
|
||||
<td>
|
||||
Limit the report to entries where the creator's user name contains the string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>areamatch</code></td>
|
||||
<td>
|
||||
Limit the report to entries where the area name contains the string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>roommatch</code></td>
|
||||
<td>
|
||||
Limit the report to entries where the room name contains the string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>namematch</code></td>
|
||||
<td>
|
||||
Limit the report to entries where the entry name contains the string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>descrmatch</code></td>
|
||||
<td>
|
||||
Limit the report to entries where the description contains the string.
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>typematch[]</code></td>
|
||||
<td>
|
||||
Limit the report to entries of types X,Y,Z (eg typematch[]=X&typematch[]=Y&typematch[]=Z). Default: all types.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>match_approved</code></td>
|
||||
<td>
|
||||
Limit the report to entries that are/are not approved. Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>0</code></td><td>Awaiting approval</td></tr>
|
||||
<tr><td><code>1</code></td><td>Approved</td></tr>
|
||||
<tr><td><code>2</code></td><td>All entries<em> (default)</em></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>match_confirmed</code></td>
|
||||
<td>
|
||||
Limit the report to entries that are tentative or confirmed. Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>0</code></td><td>Tentative</td></tr>
|
||||
<tr><td><code>1</code></td><td>Confirmed</td></tr>
|
||||
<tr><td><code>2</code></td><td>All entries<em> (default)</em></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>match_private</code></td>
|
||||
<td>
|
||||
Limit the report to entries that are private or public. Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>0</code></td><td>Private</td></tr>
|
||||
<tr><td><code>1</code></td><td>Public</td></tr>
|
||||
<tr><td><code>2</code></td><td>All entries<em> (default)</em></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>sortby</code></td>
|
||||
<td>
|
||||
Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>r</code></td><td>sort by room name<em> (default)</em></td></tr>
|
||||
<tr><td><code>s</code></td><td>sort by start time</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>sumby</code></td>
|
||||
<td>
|
||||
The field to use for the first column of the summary table.
|
||||
Permitted values are:
|
||||
<table>
|
||||
<tr><td><code>d</code></td><td>brief description<em> (default)</em></td></tr>
|
||||
<tr><td><code>c</code></td><td>creator's user name</td></tr>
|
||||
<tr><td><code>t</code></td><td>type</td></tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>phase</code></td>
|
||||
<td>
|
||||
The phase of report production. Phase 1 is the gathering of user input from
|
||||
the web page form; Phase 2 is the production of the report or summary. When running
|
||||
from the command line it is not necessary to set this option as it is automatically
|
||||
set to 2 by MRBS. However when running report.php from the web browser or by using wget,
|
||||
setting phase=2 will force report.php to go straight to the production of a report.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>custom</td>
|
||||
<td>
|
||||
Custom fields can be searched for using the same syntax as above.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>RETURN VALUES</h1>
|
||||
<p>
|
||||
Returns 0 on success.
|
||||
</p>
|
||||
<h1>EXAMPLES</h1>
|
||||
<p>
|
||||
To produce a summary in CSV format of all bookings for 2019, arranged by the creator's
|
||||
name, and send the summary to the file summary.csv:
|
||||
</p>
|
||||
<p>
|
||||
<code>
|
||||
/usr/local/bin/php /home/mrbs/report.php output=1 from_date=2019-01-01 to_date=2020-01-01 sumby=c > summary.csv
|
||||
</code>
|
||||
</p>
|
||||
<p>
|
||||
The CLI output can be simulated in a browser by putting the parameters in a query string. But note
|
||||
that the parameter <code>phase=2</code> must be added to tell MRBS that it is on the second
|
||||
phase of report production (the first phase is gathering the user input from the form; if report.php
|
||||
is called from the command line the phase is automatically set to 2). For example,
|
||||
to simulate the CLI command above enter into the browser (note the addition of the <code>
|
||||
output_format</code> parameter as CSV is not the default format when running from the browser):
|
||||
</p>
|
||||
<p>
|
||||
<code>
|
||||
report.php?phase=2&output=1&output_format=2&from_date=2019-01-01&to_date=2020-01-01&sumby=c
|
||||
</code>
|
||||
</p>
|
||||
<p>
|
||||
When using wget, don't forget to escape ampersands with a backslash. For example:
|
||||
</p>
|
||||
<p>
|
||||
<code>
|
||||
wget -O myreport.csv http://localhost/mrbs/report.php?phase=2\&output=1\&output_format=2\&sumby=c
|
||||
</code>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,60 @@
|
||||
-- MySQL dump 10.13 Distrib 9.1.0, for Linux (aarch64)
|
||||
--
|
||||
-- Host: sql.s1172.vhostgo.com Database: hotel
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 5.7.43
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
|
||||
/*!50503 SET NAMES utf8mb4 */;
|
||||
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
|
||||
/*!40103 SET TIME_ZONE='+00:00' */;
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `mrbs_users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `mrbs_users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `mrbs_users` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`level` smallint(6) NOT NULL DEFAULT '0',
|
||||
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`display_name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`email` varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
`last_login` int(11) NOT NULL DEFAULT '0',
|
||||
`reset_key_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
`reset_key_expiry` int(11) NOT NULL DEFAULT '0',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uq_name` (`name`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4;
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `mrbs_users`
|
||||
--
|
||||
|
||||
LOCK TABLES `mrbs_users` WRITE;
|
||||
/*!40000 ALTER TABLE `mrbs_users` DISABLE KEYS */;
|
||||
INSERT INTO `mrbs_users` VALUES (1,2,'jeremy','jeremy','$2y$10$C4VGlDbJiBVmt8PXiMmlhOi6NGCtTCMrV/3U5dwsdJMvQiIj4beAm','admin@hi-luzhou-lj.com','2026-09-08 05:53:11',1788846791,'$2y$10$hgrY2nC7A23kKXA5L5nvb.4GwDkC2OC2pWrK1Dizc/Sb2.2NtrjNq',1744439790),(2,1,'eason','陈寒','$2y$10$EWyBLvJk0UiejPlCpMBLnehSQlT6Xpw9pbRv2y/wrb0lKdtm3XpQy','','2023-02-15 01:12:52',1788488228,NULL,0),(3,1,'lucky','张欢','$2y$10$U22AeosZmnUbi8pmQ.R9M.xndnBrZV5AgHrd9qQU7JbXv2.UMwU0.','','2024-10-17 02:44:42',1729133517,NULL,0),(9,1,'belinda','王静','$2y$10$qJr0sfz6u4GZua32cDLXTOg6vyw5rLJBaXGAakmn3HEphCWoftxOK','belinda.wang@hi-luzhou-lj.com','2026-04-09 07:39:10',1788846084,NULL,0),(21,1,'cindy','冯晨','$2y$10$o/SOJOp9UVbtt0MqRoR66ee5AyJlznJ681EZO0f2/LOXUEaRbax1.','','2024-12-06 05:34:50',1749530710,NULL,0),(29,1,'suri','钟玉琴','$2y$10$DtiUpoA3rBqSv7DBDr0YtuDLKnV/Xur52ojtrx.fvO/i2A7nTOIkG','suri.zhong@hi-luzhou-lj.com','2025-11-03 07:15:05',1786963122,NULL,0),(31,0,'amanda_wang','王瑞宁','$2y$10$YkVG5t2GV77Y1ijts.lPjeTjf0ieQcTyFUi4vjdVyCc59Hmlg/kKu','amanda.wang@hi-luzhou-lj.com','2026-08-18 08:15:35',1786847093,NULL,0),(32,1,'lilian','廖娟','$2y$10$7MhFElkB/szgUoe4Y31wEutZDjA1TIFwq8vwpWg59xPcc5PjaJDia','','2025-04-08 08:52:11',1788484836,NULL,0),(35,0,'ally','向容','$2y$10$rsmGiJLmnUt6PbKXA804Oe2HWnaGGpp5InjQaWlczhMfpY9BmTMF2','1324163081@qq.com','2026-08-07 08:42:11',1785938012,NULL,0),(36,0,'hugo','曹昌帅','$2y$10$kkyZJH5w4QgaLAdXufpTrO33xNf3zgMPnW0AsoSbwqafsCXenwA8.','2903358368@qq.com','2026-08-07 08:42:27',1755312304,NULL,0),(37,0,'catherine','王琳','$2y$10$KRovytS5py0puVzqpDS0Q.gr/Z6l6KMFe0XrvOjKfEZt6HWfljj.K','catherine.wang@hi-luzhou-lj.com','2026-08-07 08:42:37',1771745151,NULL,0),(38,0,'nancy','廖欣','$2y$10$bHktJMRFPGuAxFt5YX1UoOwqoPQaAQ5iAsJbM/b2ImTRxYarCBkwK','nancy.liao@hi-luzhou-lj.com','2026-08-07 08:42:21',1773279134,NULL,0),(39,1,'gira','吴浩楠','$2y$10$2XJ3abvI8F08.wl4MV.xSuuSfNLGLPxyJiBaptnp38jy1RysXr1fe','2490187466@qq.com','2026-08-10 02:45:59',1788276180,NULL,0),(40,1,'karry','王瑞宁','$2y$10$R02L3MXf4cZx2XmxWSz0xunM6Flok1db5Urlsg3Ym9kxFjjHfHnkm','amanda.wang@hi-luzhou-lj.com','2026-08-18 08:16:26',1787104559,NULL,0);
|
||||
/*!40000 ALTER TABLE `mrbs_users` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
|
||||
-- Dump completed on 2026-09-08 6:45:34
|
||||
@@ -0,0 +1,29 @@
|
||||
--
|
||||
-- mrbs/purge.my.sql 2001-01-13 : Purge old MRBS entries, for MySQL
|
||||
--
|
||||
-- This SQL script will delete old entries from your MRBS database.
|
||||
-- By default, entries which ended 30 days or more in the past will be removed,
|
||||
-- Repeat table records with no corresponding entry records will be removed.
|
||||
--
|
||||
-- If old entries get purged from a series, then somebody edits the series,
|
||||
-- the old entries will be re-created unless they change the start date on
|
||||
-- the form. Fixing this would require changing the start_time and end_time
|
||||
-- in the repeat record to match oldest undeleted entry; this is left as an
|
||||
-- exercise to the reader.
|
||||
--
|
||||
-- If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
-- to something else then you must edit each 'DELETE FROM' line below.
|
||||
--
|
||||
-- MySQL Notes:
|
||||
-- To change the number of days, edit BOTH places below.
|
||||
--
|
||||
-- Because MySQL lacks sub-selects, I can't use SQL to remove orphan repeat
|
||||
-- entries. (See purge.pg.sql for the "right way".) Instead, this removes
|
||||
-- records from the repeat table based on their repeat end_date, which is
|
||||
-- close enough to be almost the same thing.
|
||||
|
||||
DELETE FROM mrbs_entry
|
||||
WHERE end_time < unix_timestamp(date_sub(current_timestamp, interval 30 day));
|
||||
|
||||
DELETE FROM mrbs_repeat
|
||||
WHERE end_date < unix_timestamp(date_sub(current_timestamp, interval 30 day));
|
||||
@@ -0,0 +1,26 @@
|
||||
--
|
||||
-- mrbs/purge.pg.sql 2001-01-13 : Purge old MRBS entries, for PostgreSQL
|
||||
--
|
||||
-- This SQL script will delete old entries from your MRBS database.
|
||||
-- By default, entries which ended 30 days or more in the past will be removed,
|
||||
-- Repeat table records with no corresponding entry records will be removed.
|
||||
--
|
||||
-- If old entries get purged from a series, then somebody edits the series,
|
||||
-- the old entries will be re-created unless they change the start date on
|
||||
-- the form. Fixing this would require changing the start_time and end_time
|
||||
-- in the repeat record to match oldest undeleted entry; this is left as an
|
||||
-- exercise to the reader.
|
||||
--
|
||||
-- If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
-- to something else then you must edit each 'DELETE FROM' line below.
|
||||
--
|
||||
|
||||
BEGIN;
|
||||
|
||||
DELETE FROM mrbs_entry
|
||||
WHERE end_time < date_part('epoch', current_timestamp - interval '30 days');
|
||||
|
||||
DELETE FROM mrbs_repeat
|
||||
WHERE id NOT IN (SELECT repeat_id FROM mrbs_entry);
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,848 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Auth;
|
||||
|
||||
use MRBS\DB\DB;
|
||||
use MRBS\Language;
|
||||
use MRBS\MailQueue;
|
||||
use MRBS\User;
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use function MRBS\_tbl;
|
||||
use function MRBS\auth;
|
||||
use function MRBS\db;
|
||||
use function MRBS\format_compound_name;
|
||||
use function MRBS\generate_token;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\multisite;
|
||||
use function MRBS\parse_email;
|
||||
use function MRBS\row_cast_columns;
|
||||
use function MRBS\toTimeString;
|
||||
use function MRBS\url_base;
|
||||
|
||||
class AuthDb extends AuthDbAbstract
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->db_table = _tbl('users');
|
||||
$this->column_name_username = 'name';
|
||||
$this->column_name_display_name = 'display_name';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|null $user a username or email address
|
||||
*/
|
||||
public function validateUser(
|
||||
#[\SensitiveParameter]
|
||||
?string $user,
|
||||
#[\SensitiveParameter]
|
||||
?string $pass)
|
||||
{
|
||||
// The string $user that the user logged on with could be either a username or
|
||||
// an email address, or even possibly just the local part of an email address.
|
||||
// So it's just possible that there is more than one user with this password and
|
||||
// username | email address | local-part. If we get more than one, then we don't
|
||||
// know which user it is, so we return false.
|
||||
$valid_usernames = array();
|
||||
|
||||
if (($valid_username = $this->validateUsername($user, $pass)) !== false)
|
||||
{
|
||||
$valid_usernames[] = $valid_username;
|
||||
}
|
||||
|
||||
$valid_usernames = array_merge($valid_usernames, $this->validateEmail($user, $pass));
|
||||
$valid_usernames = array_unique($valid_usernames);
|
||||
|
||||
if (count($valid_usernames) == 1)
|
||||
{
|
||||
$result = $valid_usernames[0];
|
||||
// Update the database with this login, but don't change the timestamp
|
||||
$now = time();
|
||||
$sql = "UPDATE " . _tbl('users') . "
|
||||
SET last_login=?, timestamp=timestamp
|
||||
WHERE name=?";
|
||||
$sql_params = array($now, $result);
|
||||
$this->connection()->command($sql, $sql_params);
|
||||
return $result;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function connection() : ?DB
|
||||
{
|
||||
return db();
|
||||
}
|
||||
|
||||
|
||||
/* validateUsername($user, $pass)
|
||||
*
|
||||
* Checks if the specified username/password pair are valid
|
||||
*
|
||||
* $user - The user name
|
||||
* $pass - The password
|
||||
*
|
||||
* Returns:
|
||||
* false - The pair are invalid or do not exist
|
||||
* string - The validated username
|
||||
*/
|
||||
private function validateUsername(
|
||||
#[\SensitiveParameter]
|
||||
?string $user,
|
||||
#[\SensitiveParameter]
|
||||
?string $pass)
|
||||
{
|
||||
$sql_params = array();
|
||||
|
||||
// We use syntax_casesensitive_equals() rather than just '=' because '=' in MySQL
|
||||
// permits trailing spacings, eg 'john' = 'john '. We could use LIKE, but that then
|
||||
// permits wildcards, so we could use a combination of LIKE and '=' but that's a bit
|
||||
// messy. We could use STRCMP, but that's MySQL only.
|
||||
|
||||
// Usernames are unique in the users table, so we only look for one.
|
||||
$sql = "SELECT password_hash, name
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE " . $this->connection()->syntax_casesensitive_equals('name', mb_strtolower($user), $sql_params) . "
|
||||
LIMIT 1";
|
||||
|
||||
$res = $this->connection()->query($sql, $sql_params);
|
||||
|
||||
$row = $res->next_row_keyed();
|
||||
|
||||
if (!isset($row['password_hash']))
|
||||
{
|
||||
// No user found with that name
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($this->checkPassword($pass, $row['password_hash'], 'name', $row['name'])) ? $row['name'] : false;
|
||||
}
|
||||
|
||||
|
||||
/* authValidateEmail($email, $pass)
|
||||
*
|
||||
* Checks if the specified email/password pair are valid
|
||||
*
|
||||
* $email - The email address
|
||||
* $pass - The password
|
||||
*
|
||||
* Returns:
|
||||
* array - An array of valid usernames, empty if none found
|
||||
*/
|
||||
private function validateEmail(
|
||||
#[\SensitiveParameter]
|
||||
string $email,
|
||||
#[\SensitiveParameter]
|
||||
string $pass) : array
|
||||
{
|
||||
$valid_usernames = array();
|
||||
|
||||
// Email addresses are not unique in the users table, so we need to find all of them.
|
||||
$users = self::getUsersByEmail($email);
|
||||
|
||||
// Check all the users that have this email address and password hash.
|
||||
foreach($users as $user)
|
||||
{
|
||||
if (isset($user['password_hash']) &&
|
||||
$this->checkPassword($pass, $user['password_hash'], 'email', $email))
|
||||
{
|
||||
$valid_usernames[] = $user['name'];
|
||||
}
|
||||
}
|
||||
|
||||
return $valid_usernames;
|
||||
}
|
||||
|
||||
|
||||
protected function getUserFresh(string $username) : ?User
|
||||
{
|
||||
$row = $this->getUserByUsername($username);
|
||||
|
||||
// The username doesn't exist - return NULL
|
||||
if (!isset($row))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The username does exist - return a User object
|
||||
$user = new User($username);
|
||||
|
||||
// $user->level and $user->display_name will be set as part of this
|
||||
foreach ($row as $key => $value)
|
||||
{
|
||||
if ($key == 'name')
|
||||
{
|
||||
// This has already been set as the 'username' property;
|
||||
continue;
|
||||
}
|
||||
$user->$key = $value;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
// Return an array of all users
|
||||
public function getUsers() : array
|
||||
{
|
||||
// Add in an extra column, last_updated, which is the SQL timestamp converted to a UNIX
|
||||
// timestamp. We do the conversion in the SQL query so that it is converted using the
|
||||
// same timezone that it was stored with.
|
||||
$sql = "SELECT *, ". $this->connection()->syntax_timestamp_to_unix("timestamp") . " AS last_updated
|
||||
FROM " . _tbl('users') . "
|
||||
ORDER BY name";
|
||||
|
||||
$res = $this->connection()->query($sql);
|
||||
|
||||
$result = $res->all_rows_keyed();
|
||||
|
||||
foreach ($result as &$row)
|
||||
{
|
||||
row_cast_columns($row, 'users');
|
||||
// Turn the last_updated column into an int (some MySQL drivers will return a string,
|
||||
// and it won't have been caught by row_cast_columns() as it's a derived result).
|
||||
$row['last_updated'] = intval($row['last_updated']);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public function canCreateUsers() : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public function canValidateByEmail() : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public function canResetPassword() : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*/
|
||||
public function canResetByEmail() : bool
|
||||
{
|
||||
// We allow resetting by email, even if there are multiple users with the
|
||||
// same email address.
|
||||
return $this->canValidateByEmail();
|
||||
}
|
||||
|
||||
|
||||
public function requestPassword(?string $login) : bool
|
||||
{
|
||||
if (!isset($login) || ($login === ''))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the possible users given this login, which could be a username or email address.
|
||||
// However all the possible users must have the same email address, so check the email
|
||||
// addresses at the same time.
|
||||
$possible_users = array();
|
||||
|
||||
$user = $this->getUserByUsername($login);
|
||||
|
||||
// Users must have an email address otherwise we won't be able to mail a reset link
|
||||
if (isset($user) && isset($user['email']) && ($user['email'] !== ''))
|
||||
{
|
||||
$possible_users[] = $user;
|
||||
}
|
||||
|
||||
if ($this->canValidateByEmail())
|
||||
{
|
||||
$users = $this->getUsersByEmail($login);
|
||||
if (!empty($users))
|
||||
{
|
||||
// Check that the email addresses are the same
|
||||
if (!empty($possible_users) &&
|
||||
(mb_strtolower($possible_users[0]['email']) !== mb_strtolower($login)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
foreach ($users as $user)
|
||||
{
|
||||
$possible_users[] = $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($possible_users))
|
||||
{
|
||||
// Generate a key
|
||||
$key = generate_token(32);
|
||||
|
||||
// Update the database
|
||||
if ($this->setResetKey($possible_users, $key))
|
||||
{
|
||||
// Email the user
|
||||
return $this->notifyUser($possible_users, $key);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public function resetPassword(
|
||||
#[\SensitiveParameter]
|
||||
?string $username,
|
||||
?string $key,
|
||||
#[\SensitiveParameter]
|
||||
?string $password) : bool
|
||||
{
|
||||
// Check that we've got a password and we're allowed to reset the password
|
||||
if (!isset($password) || !auth()->isValidReset($username, $key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set the new password and clear the reset key
|
||||
$sql = "UPDATE " . _tbl('users') . "
|
||||
SET password_hash=:password_hash,
|
||||
reset_key_hash=NULL,
|
||||
reset_key_expiry=0
|
||||
WHERE name=:name"; // PostgreSQL does not support LIMIT with UPDATE
|
||||
|
||||
$sql_params = array(
|
||||
':password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
':name' => $username
|
||||
);
|
||||
|
||||
$this->connection()->command($sql, $sql_params);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function isValidReset(?string $user, ?string $key) : bool
|
||||
{
|
||||
if (!isset($user) || !isset($key) || ($user === '') || ($key === ''))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$sql = "SELECT reset_key_hash, reset_key_expiry
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE name=:name
|
||||
LIMIT 1";
|
||||
|
||||
$sql_params = array(':name' => $user);
|
||||
$res = $this->connection()->query($sql,$sql_params);
|
||||
|
||||
// Check we've found a row
|
||||
if ($res->count() == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$row = $res->next_row_keyed();
|
||||
|
||||
// Check that the reset hasn't expired
|
||||
if (time() > $row['reset_key_expiry'])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check we've got the correct key
|
||||
return password_verify($key, $row['reset_key_hash']);
|
||||
}
|
||||
|
||||
|
||||
// Returns an unsorted array of registrants display names
|
||||
protected function getRegistrantsDisplayNamesUnsortedWithout(int $id, bool $with_registrant_username) : array
|
||||
{
|
||||
// For the 'db' auth type we can improve performance by doing a single query
|
||||
// on the participants table joined with the users table. (Actually it's two
|
||||
// queries in a UNION: one getting the rows where there isn't an entry in the
|
||||
// users table and another the rows where there is.)
|
||||
$sql = "SELECT P.username as username,
|
||||
P.username as display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U
|
||||
ON P.username=U.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND (U.display_name IS NULL OR U.display_name='')
|
||||
UNION
|
||||
SELECT U.name as username,
|
||||
U.display_name as display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U
|
||||
ON P.username=U.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND U.display_name IS NOT NULL AND U.display_name!=''";
|
||||
|
||||
$result = array();
|
||||
$res = $this->connection()->query($sql, array(':entry_id' => $id));
|
||||
|
||||
while (false !== ($row = $res->next_row_keyed()))
|
||||
{
|
||||
$result[] = ($with_registrant_username) ? format_compound_name($row['username'], $row['display_name']) : $row['display_name'];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
// Returns an unsorted array of registrants display names, including, if
|
||||
// different, the display name of the person that registered them.
|
||||
protected function getRegistrantsDisplayNamesUnsortedWith(int $id, bool $with_registrant_username) : array
|
||||
{
|
||||
// For the 'db' auth type we can improve performance by doing a single query
|
||||
// on the participants table joined with the users table. (Actually it's four
|
||||
// queries in a UNION: one getting the rows where there isn't an entry in the
|
||||
// users table and another the rows where there is, etc. for both the registrant
|
||||
// and the person that registered them.)
|
||||
$sql = "SELECT P.username as registrant_username,
|
||||
P.username as registrant_display_name,
|
||||
P.create_by as create_by_username,
|
||||
P.create_by as create_by_display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U1
|
||||
ON P.username=U1.name
|
||||
LEFT JOIN " . _tbl('users') . " U2
|
||||
ON P.create_by=U2.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND (U1.display_name IS NULL OR U1.display_name='')
|
||||
AND (U2.display_name IS NULL OR U2.display_name='')
|
||||
|
||||
UNION
|
||||
|
||||
SELECT P.username as registrant_username,
|
||||
P.username as registrant_display_name,
|
||||
P.create_by as create_by_username,
|
||||
U2.display_name as registrant_display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U1
|
||||
ON P.username=U1.name
|
||||
LEFT JOIN " . _tbl('users') . " U2
|
||||
ON P.create_by=U2.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND (U1.display_name IS NULL OR U1.display_name='')
|
||||
AND U2.display_name IS NOT NULL AND U2.display_name!=''
|
||||
|
||||
UNION
|
||||
|
||||
SELECT P.username as registrant_username,
|
||||
U1.display_name as registrant_display_name,
|
||||
P.create_by as create_by_username,
|
||||
P.create_by as registrant_display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U1
|
||||
ON P.username=U1.name
|
||||
LEFT JOIN " . _tbl('users') . " U2
|
||||
ON P.create_by=U2.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND U1.display_name IS NOT NULL AND U1.display_name!=''
|
||||
AND (U2.display_name IS NULL OR U2.display_name='')
|
||||
|
||||
UNION
|
||||
|
||||
SELECT P.username as registrant_username,
|
||||
U1.display_name as registrant_display_name,
|
||||
P.create_by as create_by_username,
|
||||
U2.display_name as registrant_display_name
|
||||
FROM " . _tbl('participants') . " P
|
||||
LEFT JOIN " . _tbl('users') . " U1
|
||||
ON P.username=U1.name
|
||||
LEFT JOIN " . _tbl('users') . " U2
|
||||
ON P.create_by=U2.name
|
||||
WHERE P.entry_id=:entry_id
|
||||
AND U1.display_name IS NOT NULL AND U1.display_name!=''
|
||||
AND U2.display_name IS NOT NULL AND U2.display_name!=''";
|
||||
|
||||
$result = array();
|
||||
|
||||
$res = $this->connection()->query($sql, array(':entry_id' => $id));
|
||||
|
||||
while (false !== ($row = $res->next_row_keyed()))
|
||||
{
|
||||
if ($row['registrant_username'] === $row['create_by_username'])
|
||||
{
|
||||
if ($with_registrant_username)
|
||||
{
|
||||
$result[] = format_compound_name($row['registrant_username'], $row['registrant_display_name']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$result[] = $row['registrant_display_name'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($with_registrant_username && ($row['registrant_username'] !== $row['registrant_display_name']))
|
||||
{
|
||||
$result[] = get_vocab('registrant_username_and_registered_by',
|
||||
$row['registrant_username'],
|
||||
$row['registrant_display_name'],
|
||||
$row['create_by_display_name']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$result[] = get_vocab('registrant_registered_by',
|
||||
$row['registrant_display_name'],
|
||||
$row['create_by_display_name']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
protected function getRegistrantsDisplayNamesUnsorted(int $id, bool $with_registered_by, $with_registrant_username) : array
|
||||
{
|
||||
if ($with_registered_by)
|
||||
{
|
||||
return $this->getRegistrantsDisplayNamesUnsortedWith($id, $with_registrant_username);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this->getRegistrantsDisplayNamesUnsortedWithout($id, $with_registrant_username);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function notifyUser(array $users, string $key) : bool
|
||||
{
|
||||
global $auth, $mail_settings;
|
||||
|
||||
if (empty($users) || !isset($users[0]['email']) || ($users[0]['email'] === ''))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$expiry_time = $auth['db']['reset_key_expiry'];
|
||||
toTimeString($expiry_time, $expiry_units, true, 'hours');
|
||||
$addresses = array(
|
||||
'from' => $mail_settings['from']
|
||||
);
|
||||
// Add the To address, using the display name if possible (ie if it exists and there's
|
||||
// only one user).
|
||||
// Also get a name to use in the message body
|
||||
if ((count($users) == 1) &&
|
||||
isset($users[0]['display_name']) &&
|
||||
($users[0]['display_name'] !== ''))
|
||||
{
|
||||
$mailer = new PHPMailer();
|
||||
$mailer->CharSet = Language::MAIL_CHARSET;
|
||||
// Note that addrFormat() returns a MIME-encoded address
|
||||
$addresses['to'] = $mailer->addrFormat(array($users[0]['email'], $users[0]['display_name']));
|
||||
$name = $users[0]['display_name'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$addresses['to'] = $users[0]['email'];
|
||||
// If there's only one user we can use the username, otherwise we have to use the
|
||||
// email address which is the same for all users.
|
||||
$name = (count($users) == 1) ? $users[0]['name'] : $users[0]['email'];
|
||||
}
|
||||
$subject = get_vocab('password_reset_subject');
|
||||
$body = '<p>';
|
||||
$body .= get_vocab('password_reset_body', intval($expiry_time), $expiry_units, $name);
|
||||
$body .= "</p>\n";
|
||||
|
||||
// Construct and add in the link
|
||||
$usernames = array();
|
||||
foreach ($users as $user)
|
||||
{
|
||||
$usernames[] = $user['name'];
|
||||
}
|
||||
$usernames = array_unique($usernames);
|
||||
|
||||
$vars = array(
|
||||
'action' => 'reset',
|
||||
'usernames' => $usernames,
|
||||
'key' => $key
|
||||
);
|
||||
$query = http_build_query($vars, '', '&');
|
||||
$href = url_base() . multisite("reset_password.php?$query");
|
||||
$body .= "<p><a href=\"$href\">" . get_vocab('reset_password') . "</a>.</p>";
|
||||
|
||||
MailQueue::add(
|
||||
$addresses,
|
||||
$subject,
|
||||
strip_tags($body),
|
||||
$body,
|
||||
null,
|
||||
Language::MAIL_CHARSET
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function setResetKey(array $users, string $key) : bool
|
||||
{
|
||||
global $auth;
|
||||
|
||||
if (empty($users))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$ids = array();
|
||||
foreach($users as $user)
|
||||
{
|
||||
// Use intval to make sure the string is safe for the SQL query
|
||||
$ids[] = intval($user['id']);
|
||||
}
|
||||
|
||||
$sql = "UPDATE " . _tbl('users') . "
|
||||
SET reset_key_hash=:reset_key_hash,
|
||||
reset_key_expiry=:reset_key_expiry
|
||||
WHERE id IN (" . implode(',', $ids) . ")";
|
||||
|
||||
$sql_params = array(
|
||||
':reset_key_hash' => password_hash($key, PASSWORD_DEFAULT),
|
||||
':reset_key_expiry' => time() + $auth['db']['reset_key_expiry']
|
||||
);
|
||||
|
||||
$this->connection()->command($sql, $sql_params);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private function getUserByUsername(string $username) : ?array
|
||||
{
|
||||
$sql = "SELECT *
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE name=:name
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->connection()->query($sql, array(':name' => $username));
|
||||
|
||||
// The username doesn't exist - return NULL
|
||||
if ($result->count() === 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $result->next_row_keyed();
|
||||
}
|
||||
|
||||
|
||||
public function getUserByUserId(int $id) : ?User
|
||||
{
|
||||
$sql = "SELECT *
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE id=:id
|
||||
LIMIT 1";
|
||||
|
||||
$result = $this->connection()->query($sql, array(':id' => $id));
|
||||
|
||||
// The username doesn't exist - return NULL
|
||||
if ($result->count() === 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The username does exist - return a User object
|
||||
$user = new User();
|
||||
$row = $result->next_row_keyed();
|
||||
|
||||
// $user->level and $user->display_name will be set as part of this
|
||||
foreach ($row as $key => $value)
|
||||
{
|
||||
if ($key == 'name')
|
||||
{
|
||||
$user->username = $value;
|
||||
}
|
||||
$user->$key = $value;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
|
||||
public function getUsernameByEmail(string $email) : ?string
|
||||
{
|
||||
$sql = "SELECT name
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE email=?";
|
||||
|
||||
$res = $this->connection()->query($sql, array($email));
|
||||
|
||||
if ($res->count() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($res->count() > 1)
|
||||
{
|
||||
// Could maybe do something better here
|
||||
trigger_error("Email address not unique", E_USER_NOTICE);
|
||||
}
|
||||
$row = $res->next_row_keyed();
|
||||
return $row['name'];
|
||||
}
|
||||
|
||||
|
||||
// Returns an array of rows for all users with the email address $email.
|
||||
// Assumes that email addresses are case insensitive.
|
||||
// Allows equivalent Gmail addresses, ie ignores dots in the local part and
|
||||
// treats gmail.com and googlemail.com as equivalent domains.
|
||||
private function getUsersByEmail(string $email) : array
|
||||
{
|
||||
global $auth;
|
||||
|
||||
$result = array();
|
||||
|
||||
// For the moment we will assume that email addresses are case-insensitive. Whilst it is true
|
||||
// on most systems, it isn't always true. The domain is case-insensitive but the local-part can
|
||||
// be case-sensitive. But before we can take account of this, the email addresses in the database
|
||||
// need to be normalised so that all the domain names are stored in lower case. Then it will be
|
||||
// possible to do a case-sensitive comparison.
|
||||
if (mb_strpos($email, '@') === false)
|
||||
{
|
||||
if (!empty($auth['allow_local_part_email']))
|
||||
{
|
||||
// We're just checking the local-part of the email address
|
||||
$sql_params = array($email);
|
||||
$condition = "LOWER(?)=LOWER(" . $this->connection()->syntax_simple_split('email', '@', 1, $sql_params) .")";
|
||||
}
|
||||
else
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$address = parse_email($email);
|
||||
// Invalid email address
|
||||
if ($address === false)
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
// Special case for Gmail addresses: ignore dots in the local part and treat gmail.com and
|
||||
// googlemail.com as equivalent domains.
|
||||
elseif (in_array(mb_strtolower($address['domain']), array('gmail.com', 'googlemail.com')))
|
||||
{
|
||||
$sql_params = array(str_replace('.', '', $address['local']));
|
||||
$sql_params[] = $sql_params[0];
|
||||
$condition = "(LOWER(?) = REPLACE(TRIM(TRAILING '@gmail.com' FROM LOWER(email)), '.', '')) OR " .
|
||||
"(LOWER(?) = REPLACE(TRIM(TRAILING '@googlemail.com' FROM LOWER(email)), '.', ''))";
|
||||
}
|
||||
// Everything else: check the complete email address
|
||||
else
|
||||
{
|
||||
$sql_params = array($email);
|
||||
$condition = "LOWER(?)=LOWER(email)";
|
||||
}
|
||||
}
|
||||
|
||||
$sql = "SELECT *
|
||||
FROM " . _tbl('users') . "
|
||||
WHERE $condition";
|
||||
|
||||
$res = $this->connection()->query($sql, $sql_params);
|
||||
|
||||
while (false !== ($row = $res->next_row_keyed()))
|
||||
{
|
||||
$result[] = $row;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
private function rehash(
|
||||
#[\SensitiveParameter]
|
||||
string $password,
|
||||
string $column_name,
|
||||
string $column_value) : void
|
||||
{
|
||||
$sql_params = array(password_hash($password, PASSWORD_DEFAULT));
|
||||
|
||||
switch ($column_name)
|
||||
{
|
||||
case 'name':
|
||||
$condition = $this->connection()->syntax_casesensitive_equals($column_name, mb_strtolower($column_value), $sql_params);
|
||||
break;
|
||||
case 'email':
|
||||
// For the moment we will assume that email addresses are case insensitive. Whilst it is true
|
||||
// on most systems, it isn't always true. The domain is case insensitive but the local-part can
|
||||
// be case sensitive. But before we can take account of this, the email addresses in the database
|
||||
// need to be normalised so that all the domain names are stored in lower case. Then it will be possible
|
||||
// to do a case sensitive comparison.
|
||||
$sql_params[] = $column_value;
|
||||
$condition = "LOWER($column_name)=LOWER(?)";
|
||||
break;
|
||||
default:
|
||||
trigger_error("Unsupported column name '$column_name'.", E_USER_NOTICE);
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
$sql = "UPDATE " . _tbl('users') . "
|
||||
SET password_hash=?
|
||||
WHERE $condition";
|
||||
|
||||
$this->connection()->command($sql, $sql_params);
|
||||
}
|
||||
|
||||
|
||||
// Checks $password against $password_hash for the row in the user table
|
||||
// where $column_name=$column_value. Typically $column_name will be either
|
||||
// 'name' or 'email'.
|
||||
// Returns a boolean: true if they match, otherwise false.
|
||||
private function checkPassword(
|
||||
#[\SensitiveParameter]
|
||||
string $password,
|
||||
string $password_hash,
|
||||
string $column_name,
|
||||
string $column_value) : bool
|
||||
{
|
||||
$result = false;
|
||||
$do_rehash = false;
|
||||
|
||||
/* If the hash starts '$' it's a PHP password hash */
|
||||
if (substr($password_hash, 0, 1) == '$')
|
||||
{
|
||||
if (password_verify($password, $password_hash))
|
||||
{
|
||||
$result = true;
|
||||
if (password_needs_rehash($password_hash, PASSWORD_DEFAULT))
|
||||
{
|
||||
$do_rehash = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Otherwise it's a legacy MD5 hash */
|
||||
else
|
||||
{
|
||||
if (md5($password) == $password_hash)
|
||||
{
|
||||
$result = true;
|
||||
$do_rehash = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($do_rehash)
|
||||
{
|
||||
$this->rehash($password, $column_name, $column_value);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS\Session;
|
||||
|
||||
use MRBS\Form\ElementA;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputPassword;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\FieldInputText;
|
||||
use MRBS\Form\Form;
|
||||
use function MRBS\auth;
|
||||
use function MRBS\get_form_var;
|
||||
use function MRBS\get_vocab;
|
||||
use function MRBS\location_header;
|
||||
use function MRBS\multisite;
|
||||
use function MRBS\print_footer;
|
||||
use function MRBS\print_header;
|
||||
use function MRBS\this_page;
|
||||
|
||||
/**
|
||||
* An abstract class for those session schemes that implement a login form.
|
||||
*/
|
||||
abstract class SessionWithLogin extends Session
|
||||
{
|
||||
protected $form = array();
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
// Get the non-standard form variables
|
||||
$vars = [
|
||||
'action' => 'string',
|
||||
'username' => 'string',
|
||||
'password' => 'string',
|
||||
'returl' => 'url_local'
|
||||
];
|
||||
|
||||
foreach ($vars as $var => $type)
|
||||
{
|
||||
$this->form[$var] = get_form_var($var, $type, null, INPUT_POST);
|
||||
}
|
||||
|
||||
// Allow the target_url to be a GET or POST value to help password managers (the target_url can
|
||||
// be stored as a query string parameter in the password manager).
|
||||
$this->form['target_url'] = get_form_var('target_url', 'url_local');
|
||||
|
||||
if (isset($this->form['username']))
|
||||
{
|
||||
// It's easy for extra spaces to appear, especially on a mobile device
|
||||
$this->form['username'] = trim($this->form['username']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Gets the username and password. Returns: Nothing
|
||||
//
|
||||
// $target_url The URL to go to after successful login
|
||||
// $returl The URL to return to eventually
|
||||
public function authGet(?string $target_url=null, ?string $returl=null, ?string $error=null, bool $raw=false) : void
|
||||
{
|
||||
if (!isset($target_url))
|
||||
{
|
||||
$target_url = $this->form['target_url'] ?? this_page(true);
|
||||
}
|
||||
|
||||
// Omit the Login link in the header when we're on the login page itself
|
||||
print_header(null, false, true);
|
||||
$action = multisite(this_page());
|
||||
$this->printLoginForm($action, $target_url, $returl, $error, $raw);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
|
||||
// Logon form. Returns an array.
|
||||
public function getLogonFormParams() : ?array
|
||||
{
|
||||
return array(
|
||||
'action' => multisite('admin.php'),
|
||||
'method' => Form::METHOD_POST,
|
||||
'hidden_inputs' => array('target_url' => this_page(true),
|
||||
'action' => 'QueryName')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// Returns the parameters ('method', 'action' and 'hidden_inputs') for a
|
||||
// logoff form. Returns an array of parameters, or null if no form is to be
|
||||
// shown.
|
||||
public function getLogoffFormParams() : ?array
|
||||
{
|
||||
return array(
|
||||
'action' => multisite('admin.php'),
|
||||
'method' => Form::METHOD_POST,
|
||||
'hidden_inputs' => array('target_url' => this_page(true),
|
||||
'action' => 'SetName',
|
||||
'username' => '',
|
||||
'password' => '')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function processForm() : void
|
||||
{
|
||||
if (isset($this->form['action']))
|
||||
{
|
||||
// Target of the form with sets the URL argument "action=QueryName".
|
||||
// Will eventually return to URL argument "target_url=whatever".
|
||||
if ($this->form['action'] == 'QueryName')
|
||||
{
|
||||
$this->authGet($this->form['target_url']);
|
||||
exit(); // unnecessary because authGet() exits, but just included for clarity
|
||||
}
|
||||
|
||||
// Target of the form with sets the URL argument "action=SetName".
|
||||
// Will eventually return to URL argument "target_url=whatever".
|
||||
if ($this->form['action'] == 'SetName')
|
||||
{
|
||||
// First make sure the password is valid
|
||||
if (!isset($this->form['username']) || ($this->form['username'] == ''))
|
||||
{
|
||||
$this->logoffUser();
|
||||
}
|
||||
else
|
||||
{
|
||||
// If we're going to do something then check the CSRF token first.
|
||||
// (Don't check the token before logging off the user because if the session has
|
||||
// expired due to inactivity, the token will be invalid, but that won't matter because
|
||||
// the result will be the same anyway - logging off the user - and we avoid
|
||||
// generating an unnecessary CSRF error message.)
|
||||
Form::checkToken();
|
||||
|
||||
// Get a valid user
|
||||
$valid_username = $this->getValidUser($this->form['username'], $this->form['password']);
|
||||
|
||||
// Successful login. You can't get out of getValidUser() without a valid username and password
|
||||
$this->logonUser($valid_username);
|
||||
|
||||
if (!empty($this->form['returl']))
|
||||
{
|
||||
// check to see whether there's a query string already
|
||||
$this->form['target_url'] .= (mb_strpos($this->form['target_url'], '?') === false) ? '?' : '&';
|
||||
$this->form['target_url'] .= 'returl=' . urlencode($this->form['returl']);
|
||||
}
|
||||
}
|
||||
|
||||
location_header($this->form['target_url']); // Redirect browser to initial page
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Can only return a valid username. If the username and password are not valid it will ask for new ones.
|
||||
protected function getValidUser(
|
||||
#[\SensitiveParameter]
|
||||
?string $username,
|
||||
#[\SensitiveParameter]
|
||||
?string $password) : string
|
||||
{
|
||||
if (!isset($this->form['password']) ||
|
||||
(($valid_username = auth()->validateUser($this->form['username'], $this->form['password'])) === false))
|
||||
{
|
||||
$this->authGet($this->form['target_url'], $this->form['returl'], get_vocab('unknown_user'));
|
||||
exit(); // unnecessary because authGet() exits, but just included for clarity
|
||||
}
|
||||
|
||||
return $valid_username;
|
||||
}
|
||||
|
||||
|
||||
protected function logonUser(string $username) : void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function logoffUser() : void
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
// Displays the login form.
|
||||
// Will eventually return to $target_url with query string returl=$returl
|
||||
// If $error is set then an $error is printed.
|
||||
// If $raw is true then the message is not HTML escaped
|
||||
private function printLoginForm(string $action, ?string $target_url, ?string $returl, ?string $error=null, bool $raw=false) : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
$form->setAttributes(array('class' => 'standard',
|
||||
'id' => 'logon',
|
||||
'action' => $action));
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('returl' => $returl,
|
||||
'target_url' => $target_url,
|
||||
'action' => 'SetName');
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// Now for the visible fields
|
||||
if (isset($error))
|
||||
{
|
||||
$p = new ElementP();
|
||||
$p->setText($error, false, $raw);
|
||||
$form->addElement($p);
|
||||
}
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('please_login'));
|
||||
|
||||
// The username field
|
||||
if (auth()->canValidateByEmail() && auth()->canValidateByUsername())
|
||||
{
|
||||
$tag = 'username_or_email';
|
||||
}
|
||||
elseif (auth()->canValidateByUsername())
|
||||
{
|
||||
$tag = 'users.name';
|
||||
}
|
||||
else
|
||||
{
|
||||
$tag = 'users.email';
|
||||
}
|
||||
|
||||
$placeholder = get_vocab($tag);
|
||||
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('user'))
|
||||
->setLabelAttributes(array('title' => $placeholder))
|
||||
->setControlAttributes(array('id' => 'username',
|
||||
'name' => 'username',
|
||||
'placeholder' => $placeholder,
|
||||
'required' => true,
|
||||
'autofocus' => true,
|
||||
'autocomplete' => 'username'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The password field
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel(get_vocab('users.password'))
|
||||
->setControlAttributes(array('id' => 'password',
|
||||
'name' => 'password',
|
||||
'autocomplete' => 'current-password'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
// The submit button
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('login')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
if (auth()->canResetPassword())
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldDiv();
|
||||
$a = new ElementA();
|
||||
$a->setAttribute('href', multisite('reset_password.php'))
|
||||
->setText(get_vocab('lost_password'));
|
||||
$field->addControl($a);
|
||||
$fieldset->addElement($field);
|
||||
$form->addElement($fieldset);
|
||||
}
|
||||
|
||||
$form->render();
|
||||
|
||||
|
||||
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
// Check we've got the right authentication type for the session scheme.
|
||||
// To be called for those session schemes which require the same
|
||||
// authentication type
|
||||
protected function checkTypeMatchesSession() : void
|
||||
{
|
||||
global $auth;
|
||||
|
||||
if ($auth['type'] !== $auth['session'])
|
||||
{
|
||||
$class = get_called_class();
|
||||
$message = "MRBS configuration error: $class needs \$auth['type'] set to '" . $auth['session'] . "'";
|
||||
die($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,491 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Auth\AuthFactory;
|
||||
use MRBS\Session\Session;
|
||||
use MRBS\Session\SessionFactory;
|
||||
|
||||
|
||||
// Convenience wrapper function to provide access to an Auth object
|
||||
function auth()
|
||||
{
|
||||
global $auth;
|
||||
|
||||
static $auth_obj = null;
|
||||
|
||||
if (is_null($auth_obj))
|
||||
{
|
||||
$auth_obj = AuthFactory::create($auth['type']);
|
||||
}
|
||||
|
||||
return $auth_obj;
|
||||
}
|
||||
|
||||
|
||||
// Convenience wrapper function to provide access to a Session object
|
||||
function session() : Session
|
||||
{
|
||||
global $auth;
|
||||
|
||||
static $session_obj = null;
|
||||
|
||||
if (is_null($session_obj))
|
||||
{
|
||||
$session_obj = SessionFactory::create($auth['session']);
|
||||
}
|
||||
|
||||
return $session_obj;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Checks if a page is open to users, using the config variables
|
||||
// $auth['only_admin_can_book'] and $auth['only_admin_can_book_before']
|
||||
function booking_level() : int
|
||||
{
|
||||
global $auth;
|
||||
|
||||
if ($auth['allow_anonymous_booking'])
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($auth['only_admin_can_book'])
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
elseif ($auth['only_admin_can_book_before'])
|
||||
{
|
||||
$go_live = strtotime($auth['only_admin_can_book_before']);
|
||||
if ($go_live === false)
|
||||
{
|
||||
$message = "Could not calculate time from '" . $auth['only_admin_can_book_before'] . "'.";
|
||||
trigger_error($message);
|
||||
return 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
return (time() >= $go_live) ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
// Gets the minimum user level required to access a page
|
||||
function get_page_level($page)
|
||||
{
|
||||
global $auth, $max_level;
|
||||
|
||||
// If you're resetting your password you won't be logged in and $auth['deny_public_access']
|
||||
// should not apply.
|
||||
if (in_array($page, array('reset_password.php', 'reset_password_handler.php')))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Otherwise ...
|
||||
switch ($page)
|
||||
{
|
||||
// These pages are open to the public by default as they only contain
|
||||
// read features.
|
||||
case 'help.php':
|
||||
case 'index.php':
|
||||
$result = 0;
|
||||
break;
|
||||
|
||||
// These pages reveal usernames, which could be of assistance to someone trying to
|
||||
// break into the system, so users are required to be logged in before viewing them.
|
||||
case 'search.php':
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
case 'view_entry.php':
|
||||
$result = ($auth['allow_anonymous_booking']) ? 0 : 1;
|
||||
break;
|
||||
|
||||
// These pages are set to have a minimum access level of 1 as ordinary users
|
||||
// should be able to access them because they will have read access and in some
|
||||
// cases write access for their own entries. Where necessary further checks are
|
||||
// made within the page to prevent ordinary users gaining access to admin features.
|
||||
case 'admin.php':
|
||||
case 'approve_entry_handler.php': // Ordinary users are allowed to remind admins
|
||||
case 'edit_message.php': // Booking admins can edit messages
|
||||
case 'edit_message_handler.php': // Booking admins can edit messages
|
||||
case 'edit_room.php': // Ordinary users can view room details
|
||||
case 'edit_users.php': // Ordinary users can edit their own details
|
||||
case 'pending.php': // Ordinary users can view their own entries
|
||||
case 'registration_handler.php': // Ordinary users can register for an event
|
||||
case 'usernames.php': // Ajax page for getting a list of users (booking admins can use this)
|
||||
$result = 1;
|
||||
break;
|
||||
|
||||
// These pages allow users to create and delete entries
|
||||
case 'check_slot.php': // Ajax page used by edit_entry.php
|
||||
case 'del_entry.php':
|
||||
case 'edit_entry.php':
|
||||
case 'edit_entry_handler.php':
|
||||
return booking_level();
|
||||
break;
|
||||
|
||||
// Everything else is for admins only
|
||||
default:
|
||||
$result = (isset($max_level)) ? $max_level : 2;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($auth['deny_public_access'])
|
||||
{
|
||||
$result = max($result, 1);
|
||||
}
|
||||
|
||||
// Can always access index.php when in kiosk mode
|
||||
if ($page == 'index.php' && is_kiosk_mode())
|
||||
{
|
||||
$result = 0;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/* getAuthorised($level)
|
||||
*
|
||||
* Check to see if the current user has a certain level of rights
|
||||
*
|
||||
* $level - The access level required
|
||||
* $returl - The URL to return to eventually
|
||||
*
|
||||
* Returns:
|
||||
* false - The user does not have the required access
|
||||
* true - The user has the required access
|
||||
*/
|
||||
function getAuthorised($level, $returl) : bool
|
||||
{
|
||||
// If the minimum level is zero (or not set) then they are
|
||||
// authorised, whoever they are
|
||||
if (empty($level))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we need to check who they are
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if(!isset($mrbs_user))
|
||||
{
|
||||
// Ask them to authenticate, if the session scheme supports it
|
||||
if (method_exists(session(), 'authGet'))
|
||||
{
|
||||
session()->authGet(null, $returl);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($mrbs_user->level >= $level);
|
||||
}
|
||||
|
||||
|
||||
/* checkAuthorised()
|
||||
*
|
||||
* Checks to see that a user is authorised to access a page.
|
||||
* If they are not, then shows an "Access Denied" message and exits.
|
||||
*
|
||||
*/
|
||||
function checkAuthorised($page, $just_check=false)
|
||||
{
|
||||
global $view, $view_all, $year, $month, $day, $area, $room;
|
||||
global $returl;
|
||||
|
||||
// Get the minimum authorisation level for this page
|
||||
$required_level = get_page_level($page);
|
||||
|
||||
if ($just_check)
|
||||
{
|
||||
if ($required_level == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $required_level));
|
||||
}
|
||||
|
||||
// Check that the user has this level
|
||||
if (getAuthorised($required_level, $returl))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we don't know the right date then use today's
|
||||
if (!isset($day) or !isset($month) or !isset($year))
|
||||
{
|
||||
$day = date('d');
|
||||
$month = date('m');
|
||||
$year = date('Y');
|
||||
}
|
||||
|
||||
if (empty($area))
|
||||
{
|
||||
$area = get_default_area();
|
||||
}
|
||||
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, isset($room) ? $room : null);
|
||||
exit();
|
||||
}
|
||||
|
||||
|
||||
/* getWritable($creator, $room)
|
||||
*
|
||||
* Determines if the current user is able to modify an entry
|
||||
*
|
||||
* $creator - The creator of the entry
|
||||
* $rooms - The id(s) of the room(s) that the entries are in. Can
|
||||
* be a scalar or an array.
|
||||
* $all - Whether to check that the creator has write access
|
||||
* for all ($all=true) or just some ($all=false) of the
|
||||
* rooms.
|
||||
*
|
||||
* Returns:
|
||||
* false - The user does not have the required access
|
||||
* true - The user has the required access
|
||||
*/
|
||||
function getWritable($creator, $rooms=null, $all=true) : bool
|
||||
{
|
||||
if (is_array($rooms) && (count($rooms) > 0))
|
||||
{
|
||||
if ($all)
|
||||
{
|
||||
// We want the user to have write access for all the rooms,
|
||||
// so if for any one room they are not, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (!getWritable($creator, $room))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the user to have write access for at least one room,
|
||||
// so if there are no rooms for which they do, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (getWritable($creator, $room))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($rooms) && !$all)
|
||||
{
|
||||
// Not yet supported. Could support it but need to decide what $rooms=null means.
|
||||
// Does it mean all rooms in the system or just all rooms in the current area?
|
||||
throw new \Exception('$rooms===null and $all===false not yet supported.');
|
||||
}
|
||||
|
||||
// You can't make bookings in rooms which are invisible
|
||||
if (!is_visible($rooms))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always allowed to modify your own stuff
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user) && isset($creator) && (compare_usernames($creator, $mrbs_user->username) === 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise you have to be a (booking) admin for this room
|
||||
if (is_book_admin($rooms))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unauthorised access
|
||||
return false;
|
||||
}
|
||||
|
||||
/* showAccessDenied()
|
||||
*
|
||||
* Displays an appropriate message when access has been denied
|
||||
*
|
||||
* Returns: Nothing
|
||||
*/
|
||||
function showAccessDenied($view=null, $view_all=null, $year=null, $month=null, $day=null, $area=null, $room=null)
|
||||
{
|
||||
global $server;
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Wrap the contents in a <div> to help with styling. Not a very nice solution, but anyway.
|
||||
echo "<div>\n";
|
||||
echo "<h1>" . get_vocab("accessdenied") . "</h1>\n";
|
||||
echo "<p>" . get_vocab("norights") . "</p>\n";
|
||||
$referrer = session()->getReferrer();
|
||||
if (isset($referrer))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo "<a href=\"" . escape_html($referrer) . "\">\n" . get_vocab("returnprev") . "</a>\n";
|
||||
echo "</p>\n";
|
||||
}
|
||||
echo "</div>\n";
|
||||
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has admin rights
|
||||
function is_admin() : bool
|
||||
{
|
||||
global $max_level;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$required_level = (isset($max_level) ? $max_level : 2);
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $required_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has booking administration rights
|
||||
// for $rooms - ie is allowed to modify and delete other people's bookings
|
||||
// and to approve bookings.
|
||||
//
|
||||
// $rooms can be either a single scalar value or an array of room ids. The default
|
||||
// value for $rooms is all rooms. (At the moment $room is ignored, but is passed here
|
||||
// so that later MRBS can be enhanced to provide fine-grained permissions.)
|
||||
//
|
||||
// $all specifies whether the user must be a booking for all $rooms, or just some of
|
||||
// them, ie at least one.
|
||||
//
|
||||
// Returns: TRUE if the user is allowed has booking admin rights for
|
||||
// the room(s); otherwise FALSE
|
||||
function is_book_admin($rooms=null, $all=true) : bool
|
||||
{
|
||||
global $min_booking_admin_level;
|
||||
|
||||
if (is_array($rooms) && (count($rooms) > 0))
|
||||
{
|
||||
if ($all)
|
||||
{
|
||||
// We want the user to be a booking admin for all the rooms,
|
||||
// so if for any one room they are not, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (!is_book_admin($room))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want the user to be a booking admin for at least one room,
|
||||
// so if there are no rooms for which they are, then return false.
|
||||
foreach ($rooms as $room)
|
||||
{
|
||||
if (is_book_admin($room))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_null($rooms) && !$all)
|
||||
{
|
||||
// Not yet supported. Could support it but need to decide what $rooms=null means.
|
||||
// Does it mean all rooms in the system or just all rooms in the current area?
|
||||
throw new \Exception('$rooms===null and $all===false not yet supported.');
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $min_booking_admin_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user has user editing rights
|
||||
function is_user_admin() : bool
|
||||
{
|
||||
global $min_user_editing_level;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
return (isset($mrbs_user) && ($mrbs_user->level >= $min_user_editing_level));
|
||||
}
|
||||
|
||||
|
||||
// Checks whether a room is visible to the current user
|
||||
// Doesn't do anything at the moment, but allows for customisation or future development
|
||||
function is_visible($room) : bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Checks whether a user is allowed to register other users for events
|
||||
function can_register_others($room_id=null) : bool
|
||||
{
|
||||
global $auth;
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return $auth['users_can_register_others'] || is_book_admin($room_id);
|
||||
}
|
||||
|
||||
|
||||
// Checks whether the current user can see others' email addresses
|
||||
function can_see_email_addresses() : bool
|
||||
{
|
||||
global $auth, $is_private_field;
|
||||
|
||||
// Admins can see everything
|
||||
if (is_admin())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
// Don't expose email addresses to the public
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// MRBS must be configured for logged-in users to see others' details
|
||||
if ($auth['only_admin_can_see_other_users'])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise the email field in the users table must not be private
|
||||
return (!auth()->canCreateUsers() || empty($is_private_field['users.email']));
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
-- ============================================================
|
||||
-- MRBS 会议预订系统 等保二级整改 - 数据库升级脚本
|
||||
-- 适用: MySQL (mrbs_users 表)
|
||||
-- 日期: 2026-09-08
|
||||
-- 说明: 为"登录失败锁定 / 口令90天定期更换"两项代码改造准备数据列
|
||||
-- 执行前请先备份: mysqldump hotel mrbs_users > mrbs_users_bak_20260908.sql
|
||||
-- 用法: mysql -u hotel -p hotel < security_upgrade_20260908.sql
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE mrbs_users
|
||||
ADD COLUMN password_changed_at INT NOT NULL DEFAULT 0 COMMENT '口令最后设置时间戳(等保90天更换, 0=存量用户需立即改密)' AFTER reset_key_expiry,
|
||||
ADD COLUMN failed_logins INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数(达到阈值触发锁定)' AFTER password_changed_at,
|
||||
ADD COLUMN locked_until INT NOT NULL DEFAULT 0 COMMENT '锁定截止时间戳(0=未锁定)' AFTER failed_logins;
|
||||
|
||||
-- 回滚(如需):
|
||||
-- ALTER TABLE mrbs_users DROP COLUMN locked_until, DROP COLUMN failed_logins, DROP COLUMN password_changed_at;
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
#
|
||||
# MySQL MRBS table creation script
|
||||
#
|
||||
# Notes:
|
||||
# (1) If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
# to something else using $db_tbl_prefix then you must edit each
|
||||
# 'CREATE TABLE', 'INSERT INTO' and 'REFERENCES' line below to replace
|
||||
# 'mrbs_' with your new table prefix. A global replace of 'mrbs_' is
|
||||
# sufficient.
|
||||
#
|
||||
# (2) If you add new fields then you should also change the global variable
|
||||
# $standard_fields. Note that if you are just adding custom fields for
|
||||
# a single site then this is not necessary.
|
||||
|
||||
CREATE TABLE mrbs_area
|
||||
(
|
||||
# tinyints and smallints in mrbs_area are assumed to represent booleans
|
||||
id int NOT NULL auto_increment,
|
||||
disabled tinyint DEFAULT 0 NOT NULL,
|
||||
area_name varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
sort_key varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
timezone varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
area_admin_email text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
resolution int,
|
||||
default_duration int,
|
||||
default_duration_all_day tinyint DEFAULT 0 NOT NULL,
|
||||
morningstarts int,
|
||||
morningstarts_minutes int,
|
||||
eveningends int,
|
||||
eveningends_minutes int,
|
||||
private_enabled tinyint,
|
||||
private_default tinyint,
|
||||
private_mandatory tinyint,
|
||||
private_override varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
min_create_ahead_enabled tinyint,
|
||||
min_create_ahead_secs int,
|
||||
max_create_ahead_enabled tinyint,
|
||||
max_create_ahead_secs int,
|
||||
min_delete_ahead_enabled tinyint,
|
||||
min_delete_ahead_secs int,
|
||||
max_delete_ahead_enabled tinyint,
|
||||
max_delete_ahead_secs int,
|
||||
max_per_day_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_per_day int DEFAULT 0 NOT NULL,
|
||||
max_per_week_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_per_week int DEFAULT 0 NOT NULL,
|
||||
max_per_month_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_per_month int DEFAULT 0 NOT NULL,
|
||||
max_per_year_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_per_year int DEFAULT 0 NOT NULL,
|
||||
max_per_future_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_per_future int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_day_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_day int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_week_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_week int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_month_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_month int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_year_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_year int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_future_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_future int DEFAULT 0 NOT NULL,
|
||||
max_duration_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
max_duration_secs int DEFAULT 0 NOT NULL,
|
||||
max_duration_periods int DEFAULT 0 NOT NULL,
|
||||
custom_html text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
approval_enabled tinyint,
|
||||
reminders_enabled tinyint,
|
||||
enable_periods tinyint,
|
||||
periods text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
confirmation_enabled tinyint,
|
||||
confirmed_default tinyint,
|
||||
times_along_top tinyint NOT NULL DEFAULT 0,
|
||||
default_type char DEFAULT 'E' NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_area_name (area_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_room
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
disabled tinyint DEFAULT 0 NOT NULL,
|
||||
area_id int DEFAULT 0 NOT NULL,
|
||||
room_name varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
sort_key varchar(25) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
description varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
capacity int DEFAULT 0 NOT NULL,
|
||||
room_admin_email text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
invalid_types varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'JSON encoded',
|
||||
custom_html text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (area_id)
|
||||
REFERENCES mrbs_area(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
UNIQUE KEY uq_room_name (area_id, room_name),
|
||||
KEY idxSortKey (sort_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_repeat
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
start_time int DEFAULT 0 NOT NULL COMMENT 'Unix timestamp',
|
||||
end_time int DEFAULT 0 NOT NULL COMMENT 'Unix timestamp',
|
||||
rep_type int DEFAULT 0 NOT NULL,
|
||||
end_date int DEFAULT 0 NOT NULL COMMENT 'Unix timestamp',
|
||||
rep_opt varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
room_id int DEFAULT 1 NOT NULL,
|
||||
timestamp timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
modified_by varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
name varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
type char DEFAULT 'E' NOT NULL,
|
||||
description text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
rep_interval smallint DEFAULT 1 NOT NULL,
|
||||
month_absolute smallint DEFAULT NULL,
|
||||
month_relative varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
status tinyint unsigned NOT NULL DEFAULT 0,
|
||||
reminded int,
|
||||
info_time int,
|
||||
info_user varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
info_text text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
ical_uid varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
ical_sequence smallint DEFAULT 0 NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (room_id)
|
||||
REFERENCES mrbs_room(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_entry
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
start_time int DEFAULT 0 NOT NULL COMMENT 'Unix timestamp',
|
||||
end_time int DEFAULT 0 NOT NULL COMMENT 'Unix timestamp',
|
||||
entry_type int DEFAULT 0 NOT NULL,
|
||||
repeat_id int DEFAULT NULL,
|
||||
room_id int DEFAULT 1 NOT NULL,
|
||||
timestamp timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
create_by varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
modified_by varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
name varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
type char DEFAULT 'E' NOT NULL,
|
||||
description text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
status tinyint unsigned NOT NULL DEFAULT 0,
|
||||
reminded int,
|
||||
info_time int,
|
||||
info_user varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
info_text text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
ical_uid varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
ical_sequence smallint DEFAULT 0 NOT NULL,
|
||||
ical_recur_id varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
allow_registration tinyint DEFAULT 0 NOT NULL,
|
||||
registrant_limit int DEFAULT 0 NOT NULL,
|
||||
registrant_limit_enabled tinyint DEFAULT 1 NOT NULL,
|
||||
registration_opens int DEFAULT 1209600 NOT NULL COMMENT 'Seconds before the start time', -- 2 weeks
|
||||
registration_opens_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
registration_closes int DEFAULT 0 NOT NULL COMMENT 'Seconds before the start_time',
|
||||
registration_closes_enabled tinyint DEFAULT 0 NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (room_id)
|
||||
REFERENCES mrbs_room(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
FOREIGN KEY (repeat_id)
|
||||
REFERENCES mrbs_repeat(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
KEY idxStartTime (start_time),
|
||||
KEY idxEndTime (end_time),
|
||||
KEY idxRoomStartEnd (room_id, start_time, end_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_participants
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
entry_id int NOT NULL,
|
||||
username varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
create_by varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
registered int,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_entryid_username (entry_id, username),
|
||||
FOREIGN KEY (entry_id)
|
||||
REFERENCES mrbs_entry(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_variables
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
variable_name varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
variable_content text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_variable_name (variable_name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_zoneinfo
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
timezone varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' NOT NULL,
|
||||
outlook_compatible tinyint unsigned NOT NULL DEFAULT 0,
|
||||
vtimezone text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
last_updated int NOT NULL DEFAULT 0,
|
||||
|
||||
/* Note that there is a limit on the length of keys which imposes a constraint
|
||||
on the size of VARCHAR that can be keyed */
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_timezone (timezone, outlook_compatible)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_sessions
|
||||
(
|
||||
id varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
access int unsigned DEFAULT NULL COMMENT 'Unix timestamp',
|
||||
data text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
|
||||
/* Note that there is a limit on the length of keys which imposes a constraint
|
||||
on the size of VARCHAR that can be keyed */
|
||||
PRIMARY KEY (id),
|
||||
KEY idxAccess (access)
|
||||
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE mrbs_users
|
||||
(
|
||||
id int NOT NULL auto_increment,
|
||||
level smallint DEFAULT 0 NOT NULL, /* play safe and give no rights */
|
||||
name varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
display_name varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
password_hash varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
email varchar(75) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
||||
timestamp timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
last_login int DEFAULT '0' NOT NULL,
|
||||
reset_key_hash varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
|
||||
reset_key_expiry int DEFAULT 0 NOT NULL,
|
||||
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_name (name)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
INSERT INTO mrbs_variables (variable_name, variable_content)
|
||||
VALUES ( 'db_version', '82');
|
||||
INSERT INTO mrbs_variables (variable_name, variable_content)
|
||||
VALUES ( 'local_db_version', '1');
|
||||
+256
@@ -0,0 +1,256 @@
|
||||
--
|
||||
-- MRBS table creation script - for PostgreSQL 7.3 and above
|
||||
--
|
||||
-- Notes:
|
||||
-- (1) MySQL inserts the current date/time into any timestamp field which is not
|
||||
-- specified on insert. To get the same effect, use PostgreSQL default
|
||||
-- value current_timestamp.
|
||||
--
|
||||
-- (2) If you have decided to change the prefix of your tables from 'mrbs_'
|
||||
-- to something else using $db_tbl_prefix then you must edit each
|
||||
-- 'CREATE TABLE', 'create index', 'INSERT INTO' and 'REFERENCES' line below
|
||||
-- to replace 'mrbs_' with your new table prefix. A global replace of
|
||||
-- 'mrbs_' will be sufficient.
|
||||
--
|
||||
-- (3) If you add new (standard) fields then you should also change the global variable
|
||||
-- $standard_fields. Note that if you are just adding custom fields for
|
||||
-- a single site then this is not necessary.
|
||||
|
||||
|
||||
CREATE TABLE mrbs_area
|
||||
(
|
||||
-- smallints in mrbs_area are assumed to represent booleans
|
||||
id serial primary key,
|
||||
disabled smallint DEFAULT 0 NOT NULL,
|
||||
area_name varchar(30),
|
||||
sort_key varchar(30) DEFAULT '' NOT NULL,
|
||||
timezone varchar(50),
|
||||
area_admin_email text,
|
||||
resolution int,
|
||||
default_duration int,
|
||||
default_duration_all_day smallint DEFAULT 0 NOT NULL,
|
||||
morningstarts int,
|
||||
morningstarts_minutes int,
|
||||
eveningends int,
|
||||
eveningends_minutes int,
|
||||
private_enabled smallint,
|
||||
private_default smallint,
|
||||
private_mandatory smallint,
|
||||
private_override varchar(32),
|
||||
min_create_ahead_enabled smallint,
|
||||
min_create_ahead_secs int,
|
||||
max_create_ahead_enabled smallint,
|
||||
max_create_ahead_secs int,
|
||||
min_delete_ahead_enabled smallint,
|
||||
min_delete_ahead_secs int,
|
||||
max_delete_ahead_enabled smallint,
|
||||
max_delete_ahead_secs int,
|
||||
max_per_day_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_per_day int DEFAULT 0 NOT NULL,
|
||||
max_per_week_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_per_week int DEFAULT 0 NOT NULL,
|
||||
max_per_month_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_per_month int DEFAULT 0 NOT NULL,
|
||||
max_per_year_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_per_year int DEFAULT 0 NOT NULL,
|
||||
max_per_future_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_per_future int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_day_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_day int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_week_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_week int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_month_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_month int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_year_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_year int DEFAULT 0 NOT NULL,
|
||||
max_secs_per_future_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_secs_per_future int DEFAULT 0 NOT NULL,
|
||||
max_duration_enabled smallint DEFAULT 0 NOT NULL,
|
||||
max_duration_secs int DEFAULT 0 NOT NULL,
|
||||
max_duration_periods int DEFAULT 0 NOT NULL,
|
||||
custom_html text,
|
||||
approval_enabled smallint,
|
||||
reminders_enabled smallint,
|
||||
enable_periods smallint,
|
||||
periods text DEFAULT NULL,
|
||||
confirmation_enabled smallint,
|
||||
confirmed_default smallint,
|
||||
times_along_top smallint DEFAULT 0 NOT NULL,
|
||||
default_type char DEFAULT 'E' NOT NULL,
|
||||
|
||||
CONSTRAINT mrbs_uq_area_name UNIQUE (area_name)
|
||||
);
|
||||
|
||||
CREATE TABLE mrbs_room
|
||||
(
|
||||
id serial primary key,
|
||||
disabled smallint DEFAULT 0 NOT NULL,
|
||||
area_id int DEFAULT 0 NOT NULL
|
||||
REFERENCES mrbs_area(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
room_name varchar(25) NOT NULL,
|
||||
sort_key varchar(25) NOT NULL,
|
||||
description varchar(60),
|
||||
capacity int DEFAULT 0 NOT NULL,
|
||||
room_admin_email text,
|
||||
invalid_types varchar(255) DEFAULT NULL,
|
||||
custom_html text,
|
||||
|
||||
CONSTRAINT mrbs_uq_room_name UNIQUE (area_id, room_name)
|
||||
);
|
||||
comment on column mrbs_room.invalid_types is 'JSON encoded';
|
||||
create index mrbs_idxSortKey on mrbs_room(sort_key);
|
||||
|
||||
CREATE TABLE mrbs_repeat
|
||||
(
|
||||
id serial primary key,
|
||||
start_time int DEFAULT 0 NOT NULL,
|
||||
end_time int DEFAULT 0 NOT NULL,
|
||||
rep_type int DEFAULT 0 NOT NULL,
|
||||
end_date int DEFAULT 0 NOT NULL,
|
||||
rep_opt varchar(32) DEFAULT '' NOT NULL,
|
||||
room_id int DEFAULT 1 NOT NULL
|
||||
REFERENCES mrbs_room(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
timestamp timestamptz DEFAULT current_timestamp,
|
||||
create_by varchar(80) DEFAULT '' NOT NULL,
|
||||
modified_by varchar(80) DEFAULT '' NOT NULL,
|
||||
name varchar(80) DEFAULT '' NOT NULL,
|
||||
type char DEFAULT 'E' NOT NULL,
|
||||
description text,
|
||||
rep_interval smallint DEFAULT 1 NOT NULL,
|
||||
month_absolute smallint DEFAULT NULL,
|
||||
month_relative varchar(4) DEFAULT NULL,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
reminded int,
|
||||
info_time int,
|
||||
info_user varchar(80),
|
||||
info_text text,
|
||||
ical_uid varchar(255) DEFAULT '' NOT NULL,
|
||||
ical_sequence smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
comment on column mrbs_repeat.start_time is 'Unix timestamp';
|
||||
comment on column mrbs_repeat.end_time is 'Unix timestamp';
|
||||
comment on column mrbs_repeat.end_date is 'Unix timestamp';
|
||||
|
||||
CREATE TABLE mrbs_entry
|
||||
(
|
||||
id serial primary key,
|
||||
start_time int DEFAULT 0 NOT NULL,
|
||||
end_time int DEFAULT 0 NOT NULL,
|
||||
entry_type int DEFAULT 0 NOT NULL,
|
||||
repeat_id int DEFAULT NULL
|
||||
REFERENCES mrbs_repeat(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
room_id int DEFAULT 1 NOT NULL
|
||||
REFERENCES mrbs_room(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE RESTRICT,
|
||||
timestamp timestamptz DEFAULT current_timestamp,
|
||||
create_by varchar(80) DEFAULT '' NOT NULL,
|
||||
modified_by varchar(80) DEFAULT '' NOT NULL,
|
||||
name varchar(80) DEFAULT '' NOT NULL,
|
||||
type char DEFAULT 'E' NOT NULL,
|
||||
description text,
|
||||
status smallint DEFAULT 0 NOT NULL,
|
||||
reminded int,
|
||||
info_time int,
|
||||
info_user varchar(80),
|
||||
info_text text,
|
||||
ical_uid varchar(255) DEFAULT '' NOT NULL,
|
||||
ical_sequence smallint DEFAULT 0 NOT NULL,
|
||||
ical_recur_id varchar(16) DEFAULT NULL,
|
||||
allow_registration smallint DEFAULT 0 NOT NULL,
|
||||
registrant_limit int DEFAULT 0 NOT NULL,
|
||||
registrant_limit_enabled smallint DEFAULT 1 NOT NULL,
|
||||
registration_opens int DEFAULT 1209600 NOT NULL, -- 2 weeks
|
||||
registration_opens_enabled smallint DEFAULT 0 NOT NULL,
|
||||
registration_closes int DEFAULT 0 NOT NULL,
|
||||
registration_closes_enabled smallint DEFAULT 0 NOT NULL
|
||||
);
|
||||
comment on column mrbs_entry.start_time is 'Unix timestamp';
|
||||
comment on column mrbs_entry.end_time is 'Unix timestamp';
|
||||
comment on column mrbs_entry.registration_opens is 'Seconds before the start time';
|
||||
comment on column mrbs_entry.registration_closes is 'Seconds before the start time';
|
||||
create index mrbs_idxStartTime on mrbs_entry(start_time);
|
||||
create index mrbs_idxEndTime on mrbs_entry(end_time);
|
||||
create index mrbs_idxRoomStartEnd on mrbs_entry(room_id, start_time, end_time);
|
||||
|
||||
CREATE TABLE mrbs_participants
|
||||
(
|
||||
id serial primary key,
|
||||
entry_id int NOT NULL
|
||||
REFERENCES mrbs_entry(id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE,
|
||||
username varchar(191),
|
||||
create_by varchar(255),
|
||||
registered int,
|
||||
|
||||
CONSTRAINT mrbs_uq_entryid_username UNIQUE (entry_id, username)
|
||||
);
|
||||
|
||||
CREATE TABLE mrbs_variables
|
||||
(
|
||||
id serial primary key,
|
||||
variable_name varchar(80),
|
||||
variable_content text,
|
||||
|
||||
CONSTRAINT mrbs_uq_variable_name UNIQUE (variable_name)
|
||||
);
|
||||
|
||||
CREATE TABLE mrbs_zoneinfo
|
||||
(
|
||||
id serial primary key,
|
||||
timezone varchar(127) DEFAULT '' NOT NULL,
|
||||
outlook_compatible smallint NOT NULL DEFAULT 0,
|
||||
vtimezone text,
|
||||
last_updated int NOT NULL DEFAULT 0,
|
||||
|
||||
CONSTRAINT mrbs_uq_timezone UNIQUE (timezone, outlook_compatible)
|
||||
);
|
||||
|
||||
CREATE TABLE mrbs_sessions
|
||||
(
|
||||
id varchar(191) NOT NULL primary key,
|
||||
access int DEFAULT NULL,
|
||||
data text DEFAULT NULL
|
||||
);
|
||||
comment on column mrbs_sessions.access is 'Unix timestamp';
|
||||
create index mrbs_idxAccess on mrbs_sessions(access);
|
||||
|
||||
CREATE TABLE mrbs_users
|
||||
(
|
||||
id serial primary key,
|
||||
level smallint DEFAULT 0 NOT NULL, /* play safe and give no rights */
|
||||
name varchar(30),
|
||||
display_name varchar(191),
|
||||
password_hash varchar(255),
|
||||
email varchar(75),
|
||||
timestamp timestamptz DEFAULT current_timestamp,
|
||||
last_login int DEFAULT 0 NOT NULL,
|
||||
reset_key_hash varchar(255),
|
||||
reset_key_expiry int DEFAULT 0 NOT NULL,
|
||||
|
||||
CONSTRAINT mrbs_uq_name UNIQUE (name)
|
||||
);
|
||||
|
||||
CREATE OR REPLACE FUNCTION update_timestamp_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.timestamp = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_mrbs_entry_timestamp BEFORE UPDATE ON mrbs_entry FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column();
|
||||
CREATE TRIGGER update_mrbs_repeat_timestamp BEFORE UPDATE ON mrbs_repeat FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column();
|
||||
CREATE TRIGGER update_mrbs_users_timestamp BEFORE UPDATE ON mrbs_users FOR EACH ROW EXECUTE PROCEDURE update_timestamp_column();
|
||||
|
||||
INSERT INTO mrbs_variables (variable_name, variable_content)
|
||||
VALUES ('db_version', '82');
|
||||
INSERT INTO mrbs_variables (variable_name, variable_content)
|
||||
VALUES ('local_db_version', '1');
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
|
||||
function test_constants($class)
|
||||
{
|
||||
echo "<h1>Testing constants</h1>\n";
|
||||
$passed = true;
|
||||
$php_constants = (new \ReflectionClass($class))->getConstants();
|
||||
$emulation_constants = (new \ReflectionClass("MRBS\Intl\\$class"))->getConstants();
|
||||
foreach ($php_constants as $name => $value)
|
||||
{
|
||||
// We are only interested in public constants
|
||||
if (!(new \ReflectionClassConstant($class, $name))->isPublic())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isset($emulation_constants[$name]))
|
||||
{
|
||||
$passed = false;
|
||||
echo "Failed to find constant $name ($value)<br>\n";
|
||||
}
|
||||
else if ($value != $emulation_constants[$name])
|
||||
{
|
||||
$passed = false;
|
||||
echo "Constant $name has different value in PHP ($value) and MRBS ($emulation_constants[$name])<br>\n";
|
||||
}
|
||||
}
|
||||
if ($passed)
|
||||
{
|
||||
echo "Passed<br>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function thead_html(array $arg_names) : string
|
||||
{
|
||||
$html = "<thead>\n";
|
||||
$html .= '<tr>';
|
||||
$html .= '<th>function</th>';
|
||||
foreach ($arg_names as $arg_name)
|
||||
{
|
||||
$html .= '<th>$' . escape_html($arg_name) . '</th>';
|
||||
}
|
||||
$html .= '<th>Result - PHP</th><th>Result - MRBS</th><th>Summary</th>';
|
||||
$html .= "<tr>\n";
|
||||
$html .= "</thead>\n";
|
||||
|
||||
return $html;
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
|
||||
include 'defaultincludes.inc';
|
||||
require_once 'functions_test.inc';
|
||||
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$color_fail = 'pink';
|
||||
$color_pass = 'palegreen';
|
||||
|
||||
|
||||
function do_asort(
|
||||
string $locale,
|
||||
array &$array,
|
||||
int $flags=\Collator::SORT_REGULAR,
|
||||
int $numeric_collation = \Collator::DEFAULT_VALUE
|
||||
)
|
||||
{
|
||||
global $color_fail, $color_pass;
|
||||
|
||||
$php_collator = new \Collator($locale);
|
||||
$php_collator->setAttribute(\Collator::NUMERIC_COLLATION, $numeric_collation);
|
||||
$mrbs_collator = new \MRBS\Intl\Collator($locale);
|
||||
$mrbs_collator->setAttribute(\MRBS\Intl\Collator::NUMERIC_COLLATION, $numeric_collation);
|
||||
|
||||
$strengths = [
|
||||
\Collator::PRIMARY,
|
||||
\Collator::SECONDARY,
|
||||
\Collator::TERTIARY,
|
||||
\Collator::QUATERNARY,
|
||||
\Collator::IDENTICAL
|
||||
];
|
||||
|
||||
foreach ($strengths as $strength)
|
||||
{
|
||||
$php_collator->setStrength($strength);
|
||||
$mrbs_collator->setStrength($strength);
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>asort</td>";
|
||||
echo "<td>" . escape_html($locale) . "</td>";
|
||||
echo "<td>" . implode(',', $array) . "</td>";
|
||||
echo "<td>" . escape_html($flags) . "</td>";
|
||||
echo "<td>" . $php_collator->getAttribute(\Collator::NUMERIC_COLLATION) . "</td>";
|
||||
echo "<td>" . $php_collator->getStrength() . "</td>";
|
||||
|
||||
$php_array = $array;
|
||||
$mrbs_array = $array;
|
||||
$php_collator->asort($php_array, $flags);
|
||||
$mrbs_collator->asort($mrbs_array, $flags);
|
||||
|
||||
echo "<td>[" . implode(',', array_keys($php_array)) . '] [' . implode(',', array_values($php_array)) . "]</td>";
|
||||
echo "<td>[" . implode(',', array_keys($mrbs_array)) . '] [' . implode(',', array_values($mrbs_array)) . "]</td>";
|
||||
|
||||
// Compare the results
|
||||
$passed = ($php_array === $mrbs_array);
|
||||
$color = ($passed) ? $color_pass : $color_fail;
|
||||
echo '<td style="background-color: ' . $color . '">';
|
||||
echo ($passed) ? 'Pass' : 'Fail';
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>\n";
|
||||
}
|
||||
echo "<tr><td colspan=\"9\"></td></tr>\n";
|
||||
}
|
||||
|
||||
function test_asort()
|
||||
{
|
||||
echo "<h1>Testing asort()</h1>\n";
|
||||
|
||||
echo "<table>\n";
|
||||
echo thead_html(['locale', 'array', 'flags', 'numeric_collation', 'strength']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
$locale = 'en-US';
|
||||
$array = ['aò', 'Ao', 'ao'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'en-US';
|
||||
$array = ['a', 'b', 'A', 'B'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'en-US';
|
||||
$array = ['aBc', 'abC', 'Abc', 'ABc'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'sv';
|
||||
$array = ['ö', 'ä', 'å'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'sv-SE';
|
||||
$array = ['ö', 'ä', 'å'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'sv';
|
||||
$array = ['ö', 'ä', 'å', 'o', 'a', 'e'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$locale = 'en-US';
|
||||
$array = ['a10', 'b2', 'A2', 'B10'];
|
||||
do_asort($locale, $array);
|
||||
do_asort($locale, $array, \Collator::SORT_NUMERIC);
|
||||
$array = ['a1', 'a2', 'a10', 'b2', 'b10'];
|
||||
do_asort($locale, $array, \Collator::SORT_NUMERIC);
|
||||
|
||||
$numeric_collation = \Collator::ON;
|
||||
$locale = 'fr';
|
||||
$array = ['1', '2', '10'];
|
||||
do_asort($locale, $array, \Collator::SORT_REGULAR, $numeric_collation);
|
||||
do_asort($locale, $array, \Collator::SORT_NUMERIC, $numeric_collation);
|
||||
|
||||
$array = ['a', 'à', 'â', 'e', 'é'];
|
||||
do_asort($locale, $array);
|
||||
$array = array_reverse($array);
|
||||
do_asort($locale, $array);
|
||||
|
||||
$array = ['a', 'A'];
|
||||
do_asort($locale, $array);
|
||||
$array = array_reverse($array);
|
||||
do_asort($locale, $array);
|
||||
|
||||
$array = ['Ba', 'aB'];
|
||||
do_asort($locale, $array);
|
||||
$array = array_reverse($array);
|
||||
do_asort($locale, $array);
|
||||
|
||||
$array = ['ABC', 'aBc', 'Abc', 'Abc', 'ABc'];
|
||||
do_asort($locale, $array);
|
||||
|
||||
$array = ['ABC', 'aBc', 'Abc', 'Abc', 'ABc'];
|
||||
do_asort($locale, $array, \Collator::SORT_STRING);
|
||||
|
||||
$locale = 'no';
|
||||
$array = ['æ', 'ø', 'å', 'A', 'AA', 'AB', 'Åb', 'åb'];
|
||||
do_asort($locale, $array, \Collator::SORT_REGULAR, $numeric_collation);
|
||||
|
||||
$locale = 'sv';
|
||||
$array = ['ä', 'ö', 'å', 'A', 'AA', 'Åb', 'åb'];
|
||||
do_asort($locale, $array, \Collator::SORT_REGULAR, $numeric_collation);
|
||||
|
||||
$locale = 'en-GB';
|
||||
$array = ['a', 'b', 'c', 'A', 'B', 'aa', 'Aa', 'AB', 'z', 'zb'];
|
||||
do_asort($locale, $array, \Collator::SORT_REGULAR, $numeric_collation);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
|
||||
function do_compare(string $locale, string $string1, string $string2, int $strength=\Collator::DEFAULT_STRENGTH)
|
||||
{
|
||||
global $color_fail, $color_pass;
|
||||
|
||||
$php_collator = new \Collator($locale);
|
||||
$php_collator->setStrength($strength);
|
||||
$mrbs_collator = new \MRBS\Intl\Collator($locale);
|
||||
$mrbs_collator->setStrength($strength);
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>compare</td>";
|
||||
echo "<td>" . escape_html($locale) . "</td>";
|
||||
echo "<td>" . escape_html($string1) . "</td>";
|
||||
echo "<td>" . escape_html($string2) . "</td>";
|
||||
echo "<td>" . escape_html($strength) . "</td>";
|
||||
|
||||
$php_compare = $php_collator->compare($string1, $string2);
|
||||
$mrbs_compare = $mrbs_collator->compare($string1, $string2);
|
||||
|
||||
echo "<td>$php_compare</td>";
|
||||
echo "<td>$mrbs_compare</td>";
|
||||
|
||||
// Compare the results
|
||||
$passed = ($php_compare === $mrbs_compare);
|
||||
$color = ($passed) ? $color_pass : $color_fail;
|
||||
echo '<td style="background-color: ' . $color . '">';
|
||||
echo ($passed) ? 'Pass' : 'Fail';
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
|
||||
function test_compare()
|
||||
{
|
||||
echo "<h1>Testing compare()</h1>\n";
|
||||
|
||||
echo "<table>\n";
|
||||
echo thead_html(['locale', 'string1', 'string2', 'strength']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
$tests = [
|
||||
['locale' => 'fr', 'string1' => 'é', 'string2' => 'è'],
|
||||
['locale' => 'fr', 'string1' => 'è', 'string2' => 'é'],
|
||||
['locale' => 'en-GB', 'string1' => 'é', 'string2' => 'è'],
|
||||
['locale' => 'en-GB', 'string1' => 'Séan', 'string2' => 'Sean'],
|
||||
['locale' => 'en-GB', 'string1' => 'a', 'string2' => 'A'],
|
||||
['locale' => 'en-GB', 'string1' => 'A', 'string2' => 'a'],
|
||||
['locale' => 'en-GB', 'string1' => 'a', 'string2' => 'b'],
|
||||
['locale' => 'en-GB', 'string1' => 'bA', 'string2' => 'Ba']
|
||||
];
|
||||
|
||||
$strengths = [
|
||||
\Collator::PRIMARY,
|
||||
\Collator::SECONDARY,
|
||||
\Collator::TERTIARY,
|
||||
\Collator::QUATERNARY,
|
||||
\Collator::IDENTICAL
|
||||
];
|
||||
|
||||
foreach ($tests as $test)
|
||||
{
|
||||
list('locale' => $locale, 'string1' => $string1, 'string2' => $string2) = $test;
|
||||
foreach ($strengths as $strength)
|
||||
{
|
||||
do_compare($locale, $string1, $string2, $strength);
|
||||
}
|
||||
echo "<tr><td colspan=\"8\"></td></tr>\n";
|
||||
}
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
|
||||
$loaded_extensions = get_loaded_extensions();
|
||||
|
||||
echo "PHP version: " . PHP_VERSION;
|
||||
echo "<br>\n";
|
||||
echo "mbstring enabled: " . var_export(in_array('mbstring', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "intl enabled: " . var_export(in_array('intl', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "<br>\n";
|
||||
|
||||
if (!in_array('intl', $loaded_extensions))
|
||||
{
|
||||
die("This test needs the 'intl' PHP extension to be loaded.");
|
||||
}
|
||||
|
||||
test_constants('Collator');
|
||||
test_asort();
|
||||
test_compare();
|
||||
@@ -0,0 +1,504 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// Program for testing the mbstring function emulations. Run it in the MRBS directory on a
|
||||
// system with the 'mbstring' extension enabled.
|
||||
|
||||
use IntlChar;
|
||||
use MRBS\Mbstring\Mbstring;
|
||||
use Throwable;
|
||||
|
||||
include 'defaultincludes.inc';
|
||||
require_once 'functions_test.inc';
|
||||
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('max_execution_time', '120');
|
||||
|
||||
$color_fail = 'pink';
|
||||
$color_pass = 'palegreen';
|
||||
|
||||
$intl_loaded = method_exists('\IntlChar', 'charName');
|
||||
$max_codepoint = 0x10FFFF;
|
||||
|
||||
|
||||
function test_chr() : void
|
||||
{
|
||||
global $color_fail, $intl_loaded, $max_codepoint;
|
||||
|
||||
$n_passed = 0;
|
||||
$failures = [];
|
||||
|
||||
for ($i =0; $i<=$max_codepoint; $i++)
|
||||
{
|
||||
$mb = mb_chr($i, 'UTF-8');
|
||||
$mrbs = Mbstring::mb_chr($i, 'UTF-8');
|
||||
if ($mb === $mrbs)
|
||||
{
|
||||
$n_passed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$failures[] = [$i, $mb, $mrbs];
|
||||
}
|
||||
}
|
||||
|
||||
echo "<p>$n_passed codepoints passed, " . count ($failures) . " failed.</p>\n";
|
||||
|
||||
if (!empty($failures))
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo "<thead>\n";
|
||||
echo '<tr>';
|
||||
echo '<th colspan="' . (($intl_loaded) ? 2 : 1) . '">Codepoint</th>';
|
||||
echo '<th>mbstring</th><th>mrbs</th><th>Summary</th>';
|
||||
echo "</tr>\n";
|
||||
echo "</thead>\n";
|
||||
echo "<tbody>\n";
|
||||
|
||||
foreach ($failures as $failure)
|
||||
{
|
||||
echo '<tr>';
|
||||
if ($intl_loaded)
|
||||
{
|
||||
echo '<td>' . IntlChar::charName($failure[0]) . '</td>';
|
||||
}
|
||||
foreach ($failure as $value)
|
||||
{
|
||||
echo "<td>$value</td>";
|
||||
}
|
||||
echo '<td style="background-color: ' . $color_fail . '">Fail</td>' . "\n";
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function test_ord() : void
|
||||
{
|
||||
global $color_fail, $intl_loaded, $max_codepoint;
|
||||
|
||||
$n_passed = 0;
|
||||
$failures = [];
|
||||
|
||||
for ($i =0; $i<=$max_codepoint; $i++)
|
||||
{
|
||||
$str = mb_chr($i, 'UTF-8');
|
||||
if (($str !== false) && mb_check_encoding($str, 'UTF-8'))
|
||||
{
|
||||
$mrbs_ord = Mbstring::mb_ord($str);
|
||||
if ($mrbs_ord === mb_ord($str))
|
||||
{
|
||||
$n_passed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$failures[] = [$str, $i, $mrbs_ord];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<p>$n_passed codepoints passed, " . count ($failures) . " failed.</p>\n";
|
||||
|
||||
if (!empty($failures))
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo "<thead>\n";
|
||||
echo '<tr>';
|
||||
echo '<th colspan="' . (($intl_loaded) ? 2 : 1) . '">Char</th>';
|
||||
echo '<th>mbstring</th><th>mrbs</th><th>Summary</th>';
|
||||
echo "</tr>\n";
|
||||
echo "</thead>\n";
|
||||
echo "<tbody>\n";
|
||||
|
||||
foreach ($failures as $failure)
|
||||
{
|
||||
echo '<tr>';
|
||||
if ($intl_loaded)
|
||||
{
|
||||
echo '<td>' . IntlChar::charName(mb_ord($failure[0])) . '</td>';
|
||||
}
|
||||
foreach ($failure as $value)
|
||||
{
|
||||
echo "<td>$value</td>";
|
||||
}
|
||||
echo '<td style="background-color: ' . $color_fail . '">Fail</td>' . "\n";
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function codepoint_notation(int $codepoint) : string
|
||||
{
|
||||
// OK to user strtoupper here instead of mb_ because we're only looking at the hex characters
|
||||
return 'U+' . str_pad(strtoupper(dechex($codepoint)), 4, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
|
||||
function test(string $function, $args) : void
|
||||
{
|
||||
global $color_fail, $color_pass;
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>$function</td>";
|
||||
|
||||
foreach ($args as $arg)
|
||||
{
|
||||
echo "<td>$arg</td>";
|
||||
}
|
||||
|
||||
// Using the mbstring versions
|
||||
echo "<td>";
|
||||
try {
|
||||
$mbstring = call_user_func_array($function, $args);
|
||||
}
|
||||
catch (Throwable $t) {
|
||||
$mbstring = get_class($t);
|
||||
}
|
||||
echo var_export($mbstring, true);
|
||||
echo "</td>";
|
||||
|
||||
// Using the MRBS emulations
|
||||
echo "<td>";
|
||||
try {
|
||||
$mrbs = call_user_func_array([__NAMESPACE__ . "\\Mbstring\\Mbstring", $function], $args);
|
||||
}
|
||||
catch (Throwable $t) {
|
||||
$mrbs = get_class($t);
|
||||
}
|
||||
echo var_export($mrbs, true);
|
||||
echo "</td>";
|
||||
|
||||
// Compare the results
|
||||
$color = ($mbstring === $mrbs) ? $color_pass : $color_fail;
|
||||
echo '<td style="background-color: ' . $color . '">';
|
||||
echo ($mbstring === $mrbs) ? 'Pass' : 'Fail';
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
|
||||
function test_strlen() : void
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo thead_html(['string', 'encoding']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
// Simple case
|
||||
test('mb_strlen', ['abcd', 'UTF-8']);
|
||||
// Multibyte
|
||||
test('mb_strlen', ['會議室預約系統', 'UTF-8']);
|
||||
test('mb_strlen', ['emojis 😀😨🙁', 'UTF-8']);
|
||||
// Empty string
|
||||
test('mb_strlen', ['', 'UTF-8']);
|
||||
|
||||
// 8bit testing
|
||||
test('mb_strlen', ['', '8bit']);
|
||||
test('mb_strlen', ['&', '8bit']);
|
||||
test('mb_strlen', ['å', '8bit']);
|
||||
test('mb_strlen', ['議', '8bit']);
|
||||
test('mb_strlen', ['👽', '8bit']);
|
||||
test('mb_strlen', ['z👽', '8bit']);
|
||||
test('mb_strlen', ['åäö', '8bit']);
|
||||
test('mb_strlen', ['👽統', '8bit']);
|
||||
test('mb_strlen', ['👿🤩', '8bit']);
|
||||
test('mb_strlen', ['系統åg', '8bit']);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
|
||||
function test_all_codepoints(string $function) : void
|
||||
{
|
||||
global $color_fail, $intl_loaded, $max_codepoint;
|
||||
|
||||
echo "<h3>Testing all codepoints</h3>\n";
|
||||
|
||||
$n_passed = 0;
|
||||
$failures = [];
|
||||
|
||||
for ($i =0; $i<=$max_codepoint; $i++)
|
||||
{
|
||||
$str = mb_chr($i, 'UTF-8');
|
||||
if (($str !== false) && mb_check_encoding($str, 'UTF-8'))
|
||||
{
|
||||
$mb = call_user_func($function, $str);
|
||||
$mrbs = call_user_func([__NAMESPACE__ . "\\Mbstring\\Mbstring", $function], $str);
|
||||
if ($mb === $mrbs)
|
||||
{
|
||||
$n_passed++;
|
||||
}
|
||||
else
|
||||
{
|
||||
$failures[] = [$str, $mb, $mrbs];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
echo "<p>$n_passed codepoints passed, " . count ($failures) . " failed.</p>\n";
|
||||
|
||||
if (!empty($failures))
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo "<thead>\n";
|
||||
echo '<tr>';
|
||||
echo '<th colspan="' . (($intl_loaded) ? 3 : 2) . '">Codepoint</th>';
|
||||
echo '<th colspan="2">mbstring</th><th colspan="2">mrbs</th><th>Summary</th>';
|
||||
echo "</tr>\n";
|
||||
echo "</thead>\n";
|
||||
echo "<tbody>\n";
|
||||
|
||||
foreach ($failures as $failure)
|
||||
{
|
||||
echo '<tr>';
|
||||
if ($intl_loaded)
|
||||
{
|
||||
echo '<td>' . IntlChar::charName(mb_ord($failure[0])) . '</td>';
|
||||
}
|
||||
foreach ($failure as $char)
|
||||
{
|
||||
echo "<td>$char</td><td>" . codepoint_notation(mb_ord($char)) . '</td>';
|
||||
}
|
||||
echo '<td style="background-color: ' . $color_fail . '">Fail</td>' . "\n";
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
}
|
||||
|
||||
function test_strtolower() : void
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo thead_html(['string']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
// Empty string
|
||||
test('mb_strtolower', ['']);
|
||||
// Simple string
|
||||
test('mb_strtolower', ['ABcDeFgHI']);
|
||||
// More complex
|
||||
test('mb_strtolower', ['AÅÄÖ']);
|
||||
// Turkish characters
|
||||
test('mb_strtolower', ['CÇGĞIİSŞ']);
|
||||
test('mb_strtolower', ['İ']);
|
||||
// Other
|
||||
test('mb_strtolower', ['Τάχιστη αλώπηξ βαφής']);
|
||||
test('mb_strtolower', ['👽系😨z😎éÉ']);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
|
||||
test_all_codepoints('mb_strtolower');
|
||||
}
|
||||
|
||||
|
||||
function test_strtoupper() : void
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo thead_html(['string']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
// Empty string
|
||||
test('mb_strtoupper', ['']);
|
||||
// Simple string
|
||||
test('mb_strtoupper', ['ABcDeFgHI']);
|
||||
// More complex
|
||||
test('mb_strtoupper', ['aåäö']);
|
||||
// Turkish characters
|
||||
test('mb_strtoupper', ['cçgğiiı̇sş']);
|
||||
// Other
|
||||
test('mb_strtoupper', ['Τάχιστη αλώπηξ βαφής']);
|
||||
test('mb_strtoupper', ['👽系😨z😎éÉ']);
|
||||
// These fail with Transliterator
|
||||
test('mb_strtoupper', ['ƛɤ']);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
|
||||
test_all_codepoints('mb_strtoupper');
|
||||
}
|
||||
|
||||
|
||||
function test_substr() : void
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo thead_html(['string', 'start', 'length']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
// Empty string
|
||||
test('mb_substr', ['', 0, null]);
|
||||
test('mb_substr', ['', 1, null]);
|
||||
test('mb_substr', ['', 0, 2]);
|
||||
test('mb_substr', ['', 1, 2]);
|
||||
|
||||
// Multibyte
|
||||
test('mb_substr', ['👽系😨z😎é', 0, null]);
|
||||
test('mb_substr', ['👽系😨z😎é', 2, null]);
|
||||
test('mb_substr', ['👽系😨z😎é', 2, 1]);
|
||||
test('mb_substr', ['👽系😨z😎é', 2, -1]);
|
||||
test('mb_substr', ['👽系😨z😎é', 2, -5]);
|
||||
test('mb_substr', ['👽系😨z😎é', -1, null]);
|
||||
test('mb_substr', ['👽系😨z😎é', -3, -1]);
|
||||
test('mb_substr', ['👽系😨z😎é', -9, -1]);
|
||||
test('mb_substr', ['👽系😨z😎é', -9, -12]);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
|
||||
function test_pos() : void
|
||||
{
|
||||
echo "<table>\n";
|
||||
echo thead_html(['haystack', 'needle', 'offset']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
// mb_strpos()
|
||||
// -----------
|
||||
|
||||
test('mb_strpos', ['0123456789a0123456789b0123456789c', 'c', 0]);
|
||||
test('mb_strpos', ['0123456789a0123456789b0123456789c', 'd', 0]);
|
||||
test('mb_strpos', ['0123456789a0123456789b0123456789c', 'c', -1]);
|
||||
test('mb_strpos', ['0123456789a0123456789b0123456789c', 'c', -2]);
|
||||
|
||||
test('mb_strpos', ['TRUE', 'E_', 0]);
|
||||
|
||||
// Equivalence
|
||||
test('mb_strpos', ['Jour précédent', 'e', 0]);
|
||||
$old_locale = setlocale(LC_ALL, '0');
|
||||
setlocale(LC_ALL, ['fr_FR', 'fr']);
|
||||
test('mb_strpos', ['Jour précédent', 'e', 0]);
|
||||
setlocale(LC_ALL, $old_locale);
|
||||
|
||||
|
||||
// mb_stripos()
|
||||
// -----------
|
||||
|
||||
test('mb_stripos', ['0123456789a0123456789b0123456789c', 'c', -1]);
|
||||
test('mb_stripos', ['0123456789a0123456789b0123456789c', 'C', -1]);
|
||||
|
||||
// Multibyte
|
||||
test('mb_stripos', ['會C議室預約系統', 'C', 2]);
|
||||
test('mb_stripos', ['會議C室預約系統', 'C', 2]);
|
||||
test('mb_stripos', ['會議C室預約系統', '預約', 2]);
|
||||
|
||||
|
||||
// mb_strrpos()
|
||||
// ------------
|
||||
|
||||
// Positive offsets, needle at start
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '0123456789a', 0]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '0123456789a', 1]);
|
||||
|
||||
// Positive offsets, needle partial match at the end
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '89cd', 10]);
|
||||
|
||||
// Positive offsets, needle longer than search area
|
||||
test('mb_strrpos', ['abcde', 'cde', 2]);
|
||||
test('mb_strrpos', ['abcde', 'cde', 3]);
|
||||
|
||||
// Negative offsets, needle longer than search area
|
||||
test('mb_strrpos', ['abcdefg', 'cdefghi', -2]);
|
||||
test('mb_strrpos', ['abcdefg', 'cdefghij', -2]);
|
||||
|
||||
// Negative offsets, needle in the middle of haystack
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', 0]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', -1]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', -2]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', -3]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', -9]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '234', -10]);
|
||||
|
||||
// Negative offsets, needle at the end of haystack
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', 0]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', -1]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', -2]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', -3]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', -4]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789c', -5]);
|
||||
|
||||
// Negative offsets, needle partial match at the end
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789cd', 0]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789cd', -1]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789cd', -2]);
|
||||
test('mb_strrpos', ['0123456789a0123456789b0123456789c', '789cd', -3]);
|
||||
|
||||
// Multibyte
|
||||
test('mb_strrpos', ['會🙂議C室🙂預約系統', '🙂', -1]);
|
||||
|
||||
// Empty haystack
|
||||
test('mb_strrpos', ['', 'A', 0]);
|
||||
|
||||
// Case sensitivity
|
||||
test('mb_strrpos', ['AaBb', 'a', 0]);
|
||||
test('mb_strrpos', ['AaBb', 'A', 0]);
|
||||
|
||||
// Offset outside haystack
|
||||
test('mb_strrpos', ['', 'A', 1]);
|
||||
test('mb_strrpos', ['A', 'A', 2]);
|
||||
test('mb_strrpos', ['A', 'A', -2]);
|
||||
|
||||
|
||||
// mb_strripos()
|
||||
// ------------
|
||||
|
||||
// Case sensitivity
|
||||
test('mb_strripos', ['AaBb', 'a', 0]);
|
||||
test('mb_strripos', ['AaBb', 'A', 0]);
|
||||
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
echo "<h1>mbstring emulation tests</h1>\n";
|
||||
|
||||
$loaded_extensions = get_loaded_extensions();
|
||||
|
||||
echo "PHP version: " . PHP_VERSION;
|
||||
echo "<br>\n";
|
||||
echo "mbstring enabled: " . var_export(in_array('mbstring', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "intl enabled: " . var_export(in_array('intl', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "iconv enabled: " . var_export(in_array('iconv', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "<br>\n";
|
||||
|
||||
if (!in_array('mbstring', $loaded_extensions))
|
||||
{
|
||||
die("This test needs the 'mbstring' PHP extension to be loaded.");
|
||||
}
|
||||
|
||||
echo "<h2>mb_chr()</h2>\n";
|
||||
test_chr();
|
||||
|
||||
echo "<h2>mb_ord()</h2>\n";
|
||||
test_ord();
|
||||
|
||||
echo "<h2>mb_strlen()</h2>\n";
|
||||
test_strlen();
|
||||
|
||||
echo "<h2>mb_strtolower()</h2>\n";
|
||||
test_strtolower();
|
||||
|
||||
echo "<h2>mb_strtoupper()</h2>\n";
|
||||
test_strtoupper();
|
||||
|
||||
echo "<h2>mb_substr()</h2>\n";
|
||||
test_substr();
|
||||
|
||||
echo "<h2>mb_*pos()</h2>\n";
|
||||
test_pos();
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
|
||||
include 'defaultincludes.inc';
|
||||
require_once 'functions_test.inc';
|
||||
|
||||
error_reporting(-1);
|
||||
ini_set('display_errors', '1');
|
||||
|
||||
$color_fail = 'pink';
|
||||
$color_pass = 'palegreen';
|
||||
|
||||
|
||||
function do_format(string $locale, $num, int $style)
|
||||
{
|
||||
global $color_fail, $color_pass;
|
||||
|
||||
$php_formatter = new \NumberFormatter($locale, $style);
|
||||
$mrbs_formatter = new \MRBS\Intl\NumberFormatter($locale, $style);
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>compare</td>";
|
||||
echo "<td>" . escape_html($locale) . "</td>";
|
||||
echo "<td>" . escape_html($num) . "</td>";
|
||||
echo "<td>" . escape_html($style) . "</td>";
|
||||
|
||||
$php_format= $php_formatter->format($num);
|
||||
$mrbs_format = $mrbs_formatter->format($num);
|
||||
|
||||
echo "<td>$php_format</td>";
|
||||
echo "<td>$mrbs_format</td>";
|
||||
|
||||
// Compare the results
|
||||
$passed = ($php_format === $mrbs_format);
|
||||
$color = ($passed) ? $color_pass : $color_fail;
|
||||
echo '<td style="background-color: ' . $color . '">';
|
||||
echo ($passed) ? 'Pass' : 'Fail';
|
||||
echo "</td>";
|
||||
|
||||
echo "</tr>\n";
|
||||
}
|
||||
|
||||
|
||||
function test_format()
|
||||
{
|
||||
echo "<h1>Testing format()</h1>\n";
|
||||
|
||||
echo "<table>\n";
|
||||
echo thead_html(['locale', 'num', 'style']);
|
||||
echo "<tbody>\n";
|
||||
|
||||
do_format('en', 1000000, \NumberFormatter::DEFAULT_STYLE);
|
||||
do_format('fr', 1000000, \NumberFormatter::DEFAULT_STYLE);
|
||||
do_format('de', 1000000, \NumberFormatter::DEFAULT_STYLE);
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
}
|
||||
|
||||
|
||||
$loaded_extensions = get_loaded_extensions();
|
||||
|
||||
echo "PHP version: " . PHP_VERSION;
|
||||
echo "<br>\n";
|
||||
echo "mbstring enabled: " . var_export(in_array('mbstring', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "intl enabled: " . var_export(in_array('intl', $loaded_extensions), true);
|
||||
echo "<br>\n";
|
||||
echo "<br>\n";
|
||||
|
||||
if (!in_array('intl', $loaded_extensions))
|
||||
{
|
||||
die("This test needs the 'intl' PHP extension to be loaded.");
|
||||
}
|
||||
|
||||
test_constants('NumberFormatter');
|
||||
test_format();
|
||||
@@ -0,0 +1,3 @@
|
||||
<Files ~ "\.inc$">
|
||||
Require all denied
|
||||
</Files>
|
||||
@@ -0,0 +1,4 @@
|
||||
<Files ~ "\.inc$">
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</Files>
|
||||
@@ -0,0 +1,411 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
require_once 'File/Passwd/Exception.php';
|
||||
|
||||
/**
|
||||
* Encryption constants.
|
||||
*/
|
||||
// SHA encryption.
|
||||
define('FILE_PASSWD_SHA', 'sha');
|
||||
// MD5 encryption
|
||||
define('FILE_PASSWD_MD5', 'md5');
|
||||
// DES encryption
|
||||
define('FILE_PASSWD_DES', 'des');
|
||||
// NT hash encryption.
|
||||
define('FILE_PASSWD_NT', 'nt');
|
||||
// LM hash encryption.
|
||||
define('FILE_PASSWD_LM', 'lm');
|
||||
// PLAIN (no encryption)
|
||||
define('FILE_PASSWD_PLAIN', 'plain');
|
||||
|
||||
/**
|
||||
* Error constants.
|
||||
*/
|
||||
// Undefined error.
|
||||
define('FILE_PASSWD_E_UNDEFINED', 0);
|
||||
// Invalid file format.
|
||||
define('FILE_PASSWD_E_INVALID_FORMAT', 1);
|
||||
define('FILE_PASSWD_E_INVALID_FORMAT_STR', 'Passwd file has invalid format.');
|
||||
// Invalid extra property.
|
||||
define('FILE_PASSWD_E_INVALID_PROPERTY', 2);
|
||||
define('FILE_PASSWD_E_INVALID_PROPERTY_STR', 'Invalid property \'%s\'.');
|
||||
// Invalid characters.
|
||||
define('FILE_PASSWD_E_INVALID_CHARS', 3);
|
||||
define('FILE_PASSWD_E_INVALID_CHARS_STR', '%s\'%s\' contains illegal characters.');
|
||||
// Invalid encryption mode.
|
||||
define('FILE_PASSWD_E_INVALID_ENC_MODE', 4);
|
||||
define('FILE_PASSWD_E_INVALID_ENC_MODE_STR', 'Encryption mode \'%s\' not supported.');
|
||||
// Exists already.
|
||||
define('FILE_PASSWD_E_EXISTS_ALREADY', 5);
|
||||
define('FILE_PASSWD_E_EXISTS_ALREADY_STR', '%s\'%s\' already exists.');
|
||||
// Exists not.
|
||||
define('FILE_PASSWD_E_EXISTS_NOT', 6);
|
||||
define('FILE_PASSWD_E_EXISTS_NOT_STR', '%s\'%s\' doesn\'t exist.');
|
||||
// User not in group.
|
||||
define('FILE_PASSWD_E_USER_NOT_IN_GROUP', 7);
|
||||
define('FILE_PASSWD_E_USER_NOT_IN_GROUP_STR', 'User \'%s\' doesn\'t exist in group \'%s\'.');
|
||||
// User not in realm.
|
||||
define('FILE_PASSWD_E_USER_NOT_IN_REALM', 8);
|
||||
define('FILE_PASSWD_E_USER_NOT_IN_REALM_STR', 'User \'%s\' doesn\'t exist in realm \'%s\'.');
|
||||
// Parameter must be of type array.
|
||||
define('FILE_PASSWD_E_PARAM_MUST_BE_ARRAY', 9);
|
||||
define('FILE_PASSWD_E_PARAM_MUST_BE_ARRAY_STR', 'Parameter %s must be of type array.');
|
||||
// Method not implemented.
|
||||
define('FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED', 10);
|
||||
define('FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED_STR', 'Method \'%s()\' not implemented.');
|
||||
// Directory couldn't be created.
|
||||
define('FILE_PASSWD_E_DIR_NOT_CREATED', 11);
|
||||
define('FILE_PASSWD_E_DIR_NOT_CREATED_STR', 'Couldn\'t create directory \'%s\'.');
|
||||
// File couldn't be opened.
|
||||
define('FILE_PASSWD_E_FILE_NOT_OPENED', 12);
|
||||
define('FILE_PASSWD_E_FILE_NOT_OPENED_STR', 'Couldn\'t open file \'%s\'.');
|
||||
// File coudn't be locked.
|
||||
define('FILE_PASSWD_E_FILE_NOT_LOCKED', 13);
|
||||
define('FILE_PASSWD_E_FILE_NOT_LOCKED_STR', 'Couldn\'t lock file \'%s\'.');
|
||||
// File couldn't be unlocked.
|
||||
define('FILE_PASSWD_E_FILE_NOT_UNLOCKED', 14);
|
||||
define('FILE_PASSWD_E_FILE_NOT_UNLOCKED_STR', 'Couldn\'t unlock file.');
|
||||
// File couldn't be closed.
|
||||
define('FILE_PASSWD_E_FILE_NOT_CLOSED', 15);
|
||||
define('FILE_PASSWD_E_FILE_NOT_CLOSED_STR', 'Couldn\'t close file.');
|
||||
|
||||
/**
|
||||
* Allowed 64 chars for salts
|
||||
*/
|
||||
$GLOBALS['_FILE_PASSWD_64'] =
|
||||
'./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* The package File_Passwd provides classes and methods
|
||||
* to handle many different kinds of passwd files.
|
||||
*
|
||||
* The File_Passwd class in certain is a factory container for its special
|
||||
* purpose extension classes, each handling a specific passwd file format.
|
||||
* It also provides a static method for reasonable fast user authentication.
|
||||
* Beside that it offers some encryption methods used by the extensions.
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @version $Revision$
|
||||
*
|
||||
* Usage Example:
|
||||
* <code>
|
||||
* $passwd = &File_Passwd::factory('Unix');
|
||||
* </code>
|
||||
*/
|
||||
class File_Passwd
|
||||
{
|
||||
/**
|
||||
* Get API version
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string API version
|
||||
*/
|
||||
function apiVersion()
|
||||
{
|
||||
return '1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate salt
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
* @param int $length salt length
|
||||
* @return string the salt
|
||||
*/
|
||||
function salt($length = 2)
|
||||
{
|
||||
$salt = '';
|
||||
$length = (int) $length;
|
||||
$length < 2 && $length = 2;
|
||||
for($i = 0; $i < $length; $i++) {
|
||||
$salt .= $GLOBALS['_FILE_PASSWD_64'][rand(0, 63)];
|
||||
}
|
||||
return $salt;
|
||||
}
|
||||
|
||||
/**
|
||||
* No encryption (plaintext)
|
||||
*
|
||||
* @access public
|
||||
* @return string plaintext input
|
||||
* @param string plaintext passwd
|
||||
*/
|
||||
function crypt_plain($plain)
|
||||
{
|
||||
return $plain;
|
||||
}
|
||||
|
||||
/**
|
||||
* DES encryption
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string crypted text
|
||||
* @param string $plain plaintext to encrypt
|
||||
* @param string $salt the salt to use for encryption (2 chars)
|
||||
*/
|
||||
function crypt_des($plain, $salt = null)
|
||||
{
|
||||
(is_null($salt) || strlen($salt) < 2) && $salt = File_Passwd::salt(2);
|
||||
return crypt($plain, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* MD5 encryption
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string crypted text
|
||||
* @param string $plain plaintext to encrypt
|
||||
* @param string $salt the salt to use for encryption
|
||||
* (>2 chars starting with $1$)
|
||||
*/
|
||||
function crypt_md5($plain, $salt = null)
|
||||
{
|
||||
if (
|
||||
is_null($salt) ||
|
||||
strlen($salt) < 3 ||
|
||||
!preg_match('/^\$1\$/', $salt))
|
||||
{
|
||||
$salt = '$1$' . File_Passwd::salt(8);
|
||||
}
|
||||
return crypt($plain, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA1 encryption
|
||||
*
|
||||
* Returns a PEAR_Error if sha1() is not available (PHP<4.3).
|
||||
*
|
||||
* @static
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed crypted string or PEAR_Error
|
||||
* @param string $plain plaintext to encrypt
|
||||
*/
|
||||
function crypt_sha($plain)
|
||||
{
|
||||
if (!function_exists('sha1')) {
|
||||
throw new File_Passwd_Exception(
|
||||
'SHA1 encryption is not available (PHP < 4.3).',
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
$hash = PEAR_ZE2 ? sha1($plain, true) : pack('H40', sha1($plain));
|
||||
return '{SHA}' . base64_encode($hash);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* APR compatible MD5 encryption
|
||||
*
|
||||
* @access public
|
||||
* @return mixed
|
||||
* @param string $plain plaintext to crypt
|
||||
* @param string $salt the salt to use for encryption
|
||||
*/
|
||||
function crypt_apr_md5($plain, $salt = null)
|
||||
{
|
||||
if (is_null($salt)) {
|
||||
$salt = File_Passwd::salt(8);
|
||||
} elseif (preg_match('/^\$apr1\$/', $salt)) {
|
||||
$salt = preg_replace('/^\$apr1\$([^$]+)\$.*/', '\\1', $salt);
|
||||
} else {
|
||||
$salt = substr($salt, 0,8);
|
||||
}
|
||||
|
||||
$length = strlen($plain);
|
||||
$context = $plain . '$apr1$' . $salt;
|
||||
|
||||
if (PEAR_ZE2) {
|
||||
$binary = md5($plain . $salt . $plain, true);
|
||||
} else {
|
||||
$binary = pack('H32', md5($plain . $salt . $plain));
|
||||
}
|
||||
|
||||
for ($i = $length; $i > 0; $i -= 16) {
|
||||
$context .= substr($binary, 0, min(16 , $i));
|
||||
}
|
||||
for ( $i = $length; $i > 0; $i >>= 1) {
|
||||
$context .= ($i & 1) ? chr(0) : $plain[0];
|
||||
}
|
||||
|
||||
$binary = PEAR_ZE2 ? md5($context, true) : pack('H32', md5($context));
|
||||
|
||||
for ($i = 0; $i < 1000; $i++) {
|
||||
$new = ($i & 1) ? $plain : $binary;
|
||||
if ($i % 3) {
|
||||
$new .= $salt;
|
||||
}
|
||||
if ($i % 7) {
|
||||
$new .= $plain;
|
||||
}
|
||||
$new .= ($i & 1) ? $binary : $plain;
|
||||
$binary = PEAR_ZE2 ? md5($new, true) : pack('H32', md5($new));
|
||||
}
|
||||
|
||||
$p = array();
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
$k = $i + 6;
|
||||
$j = $i + 12;
|
||||
if ($j == 16) {
|
||||
$j = 5;
|
||||
}
|
||||
$p[] = File_Passwd::_64(
|
||||
(ord($binary[$i]) << 16) |
|
||||
(ord($binary[$k]) << 8) |
|
||||
(ord($binary[$j])),
|
||||
5
|
||||
);
|
||||
}
|
||||
|
||||
return
|
||||
'$apr1$' . $salt . '$' . implode($p) .
|
||||
File_Passwd::_64(ord($binary[11]), 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hexadecimal string to binary data
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
* @return mixed
|
||||
* @param string $hex
|
||||
*/
|
||||
function _hexbin($hex)
|
||||
{
|
||||
$rs = '';
|
||||
$ln = strlen($hex);
|
||||
for($i = 0; $i < $ln; $i += 2) {
|
||||
$rs .= chr(hexdec($hex{$i} . $hex{$i+1}));
|
||||
}
|
||||
return $rs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to allowed 64 characters for encryption
|
||||
*
|
||||
* @static
|
||||
* @access private
|
||||
* @return string
|
||||
* @param string $value
|
||||
* @param int $count
|
||||
*/
|
||||
function _64($value, $count)
|
||||
{
|
||||
$result = '';
|
||||
while($count > 0 && --$count) {
|
||||
$result .= $GLOBALS['_FILE_PASSWD_64'][$value & 0x3f];
|
||||
$value >>= 6;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for new extensions
|
||||
*
|
||||
* o Unix for standard Unix passwd files
|
||||
* o CVS for CVS pserver passwd files
|
||||
* o SMB for SMB server passwd files
|
||||
* o Authbasic for AuthUserFiles
|
||||
* o Authdigest for AuthDigestFiles
|
||||
* o Custom for custom formatted passwd files
|
||||
*
|
||||
* Returns a PEAR_Error if the desired class/file couldn't be loaded.
|
||||
*
|
||||
* @static use &File_Passwd::factory() for instantiating your passwd object
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return object File_Passwd_$class - desired Passwd object
|
||||
* @param string $class the desired subclass of File_Passwd
|
||||
*/
|
||||
function factory($class)
|
||||
{
|
||||
$class = ucFirst(strToLower($class));
|
||||
if (!@include_once "File/Passwd/$class.php") {
|
||||
throw new File_Passwd_Exception("Couldn't load file Passwd/$class.php", 0);
|
||||
}
|
||||
$class = 'File_Passwd_'.$class;
|
||||
if (!class_exists($class)) {
|
||||
throw new File_Passwd_Exception("Couldn't load class $class.", 0);
|
||||
}
|
||||
return new $class();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Though this approach should be reasonable fast, it is NOT
|
||||
* with APR compatible MD5 encryption used for htpasswd style
|
||||
* password files encrypted in MD5. Generating one MD5 password
|
||||
* takes about 0.3 seconds!
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
* o invalid <var>$type</var> was provided
|
||||
* o invalid <var>$opt</var> was provided
|
||||
*
|
||||
* Depending on <var>$type</var>, <var>$opt</var> should be:
|
||||
* o Smb: encryption method (NT or LM)
|
||||
* o Unix: encryption method (des or md5)
|
||||
* o Authbasic: encryption method (des, sha or md5)
|
||||
* o Authdigest: the realm the user is in
|
||||
* o Cvs: n/a (empty)
|
||||
* o Custom: array of 2 elements: encryption function and delimiter
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return return mixed true if authenticated,
|
||||
* false if not or PEAR_error
|
||||
*
|
||||
* @param string $type Unix, Cvs, Smb, Authbasic or Authdigest
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user the user to authenticate
|
||||
* @param string $pass the plaintext password
|
||||
* @param mixed $opt o Smb: NT or LM
|
||||
* o Unix: des or md5
|
||||
* o Authbasic des, sha or md5
|
||||
* o Authdigest realm the user is in
|
||||
* o Custom encryption function and
|
||||
* delimiter character
|
||||
*/
|
||||
function staticAuth($type, $file, $user, $pass, $opt = '')
|
||||
{
|
||||
$type = ucFirst(strToLower($type));
|
||||
if (!@include_once "File/Passwd/$type.php") {
|
||||
throw new File_Passwd_Exception("Couldn't load file Passwd/$type.php", 0);
|
||||
}
|
||||
$func = array('File_Passwd_' . $type, 'staticAuth');
|
||||
return call_user_func($func, $file, $user, $pass, $opt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Authbasic
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Manipulate AuthUserFiles as used for HTTP Basic Authentication.
|
||||
*
|
||||
* <kbd><u>
|
||||
* Usage Example:
|
||||
* </u></kbd>
|
||||
* <code>
|
||||
* $htp = &File_Passwd::factory('AuthBasic');
|
||||
* $htp->setMode('sha');
|
||||
* $htp->setFile('/www/mike/auth/.htpasswd');
|
||||
* $htp->load();
|
||||
* $htp->addUser('mike', 'secret');
|
||||
* $htp->save();
|
||||
* </code>
|
||||
*
|
||||
* <kbd><u>
|
||||
* Output of listUser()
|
||||
* </u></kbd>
|
||||
* <pre>
|
||||
* array
|
||||
* + user => crypted_passwd
|
||||
* + user => crypted_passwd
|
||||
* </pre>
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Authbasic extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* Path to AuthUserFile
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_file = '.htpasswd';
|
||||
|
||||
/**
|
||||
* Actual encryption mode
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_mode = 'sha';
|
||||
|
||||
/**
|
||||
* Supported encryption modes
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $_modes = array('md5' => 'm', 'des' => 'd', 'sha' => 's');
|
||||
|
||||
/**
|
||||
* Constructor (ZE2)
|
||||
*
|
||||
* Rewritten because DES encryption is not
|
||||
* supportet by the Win32 httpd.
|
||||
*
|
||||
* @access protected
|
||||
* @param string $file path to AuthUserFile
|
||||
*/
|
||||
function __construct($file = '.htpasswd')
|
||||
{
|
||||
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
|
||||
unset($this->_modes['des']);
|
||||
}
|
||||
$this->setFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Raises exception if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return bool
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
* @param string $mode des, sha or md5
|
||||
*/
|
||||
function staticAuth($file, $user, $pass, $mode)
|
||||
{
|
||||
$line = File_Passwd_Common::_auth($file, $user);
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
list(,$real) = explode(':', $line);
|
||||
$crypted = File_Passwd_Authbasic::_genPass($pass, $real, $mode);
|
||||
return ($real === $crypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and rewrite AuthUserFile
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
foreach ($this->_users as $user => $pass) {
|
||||
$content .= $user . ':' . $pass . "\n";
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an user
|
||||
*
|
||||
* The username must start with an alphabetical character and must NOT
|
||||
* contain any other characters than alphanumerics, the underline and dash.
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user already exists
|
||||
* o user contains illegal characters
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user
|
||||
* @param string $pass
|
||||
*/
|
||||
function addUser($user, $pass)
|
||||
{
|
||||
if ($this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_ALREADY_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_ALREADY
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user] = $this->_genPass($pass);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if user doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or a PEAR_Error
|
||||
* @param string $user the user whose password should be changed
|
||||
* @param string $pass the new plaintext password
|
||||
*/
|
||||
function changePasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$this->_users[$user] = $this->_genPass($pass);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify password
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o an invalid encryption mode was supplied
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true if passwords equal, false if they don't, or PEAR_Error
|
||||
* @param string $user the user whose password should be verified
|
||||
* @param string $pass the plaintext password to verify
|
||||
*/
|
||||
function verifyPasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$real = $this->_users[$user];
|
||||
return ($real === $this->_genPass($pass, $real));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual encryption mode
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function getMode()
|
||||
{
|
||||
return $this->_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported encryption modes
|
||||
*
|
||||
* <pre>
|
||||
* array
|
||||
* + md5
|
||||
* + sha
|
||||
* + des
|
||||
* </pre>
|
||||
*
|
||||
* ATTN: DES encryption not available on Win32!
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function listModes()
|
||||
{
|
||||
return array_keys($this->_modes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the encryption mode
|
||||
*
|
||||
* You can choose one of md5, sha or des.
|
||||
*
|
||||
* ATTN: DES encryption not available on Win32!
|
||||
*
|
||||
* Returns a PEAR_Error if a specific encryption mode is not supported.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on succes or PEAR_Error
|
||||
* @param string $mode
|
||||
*/
|
||||
function setMode($mode)
|
||||
{
|
||||
$mode = strToLower($mode);
|
||||
if (!isset($this->_modes[$mode])) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $mode),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
$this->_mode = $mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate password with htpasswd executable
|
||||
*
|
||||
* @access private
|
||||
* @return string the crypted password
|
||||
* @param string $pass the plaintext password
|
||||
* @param string $salt the salt to use
|
||||
* @param string $mode encyption mode, usually determined from
|
||||
* <var>$this->_mode</var>
|
||||
*/
|
||||
function _genPass($pass, $salt = null, $mode = null)
|
||||
{
|
||||
$mode = is_null($mode) ? strToLower($this->_mode) : strToLower($mode);
|
||||
|
||||
if ($mode == 'md5') {
|
||||
return File_Passwd::crypt_apr_md5($pass, $salt);
|
||||
} elseif ($mode == 'des') {
|
||||
return File_Passwd::crypt_des($pass, $salt);
|
||||
} elseif ($mode == 'sha') {
|
||||
return File_Passwd::crypt_sha($pass, $salt);
|
||||
}
|
||||
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $mode),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the AuthUserFile
|
||||
*
|
||||
* Returns a PEAR_Error if AuthUserFile has invalid format.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
$this->_users = array();
|
||||
foreach ($this->_contents as $line) {
|
||||
$user = explode(':', $line);
|
||||
if (count($user) != 2) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
$this->_users[$user[0]] = trim($user[1]);
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Password
|
||||
*
|
||||
* Returns PEAR_Error FILE_PASSD_E_INVALID_ENC_MODE if the supplied
|
||||
* encryption mode is not supported.
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return mixed The crypted password on success or PEAR_Error on failure.
|
||||
* @param string $pass The plaintext password.
|
||||
* @param string $mode The encryption mode to use (des|md5|sha).
|
||||
* @param string $salt The salt to use.
|
||||
*/
|
||||
function generatePasswd($pass, $mode = FILE_PASSWD_DES, $salt = null)
|
||||
{
|
||||
if (!in_array(strToLower($mode), array('des', 'md5', 'sha'))) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $mode),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
return File_Passwd_Authbasic::_genPass($pass, $salt, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @deprecated
|
||||
*/
|
||||
function generatePassword($pass, $mode = FILE_PASSWD_DES, $salt = null)
|
||||
{
|
||||
return File_Passwd_Authbasic::generatePasswd($pass, $mode, $salt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Authdigest
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Manipulate AuthDigestFiles as used for HTTP Digest Authentication.
|
||||
*
|
||||
* <kbd><u>
|
||||
* Usage Example:
|
||||
* </u></kbd>
|
||||
* <code>
|
||||
* $htd = &File_Passwd::factory('Authdigest');
|
||||
* $htd->setFile('/www/mike/auth/.htdigest');
|
||||
* $htd->load();
|
||||
* $htd->addUser('mike', 'myRealm', 'secret');
|
||||
* $htd->save();
|
||||
* </code>
|
||||
*
|
||||
* <kbd><u>
|
||||
* Output of listUser()
|
||||
* </u></kbd>
|
||||
* <pre>
|
||||
* array
|
||||
* + user => array
|
||||
* + realm => crypted_passwd
|
||||
* + realm => crypted_passwd
|
||||
* + user => array
|
||||
* + realm => crypted_passwd
|
||||
* </pre>
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Authdigest extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* Path to AuthDigestFile
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_file = '.htdigest';
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true if authenticated, false if not or PEAR_Error
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
* @param string $realm the realm the user is in
|
||||
*/
|
||||
function staticAuth($file, $user, $pass, $realm)
|
||||
{
|
||||
$line = File_Passwd_Common::_auth($file, $user.':'.$realm);
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
@list(,,$real)= explode(':', $line);
|
||||
return (md5("$user:$realm:$pass") === $real);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and rewrite AuthDigestFile
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or a PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
if (count($this->_users)) {
|
||||
foreach ($this->_users as $user => $realm) {
|
||||
foreach ($realm as $r => $pass){
|
||||
$content .= "$user:$r:$pass\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o the user already exists in the supplied realm
|
||||
* o the user or realm contain illegal characters
|
||||
*
|
||||
* $user and $realm must start with an alphabetical charachter and must NOT
|
||||
* contain any other characters than alphanumerics, the underline and dash.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or a PEAR_Error
|
||||
* @param string $user the user to add
|
||||
* @param string $realm the realm the user should be in
|
||||
* @param string $pass the plaintext password
|
||||
*/
|
||||
function addUser($user, $realm, $pass)
|
||||
{
|
||||
if ($this->userInRealm($user, $realm)) {
|
||||
throw new File_Passwd_Exception(
|
||||
"User '$user' already exists in realm '$realm'.", 0
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $realm)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'Realm ', $realm),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user][$realm] = md5("$user:$realm:$pass");
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all user of (a | all) realm(s)
|
||||
*
|
||||
* Returns:
|
||||
* o associative array of users of ONE realm if $inRealm was supplied
|
||||
* <pre>
|
||||
* realm1
|
||||
* + user1 => pass
|
||||
* + user2 => pass
|
||||
* + user3 => pass
|
||||
* </pre>
|
||||
* o associative array of all realms with all users
|
||||
* <pre>
|
||||
* array
|
||||
* + realm1 => array
|
||||
* + user1 => pass
|
||||
* + user2 => pass
|
||||
* + user3 => pass
|
||||
* + realm2 => array
|
||||
* + user3 => pass
|
||||
* + realm3 => array
|
||||
* + user1 => pass
|
||||
* + user2 => pass
|
||||
* </pre>
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
* @param string $inRealm the realm to list users of;
|
||||
* if omitted, you'll get all realms
|
||||
*/
|
||||
function listUserInRealm($inRealm = '')
|
||||
{
|
||||
$result = array();
|
||||
foreach ($this->_users as $user => $realms){
|
||||
foreach ($realms as $realm => $pass){
|
||||
if (!empty($inRealm) && ($inRealm !== $realm)) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($result[$realm])) {
|
||||
$result[$realm] = array();
|
||||
}
|
||||
$result[$realm][$user] = $pass;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist in the supplied realm
|
||||
* o user or realm contains illegal characters
|
||||
*
|
||||
* This method in fact adds the user whith the new password
|
||||
* after deleting the user.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or a PEAR_Error
|
||||
* @param string $user the user whose password should be changed
|
||||
* @param string $realm the realm the user is in
|
||||
* @param string $pass the new plaintext password
|
||||
*/
|
||||
function changePasswd($user, $realm, $pass)
|
||||
{
|
||||
$this->delUserInRealm($user, $realm);
|
||||
|
||||
return $this->addUser($user, $realm, $pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifiy password
|
||||
*
|
||||
* Returns a PEAR_Error if the user doesn't exist in the supplied realm.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true if passwords equal, false if they don't, or PEAR_Error
|
||||
* @param string $user the user whose password should be verified
|
||||
* @param string $realm the realm the user is in
|
||||
* @param string $pass the plaintext password to verify
|
||||
*/
|
||||
function verifyPasswd($user, $realm, $pass)
|
||||
{
|
||||
if (!$this->userInRealm($user, $realm)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_USER_NOT_IN_REALM_STR, $user, $realm),
|
||||
FILE_PASSWD_E_USER_NOT_IN_REALM
|
||||
);
|
||||
}
|
||||
return ($this->_users[$user][$realm] === md5("$user:$realm:$pass"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ckeck if a certain user is in a specific realm
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return boolean
|
||||
* @param string $user the user to check
|
||||
* @param string $realm the realm the user shuold be in
|
||||
*/
|
||||
function userInRealm($user, $realm)
|
||||
{
|
||||
return (isset($this->_users[$user][$realm]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a certain user in a specific realm
|
||||
*
|
||||
* Returns a PEAR_Error if <var>$user</var> doesn't exist <var>$inRealm</var>.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user the user to remove
|
||||
* @param string $inRealm the realm the user should be in
|
||||
*/
|
||||
function delUserInRealm($user, $inRealm)
|
||||
{
|
||||
if (!$this->userInRealm($user, $inRealm)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_USER_NOT_IN_REALM_STR, $user, $inRealm),
|
||||
FILE_PASSWD_E_USER_NOT_IN_REALM
|
||||
);
|
||||
}
|
||||
unset($this->_users[$user][$inRealm]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the AuthDigestFile
|
||||
*
|
||||
* Returns a PEAR_Error if AuthDigestFile has invalid format.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
$this->_users = array();
|
||||
foreach ($this->_contents as $line) {
|
||||
$user = explode(':', $line);
|
||||
if (count($user) != 3) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
list($user, $realm, $pass) = $user;
|
||||
$this->_users[$user][$realm] = trim($pass);
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Password
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string The crypted password.
|
||||
* @param string $user The username.
|
||||
* @param string $realm The realm the user is in.
|
||||
* @param string $pass The plaintext password.
|
||||
*/
|
||||
function generatePasswd($user, $realm, $pass)
|
||||
{
|
||||
return md5("$user:$realm:$pass");
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @deprecated
|
||||
*/
|
||||
function generatePassword($user, $realm, $pass)
|
||||
{
|
||||
return File_Passwd_Authdigest::generatePasswd($user, $realm, $pass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Common
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires System
|
||||
*/
|
||||
require_once 'System.php';
|
||||
/**
|
||||
* Requires File::Passwd
|
||||
*/
|
||||
require_once 'File/Passwd.php';
|
||||
|
||||
/**
|
||||
* Baseclass for File_Passwd_* classes.
|
||||
*
|
||||
* <kbd><u>
|
||||
* Provides basic operations:
|
||||
* </u></kbd>
|
||||
* o opening & closing
|
||||
* o locking & unlocking
|
||||
* o loading & saving
|
||||
* o check if user exist
|
||||
* o delete a certain user
|
||||
* o list users
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access protected
|
||||
* @internal extend this class for your File_Passwd_* class
|
||||
*/
|
||||
class File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* passwd file
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
var $_file = 'passwd';
|
||||
|
||||
/**
|
||||
* file content
|
||||
*
|
||||
* @var aray
|
||||
* @access protected
|
||||
*/
|
||||
var $_contents = array();
|
||||
|
||||
/**
|
||||
* users
|
||||
*
|
||||
* @var array
|
||||
* @access protected
|
||||
*/
|
||||
var $_users = array();
|
||||
|
||||
/**
|
||||
* PCRE for valid chars
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
var $_pcre = '/^[a-z]+[a-z0-9_-]*$/i';
|
||||
|
||||
/**
|
||||
* Constructor (ZE2)
|
||||
*
|
||||
* @access protected
|
||||
* @param string $file path to passwd file
|
||||
*/
|
||||
function __construct($file = 'passwd')
|
||||
{
|
||||
$this->setFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the content of the file
|
||||
*
|
||||
* You must overwrite this method in your File_Passwd_* class.
|
||||
*
|
||||
* @abstract
|
||||
* @internal
|
||||
* @access public
|
||||
* @return object PEAR_Error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED_STR, 'parse'),
|
||||
FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and rewrite passwd file
|
||||
*
|
||||
* You must overwrite this method in your File_Passwd_* class.
|
||||
*
|
||||
* @abstract
|
||||
* @internal
|
||||
* @access public
|
||||
* @return object PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED_STR, 'save'),
|
||||
FILE_PASSWD_E_METHOD_NOT_IMPLEMENTED
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file, locks it exclusively and returns the filehandle
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in the desired mode
|
||||
* o file couldn't be locked exclusively
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access protected
|
||||
* @return mixed resource of type file handle or PEAR_Error
|
||||
* @param string $mode the mode to open the file with
|
||||
*/
|
||||
function &_open($mode, $file = null)
|
||||
{
|
||||
isset($file) or $file = $this->_file;
|
||||
$dir = dirname($file);
|
||||
$lock = strstr($mode, 'r') ? LOCK_SH : LOCK_EX;
|
||||
if (!is_dir($dir) && !System::mkDir('-p -m 0755 ' . $dir)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_DIR_NOT_CREATED_STR, $dir),
|
||||
FILE_PASSWD_E_DIR_NOT_CREATED
|
||||
);
|
||||
}
|
||||
if (!is_resource($fh = @fopen($file, $mode))) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_FILE_NOT_OPENED_STR, $file),
|
||||
FILE_PASSWD_E_FILE_NOT_OPENED
|
||||
);
|
||||
}
|
||||
if (!@flock($fh, $lock)) {
|
||||
fclose($fh);
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_FILE_NOT_LOCKED_STR, $file),
|
||||
FILE_PASSWD_E_FILE_NOT_LOCKED
|
||||
);
|
||||
}
|
||||
return $fh;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a prior opened and locked file handle
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access protected
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param resource $file_handle the file handle to operate on
|
||||
*/
|
||||
function _close($file_handle)
|
||||
{
|
||||
if (!@flock($file_handle, LOCK_UN)) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_FILE_NOT_UNLOCKED_STR,
|
||||
FILE_PASSWD_E_FILE_NOT_UNLOCKED
|
||||
);
|
||||
}
|
||||
if (!@fclose($file_handle)) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_FILE_NOT_CLOSED_STR,
|
||||
FILE_PASSWD_E_FILE_NOT_CLOSED
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function load()
|
||||
{
|
||||
$fh = $this->_open('r');
|
||||
|
||||
$this->_contents = array();
|
||||
while ($line = fgets($fh)) {
|
||||
if (!preg_match('/^\s*#/', $line) && $line = trim($line)) {
|
||||
$this->_contents[] = $line;
|
||||
}
|
||||
}
|
||||
$this->_close($fh);
|
||||
|
||||
return $this->parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the modified content to the passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access protected
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function _save($content)
|
||||
{
|
||||
$fh = $this->_open('w');
|
||||
|
||||
fputs($fh, $content);
|
||||
return $this->_close($fh);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set path to passwd file
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
*/
|
||||
function setFile($file)
|
||||
{
|
||||
$this->_file = $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path of passwd file
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function getFile()
|
||||
{
|
||||
return $this->_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a certain user already exists
|
||||
*
|
||||
* @access public
|
||||
* @return bool
|
||||
* @param string $user the name of the user to check if already exists
|
||||
*/
|
||||
function userExists($user)
|
||||
{
|
||||
return isset($this->_users[$user]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if user doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string
|
||||
*/
|
||||
function delUser($user)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
unset($this->_users[$user]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* List user
|
||||
*
|
||||
* Returns a PEAR_Error if <var>$user</var> doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed array of a/all user(s) or PEAR_Error
|
||||
* @param string $user the user to list or all users if empty
|
||||
*/
|
||||
function listUser($user = '')
|
||||
{
|
||||
if (empty($user)) {
|
||||
return $this->_users;
|
||||
}
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
return $this->_users[$user];
|
||||
}
|
||||
|
||||
/**
|
||||
* Base method for File_Passwd::staticAuth()
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access protected
|
||||
* @return mixed line of passwd file containing <var>$id</var>,
|
||||
* false if <var>$id</var> wasn't found or PEAR_Error
|
||||
* @param string $file path to passwd file
|
||||
* @param string $id user_id to search for
|
||||
* @param string $sep field separator
|
||||
*/
|
||||
function _auth($file, $id, $sep = ':')
|
||||
{
|
||||
$file = realpath($file);
|
||||
if (!is_file($file)) {
|
||||
throw new File_Passwd_Exception("File '$file' couldn't be found.", 0);
|
||||
}
|
||||
$fh = File_Passwd_Common::_open('r', $file);
|
||||
|
||||
$cmp = $id . $sep;
|
||||
$len = strlen($cmp);
|
||||
while ($line = fgets($fh)) {
|
||||
if (!strncmp($line, $cmp, $len)) {
|
||||
File_Passwd_Common::_close($fh);
|
||||
return trim($line);
|
||||
}
|
||||
}
|
||||
File_Passwd_Common::_close($fh);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Custom
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Manipulate custom formatted passwd files
|
||||
*
|
||||
* Usage Example:
|
||||
* <code>
|
||||
* $cust = &File_Passwd::factory('Custom');
|
||||
* $cust->setDelim('|');
|
||||
* $cust->load();
|
||||
* $cust->setEncFunc(array('File_Passwd', 'crypt_apr_md5'));
|
||||
* $cust->addUser('mike', 'pass');
|
||||
* $cust->save();
|
||||
* </code>
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Custom extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* Delimiter
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
var $_delim = ':';
|
||||
|
||||
/**
|
||||
* Encryption function
|
||||
*
|
||||
* @access private
|
||||
* @var string
|
||||
*/
|
||||
var $_enc = array('File_Passwd', 'crypt_md5');
|
||||
|
||||
/**
|
||||
* 'name map'
|
||||
*
|
||||
* @access private
|
||||
* @var array
|
||||
*/
|
||||
var $_map = array();
|
||||
|
||||
/**
|
||||
* Whether to use the 'name map' or not
|
||||
*
|
||||
* @var boolean
|
||||
* @access private
|
||||
*/
|
||||
var $_usemap = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access protected
|
||||
* @return object
|
||||
*/
|
||||
function File_Passwd_Custom($file = 'passwd')
|
||||
{
|
||||
$this->__construct($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
* o invalid encryption function <var>$opts[0]</var>,
|
||||
* or no delimiter character <var>$opts[1]</var> was provided
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_UNDEFINED |
|
||||
* FILE_PASSWD_E_FILE_NOT_OPENED |
|
||||
* FILE_PASSWD_E_FILE_NOT_LOCKED |
|
||||
* FILE_PASSWD_E_FILE_NOT_UNLOCKED |
|
||||
* FILE_PASSWD_E_FILE_NOT_CLOSED |
|
||||
* FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
* @access public
|
||||
* @return mixed Returns &true; if authenticated, &false; if not or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
* @param array $otps encryption function and delimiter charachter
|
||||
* (in this order)
|
||||
*/
|
||||
function staticAuth($file, $user, $pass, $opts)
|
||||
{
|
||||
setType($opts, 'array');
|
||||
if (count($opts) != 2 || empty($opts[1])) {
|
||||
throw new File_Passwd_Exception('Insufficient options.', 0);
|
||||
}
|
||||
|
||||
$line = File_Passwd_Common::_auth($file, $user, $opts[1]);
|
||||
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
|
||||
list(,$real)= explode($opts[1], $line);
|
||||
$crypted = File_Passwd_Custom::_genPass($pass, $real, $opts[0]);
|
||||
|
||||
return ($crypted === $real);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set delimiter
|
||||
*
|
||||
* You can set a custom char to delimit the columns of a data set.
|
||||
* Defaults to a colon (':'). Be aware that this char mustn't be
|
||||
* in the values of your data sets.
|
||||
*
|
||||
* @access public
|
||||
* @return void
|
||||
* @param string $delim custom delimiting character
|
||||
*/
|
||||
function setDelim($delim = ':')
|
||||
{
|
||||
@setType($delim, 'string');
|
||||
if (empty($delim)) {
|
||||
$this->_delim = ':';
|
||||
} else {
|
||||
$this->_delim = $delim{0};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom delimiter
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function getDelim()
|
||||
{
|
||||
return $this->_delim;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set encryption function
|
||||
*
|
||||
* You can set a custom encryption function to use.
|
||||
* The supplied function will be called by php's call_user_function(),
|
||||
* so you can supply an array with a method of a class/object, too
|
||||
* (i.e. array('File_Passwd', 'crypt_apr_md5').
|
||||
*
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
* @access public
|
||||
* @return mixed Returns &true; on success or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
* @param mixed $function callable encryption function
|
||||
*/
|
||||
function setEncFunc($function = array('File_Passwd', 'crypt_md5'))
|
||||
{
|
||||
if (!is_callable($function)) {
|
||||
if (is_array($function)) {
|
||||
$function = implode('::', $function);
|
||||
}
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $function),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
|
||||
$this->_enc = $function;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current custom encryption method
|
||||
*
|
||||
* Possible return values (examples):
|
||||
* o 'md5'
|
||||
* o 'File_Passwd::crypt_md5'
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function getEncFunc()
|
||||
{
|
||||
if (is_array($this->_enc)) {
|
||||
return implode('::', $this->_enc);
|
||||
}
|
||||
return $this->_enc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use the 'name map' of the extra properties or not
|
||||
*
|
||||
* @see File_Passwd_Custom::useMap()
|
||||
* @see setMap()
|
||||
* @see getMap()
|
||||
*
|
||||
* @access public
|
||||
* @return boolean always true if you set a value (true/false) OR
|
||||
* the actual value if called without param
|
||||
*
|
||||
* @param boolean $bool whether to use the 'name map' or not
|
||||
*/
|
||||
function useMap($bool = null)
|
||||
{
|
||||
if (is_null($bool)) {
|
||||
return $this->_usemap;
|
||||
}
|
||||
$this->_usemap = (bool) $bool;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the 'name map' to use with the extra properties of the user
|
||||
*
|
||||
* This map is used for naming the associative array of the extra properties.
|
||||
*
|
||||
* Returns a PEAR_Error if <var>$map</var> was not of type array.
|
||||
*
|
||||
* @see getMap()
|
||||
* @see useMap()
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_PARAM_MUST_BE_ARRAY
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function setMap($map = array())
|
||||
{
|
||||
if (!is_array($map)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_PARAM_MUST_BE_ARRAY_STR, '$map'),
|
||||
FILE_PASSWD_E_PARAM_MUST_BE_ARRAY
|
||||
);
|
||||
}
|
||||
$this->_map = $map;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'name map' which is used for the extra properties of the user
|
||||
*
|
||||
* @see setMap()
|
||||
* @see useMap()
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function getMap()
|
||||
{
|
||||
return $this->_map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes an rewrite passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_FILE_NOT_OPENED |
|
||||
* FILE_PASSWD_E_FILE_NOT_LOCKED |
|
||||
* FILE_PASSWD_E_FILE_NOT_UNLOCKED |
|
||||
* FILE_PASSWD_E_FILE_NOT_CLOSED
|
||||
* @access public
|
||||
* @return mixed Returns &true; on success or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
foreach ($this->_users as $user => $array){
|
||||
$pass = array_shift($array);
|
||||
$extra = implode($this->_delim, $array);
|
||||
$content .= $user . $this->_delim . $pass;
|
||||
if (!empty($extra)) {
|
||||
$content .= $this->_delim . $extra;
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Custom password file
|
||||
*
|
||||
* Returns a PEAR_Error if passwd file has invalid format.
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_INVALID_FORMAT
|
||||
* @access public
|
||||
* @return mixed Returns &true; on success or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
$this->_users = array();
|
||||
foreach ($this->_contents as $line){
|
||||
$parts = explode($this->_delim, $line);
|
||||
if (count($parts) < 2) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
$user = array_shift($parts);
|
||||
$pass = array_shift($parts);
|
||||
$values = array();
|
||||
if ($this->_usemap) {
|
||||
$values['pass'] = $pass;
|
||||
foreach ($parts as $i => $value){
|
||||
if (isset($this->_map[$i])) {
|
||||
$values[$this->_map[$i]] = $value;
|
||||
} else {
|
||||
$values[$i+1] = $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$values = array_merge(array($pass), $parts);
|
||||
}
|
||||
$this->_users[$user] = $values;
|
||||
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an user
|
||||
*
|
||||
* The username must start with an alphabetical character and must NOT
|
||||
* contain any other characters than alphanumerics, the underline and dash.
|
||||
*
|
||||
* If you use the 'name map' you should also use these naming in
|
||||
* the supplied extra array, because your values would get mixed up
|
||||
* if they are in the wrong order, which is always true if you
|
||||
* DON'T use the 'name map'!
|
||||
*
|
||||
* So be warned and USE the 'name map'!
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user already exists
|
||||
* o user contains illegal characters
|
||||
* o encryption mode is not supported
|
||||
* o any element of the <var>$extra</var> array contains the delimiter char
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_EXISTS_ALREADY |
|
||||
* FILE_PASSWD_E_INVALID_ENC_MODE |
|
||||
* FILE_PASSWD_E_INVALID_CHARS
|
||||
* @access public
|
||||
* @return mixed Returns &true; on success or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
* @param string $user the name of the user to add
|
||||
* @param string $pass the password of the user to add
|
||||
* @param array $extra extra properties of user to add
|
||||
*/
|
||||
function addUser($user, $pass, $extra = array())
|
||||
{
|
||||
if ($this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_ALREADY_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_ALREADY
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user) || strstr($user, $this->_delim)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
if (!is_array($extra)) {
|
||||
setType($extra, 'array');
|
||||
}
|
||||
foreach ($extra as $e){
|
||||
if (strstr($e, $this->_delim)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'Property ', $e),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$pass = $this->_genPass($pass);
|
||||
|
||||
/**
|
||||
* If you don't use the 'name map' the user array will be numeric.
|
||||
*/
|
||||
if (!$this->_usemap) {
|
||||
array_unshift($extra, $pass);
|
||||
$this->_users[$user] = $extra;
|
||||
} else {
|
||||
$map = $this->_map;
|
||||
array_unshift($map, 'pass');
|
||||
$extra['pass'] = $pass;
|
||||
foreach ($map as $key){
|
||||
$this->_users[$user][$key] = @$extra[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify properties of a certain user
|
||||
*
|
||||
* # DON'T MODIFY THE PASSWORD WITH THIS METHOD!
|
||||
*
|
||||
* You should use this method only if the 'name map' is used, too.
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o any property contains the custom delimiter character
|
||||
*
|
||||
* @see changePasswd()
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_EXISTS_NOT |
|
||||
* FILE_PASSWD_E_INVALID_CHARS
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user the user to modify
|
||||
* @param array $properties an associative array of
|
||||
* properties to modify
|
||||
*/
|
||||
function modUser($user, $properties = array())
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_array($properties)) {
|
||||
setType($properties, 'array');
|
||||
}
|
||||
|
||||
foreach ($properties as $key => $value){
|
||||
if (strstr($value, $this->_delim)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user][$key] = $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exists
|
||||
* o encryption mode is not supported
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_EXISTS_NOT |
|
||||
* FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
* @access public
|
||||
* @return mixed Returns &true; on success or
|
||||
* <classname>PEAR_Error</classname> on failure.
|
||||
* @param string $user the user whose password should be changed
|
||||
* @param string $pass the new plaintext password
|
||||
*/
|
||||
function changePasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
|
||||
$pass = $this->_genPass($pass);
|
||||
|
||||
if ($this->_usemap) {
|
||||
$this->_users[$user]['pass'] = $pass;
|
||||
} else {
|
||||
$this->_users[$user][0] = $pass;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o encryption mode is not supported
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_EXISTS_NOT |
|
||||
* FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
* @access public
|
||||
* @return mixed Returns &true; if passwors equal, &false; if they don't
|
||||
* or <classname>PEAR_Error</classname> on fialure.
|
||||
* @param string $user the user whose password should be verified
|
||||
* @param string $pass the password to verify
|
||||
*/
|
||||
function verifyPasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$real =
|
||||
$this->_usemap ?
|
||||
$this->_users[$user]['pass'] :
|
||||
$this->_users[$user][0]
|
||||
;
|
||||
return ($real === $this->_genPass($pass, $real));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate crypted password from the plaintext password
|
||||
*
|
||||
* Returns a PEAR_Error if actual encryption mode is not supported.
|
||||
*
|
||||
* @throws PEAR_Error FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
* @access private
|
||||
* @return mixed Returns the crypted password or
|
||||
* <classname>PEAR_Error</classname>
|
||||
* @param string $pass the plaintext password
|
||||
* @param string $salt the crypted password from which to gain the salt
|
||||
* @param string $func the encryption function to use
|
||||
*/
|
||||
function _genPass($pass, $salt = null, $func = null)
|
||||
{
|
||||
if (is_null($func)) {
|
||||
$func = $this->_enc;
|
||||
}
|
||||
|
||||
if (!is_callable($func)) {
|
||||
if (is_array($func)) {
|
||||
$func = implode('::', $func);
|
||||
}
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $func),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
|
||||
if ($func === 'md5') {
|
||||
$salt = null;
|
||||
}
|
||||
|
||||
$return = @call_user_func($func, $pass, $salt);
|
||||
|
||||
if (is_null($return) || $return === false) {
|
||||
$return = @call_user_func($func, $pass);
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Cvs
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Manipulate CVS pserver passwd files.
|
||||
*
|
||||
* <kbd><u>
|
||||
* A line of a CVS pserver passwd file consists of 2 to 3 colums:
|
||||
* </u></kbd>
|
||||
* <pre>
|
||||
* user1:1HCoDDWxK9tbM:sys_user1
|
||||
* user2:0O0DYYdzjCVxs
|
||||
* user3:MIW9UUoifhqRo:sys_user2
|
||||
* </pre>
|
||||
*
|
||||
* If the third column is specified, the CVS user named in the first column is
|
||||
* mapped to the corresponding system user named in the third column.
|
||||
* That doesn't really affect us - just for your interest :)
|
||||
*
|
||||
* <kbd><u>Output of listUser()</u></kbd>
|
||||
* <pre>
|
||||
* array
|
||||
* + user => array
|
||||
* + passwd => crypted_passwd
|
||||
* + system => system_user
|
||||
* + user => array
|
||||
* + passwd => crypted_passwd
|
||||
* + system => system_user
|
||||
* </pre>
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Cvs extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function File_Passwd_Cvs($file = 'passwd')
|
||||
{
|
||||
parent::__construct($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
* @access public
|
||||
* @return mixed true if authenticated, false if not or PEAR_Error
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
*/
|
||||
function staticAuth($file, $user, $pass)
|
||||
{
|
||||
$line = File_Passwd_Common::_auth($file, $user);
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
@list(,$real) = explode(':', $line);
|
||||
return (File_Passwd_Cvs::generatePassword($pass, $real) === $real);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and rewrite CVS passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
foreach ($this->_users as $user => $v){
|
||||
$content .= $user . ':' . $v['passwd'];
|
||||
if (isset($v['system']) && !empty($v['system'])) {
|
||||
$content .= ':' . $v['system'];
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the CVS passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if passwd file has invalid format.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
$this->_users = array();
|
||||
foreach ($this->_contents as $line) {
|
||||
$user = explode(':', $line);
|
||||
if (count($user) < 2) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
@list($user, $pass, $system) = $user;
|
||||
$this->_users[$user]['passwd'] = $pass;
|
||||
if (!empty($system)) {
|
||||
$this->_users[$user]['system'] = $system;
|
||||
}
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an user
|
||||
*
|
||||
* The username must start with an alphabetical character and must NOT
|
||||
* contain any other characters than alphanumerics, the underline and dash.
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user already exists
|
||||
* o user or system_user contains illegal characters
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user the name of the user to add
|
||||
* @param string $pass the password of the user tot add
|
||||
* @param string $system_user the systemuser this user maps to
|
||||
*/
|
||||
function addUser($user, $pass, $system_user = '')
|
||||
{
|
||||
if ($this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_ALREADY_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_ALREADY
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
@setType($system_user, 'string');
|
||||
if (!empty($system_user) && !preg_match($this->_pcre, $system_user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(
|
||||
FILE_PASSWD_E_INVALID_CHARS_STR,
|
||||
'System user ',
|
||||
$system_user
|
||||
),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user]['passwd'] = $this->generatePassword($pass);
|
||||
$this->_users[$user]['system'] = $system_user;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if the user doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true if passwords equal, false ifthe don't or PEAR_Error
|
||||
* @param string $user user whose password should be verified
|
||||
* @param string $pass the plaintext password that should be verified
|
||||
*/
|
||||
function verifyPasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$real = $this->_users[$user]['passwd'];
|
||||
return ($real === $this->generatePassword($pass, $real));
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if user doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function changePasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$this->_users[$user]['passwd'] = $this->generatePassword($pass);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the corresponding system user of a certain cvs user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o system user contains illegal characters
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function changeSysUser($user, $system)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $system)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(
|
||||
FILE_PASSWD_E_INVALID_CHARS_STR,
|
||||
'System user ',
|
||||
$system_user
|
||||
),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user]['system'] = $system;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate crypted password
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string the crypted password
|
||||
* @param string $pass new plaintext password
|
||||
* @param string $salt new crypted password from which to gain the salt
|
||||
*/
|
||||
function generatePasswd($pass, $salt = null)
|
||||
{
|
||||
return File_Passwd::crypt_des($pass, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @deprecated
|
||||
*/
|
||||
function generatePassword($pass, $salt = null)
|
||||
{
|
||||
return File_Passwd_Cvs::generatePasswd($pass, $salt);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php
|
||||
class File_Passwd_Exception extends Exception {}
|
||||
@@ -0,0 +1,415 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Smb
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @author Michael Bretterklieber <michael@bretterklieber.com>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Requires Crypt::CHAP
|
||||
*/
|
||||
require_once 'Crypt/CHAP.php';
|
||||
|
||||
/**
|
||||
* Manipulate SMB server passwd files.
|
||||
*
|
||||
* # Usage Example 1 (modifying existing file):
|
||||
* <code>
|
||||
* $f = &File_Passwd::factory('SMB');
|
||||
* $f->setFile('./smbpasswd');
|
||||
* $f->load();
|
||||
* $f->addUser('sepp3', 'MyPw', array('userid' => 12));
|
||||
* $f->changePasswd('sepp', 'MyPw');
|
||||
* $f->delUser('karli');
|
||||
* foreach($f->listUser() as $user => $data) {
|
||||
* echo $user . ':' . implode(':', $data) ."\n";
|
||||
* }
|
||||
* $f->save();
|
||||
* </code>
|
||||
*
|
||||
* # Usage Example 2 (creating a new file):
|
||||
* <code>
|
||||
* $f = &File_Passwd::factory('SMB');
|
||||
* $f->setFile('./smbpasswd');
|
||||
* $f->addUser('sepp1', 'MyPw', array('userid'=> 12));
|
||||
* $f->addUser('sepp3', 'MyPw', array('userid' => 1000));
|
||||
* $f->save();
|
||||
* </code>
|
||||
*
|
||||
* # Usage Example 3 (authentication):
|
||||
* <code>
|
||||
* $f = &File_Passwd::factory('SMB');
|
||||
* $f->setFile('./smbpasswd');
|
||||
* $f->load();
|
||||
* if (true === $f->verifyPasswd('sepp', 'MyPw')) {
|
||||
* echo "User valid";
|
||||
* } else {
|
||||
* echo "User invalid or disabled";
|
||||
* }
|
||||
* </code>
|
||||
*
|
||||
* @author Michael Bretterklieber <michael@bretterklieber.com>
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Smb extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* Object which generates the NT-Hash and LAN-Manager-Hash passwds
|
||||
*
|
||||
* @access protected
|
||||
* @var object
|
||||
*/
|
||||
var $msc;
|
||||
|
||||
/**
|
||||
* Constructor (ZE2)
|
||||
*
|
||||
* Rewritten because we want to init our crypt engine.
|
||||
*
|
||||
* @access public
|
||||
* @param string $file SMB passwd file
|
||||
*/
|
||||
function __construct($file = 'smbpasswd')
|
||||
{
|
||||
$this->setFile($file);
|
||||
$this->msc = new Crypt_CHAP_MSv1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
* o invalid encryption method <var>$nt_or_lm</var> was provided
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
* @access public
|
||||
* @return mixed true if authenticated, false if not or PEAR_Error
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
* @param string $nt_or_lm encryption mode to use (NT or LM hash)
|
||||
*/
|
||||
function staticAuth($file, $user, $pass, $nt_or_lm = 'nt')
|
||||
{
|
||||
$line = File_Passwd_Common::_auth($file, $user);
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
@list(,,$lm,$nt) = explode(':', $line);
|
||||
$chap = new Crypt_CHAP_MSv1;
|
||||
|
||||
switch(strToLower($nt_or_lm)){
|
||||
case FILE_PASSWD_NT:
|
||||
$real = $nt;
|
||||
$crypted = $chap->ntPasswordHash($pass);
|
||||
break;
|
||||
case FILE_PASSWD_LM:
|
||||
$real = $lm;
|
||||
$crypted = $chap->lmPasswordHash($pass);
|
||||
break;
|
||||
default:
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $nt_or_lm),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
return (strToUpper(bin2hex($crypted)) === $real);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse smbpasswd file
|
||||
*
|
||||
* Returns a PEAR_Error if passwd file has invalid format.
|
||||
*
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
foreach ($this->_contents as $line){
|
||||
$info = explode(':', $line);
|
||||
if (count($info) < 4) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
$user = array_shift($info);
|
||||
if (!empty($user)) {
|
||||
array_walk($info, 'trim');
|
||||
$this->_users[$user] = @array(
|
||||
'userid' => $info[0],
|
||||
'lmhash' => $info[1],
|
||||
'nthash' => $info[2],
|
||||
'flags' => $info[3],
|
||||
'lct' => $info[4],
|
||||
'comment' => $info[5]
|
||||
);
|
||||
}
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user already exists
|
||||
* o user contains illegal characters
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @access public
|
||||
* @param string $user the user to add
|
||||
* @param string $pass the new plaintext password
|
||||
* @param array $params additional properties of user
|
||||
* + userid
|
||||
* + comment
|
||||
* @param boolean $isMachine whether to add an machine account
|
||||
*/
|
||||
function addUser($user, $pass, $params, $isMachine = false)
|
||||
{
|
||||
if ($this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_ALREADY_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_ALREADY
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
if ($isMachine) {
|
||||
$flags = '[W ]';
|
||||
$user .= '$';
|
||||
} else {
|
||||
$flags = '[U ]';
|
||||
}
|
||||
$this->_users[$user] = array(
|
||||
'flags' => $flags,
|
||||
'userid' => (int)@$params['userid'],
|
||||
'comment' => trim(@$params['comment']),
|
||||
'lct' => 'LCT-' . strToUpper(dechex(time()))
|
||||
);
|
||||
return $this->changePasswd($user, $pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a certain user
|
||||
*
|
||||
* <b>You should not modify the password with this method
|
||||
* unless it is already encrypted as nthash and lmhash!</b>
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o an invalid property was supplied
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user the user to modify
|
||||
* @param array $params an associative array of properties to change
|
||||
*/
|
||||
function modUser($user, $params)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
foreach ($params as $key => $value){
|
||||
$key = strToLower($key);
|
||||
if (!isset($this->_users[$user][$key])) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_PROPERTY_STR, $key),
|
||||
FILE_PASSWD_E_INVALID_PROPERTY
|
||||
);
|
||||
}
|
||||
$this->_users[$user][$key] = trim($value);
|
||||
$this->_users[$user]['lct']= 'LCT-' . strToUpper(dechex(time()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the passwd of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if <var>$user</var> doesn't exist.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
* @param string $user the user whose passwd should be changed
|
||||
* @param string $pass the new plaintext passwd
|
||||
*/
|
||||
function changePasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
if (empty($pass)) {
|
||||
$nthash = $lmhash = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
|
||||
} else {
|
||||
$nthash = strToUpper(bin2hex($this->msc->ntPasswordHash($pass)));
|
||||
$lmhash = strToUpper(bin2hex($this->msc->lmPasswordHash($pass)));
|
||||
}
|
||||
$this->_users[$user]['nthash'] = $nthash;
|
||||
$this->_users[$user]['lmhash'] = $lmhash;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a user's password
|
||||
*
|
||||
* Prefer NT-Hash instead of weak LAN-Manager-Hash
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o user is disabled
|
||||
*
|
||||
* @return mixed true if passwds equal, false if they don't or PEAR_Error
|
||||
* @access public
|
||||
* @param string $user username
|
||||
* @param string $nthash NT-Hash in hex
|
||||
* @param string $lmhash LAN-Manager-Hash in hex
|
||||
*/
|
||||
function verifyEncryptedPasswd($user, $nthash, $lmhash = '')
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
if (strstr($this->_users[$user]['flags'], 'D')) {
|
||||
throw new File_Passwd_Exception("User '$user' is disabled.", 0);
|
||||
}
|
||||
if (!empty($nthash)) {
|
||||
return $this->_users[$user]['nthash'] === strToUpper($nthash);
|
||||
}
|
||||
if (!empty($lmhash)) {
|
||||
return $this->_users[$user]['lm'] === strToUpper($lmhash);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies an account with the given plaintext password
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o user doesn't exist
|
||||
* o user is disabled
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @return mixed true if passwds equal, false if they don't or PEAR_Error
|
||||
* @access public
|
||||
* @param string $user username
|
||||
* @param string $pass the plaintext password
|
||||
*/
|
||||
function verifyPasswd($user, $pass)
|
||||
{
|
||||
$nthash = bin2hex($this->msc->ntPasswordHash($pass));
|
||||
$lmhash = bin2hex($this->msc->lmPasswordHash($pass));
|
||||
return $this->verifyEncryptedPasswd($user, $nthash, $lmhash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes and rewrite CVS passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
foreach ($this->_users as $user => $userdata) {
|
||||
$content .= $user . ':' .
|
||||
$userdata['userid'] . ':' .
|
||||
$userdata['lmhash'] . ':' .
|
||||
$userdata['nthash'] . ':' .
|
||||
$userdata['flags'] . ':' .
|
||||
$userdata['lct'] . ':' .
|
||||
$userdata['comment']. "\n";
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Password
|
||||
*
|
||||
* @static
|
||||
* @access public
|
||||
* @return string The crypted password.
|
||||
* @param string $pass The plaintext password.
|
||||
* @param string $mode The encryption mode to use (nt|lm).
|
||||
*/
|
||||
function generatePasswd($pass, $mode = 'nt')
|
||||
{
|
||||
$chap = &new Crypt_CHAP_MSv1;
|
||||
$hash = strToLower($mode) == 'nt' ?
|
||||
$chap->ntPasswordHash($pass) :
|
||||
$chap->lmPasswordHash($pass);
|
||||
return strToUpper(bin2hex($hash));
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @deprecated
|
||||
*/
|
||||
function generatePassword($pass, $mode = 'nt')
|
||||
{
|
||||
return File_Passwd_Smb::generatePasswd($pass, $mode);
|
||||
}
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,652 @@
|
||||
<?php
|
||||
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
|
||||
|
||||
/**
|
||||
* File::Passwd::Unix
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category FileFormats
|
||||
* @package File_Passwd
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @copyright 2003-2005 Michael Wallner
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id$
|
||||
* @link http://pear.php.net/package/File_Passwd
|
||||
*/
|
||||
|
||||
/**
|
||||
* Requires File::Passwd::Common
|
||||
*/
|
||||
require_once 'File/Passwd/Common.php';
|
||||
|
||||
/**
|
||||
* Manipulate standard Unix passwd files.
|
||||
*
|
||||
* <kbd><u>Usage Example:</u></kbd>
|
||||
* <code>
|
||||
* $passwd = &File_Passwd::factory('Unix');
|
||||
* $passwd->setFile('/my/passwd/file');
|
||||
* $passwd->load();
|
||||
* $passwd->addUser('mike', 'secret');
|
||||
* $passwd->save();
|
||||
* </code>
|
||||
*
|
||||
*
|
||||
* <kbd><u>Output of listUser()</u></kbd>
|
||||
* # using the 'name map':
|
||||
* <pre>
|
||||
* array
|
||||
* + user => array
|
||||
* + pass => crypted_passwd or 'x' if shadowed
|
||||
* + uid => user id
|
||||
* + gid => group id
|
||||
* + gecos => comments
|
||||
* + home => home directory
|
||||
* + shell => standard shell
|
||||
* </pre>
|
||||
* # without 'name map':
|
||||
* <pre>
|
||||
* array
|
||||
* + user => array
|
||||
* + 0 => crypted_passwd
|
||||
* + 1 => ...
|
||||
* + 2 => ...
|
||||
* </pre>
|
||||
*
|
||||
* @author Michael Wallner <mike@php.net>
|
||||
* @package File_Passwd
|
||||
* @version $Revision$
|
||||
* @access public
|
||||
*/
|
||||
class File_Passwd_Unix extends File_Passwd_Common
|
||||
{
|
||||
/**
|
||||
* A 'name map' wich refer to the extra properties
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $_map = array('uid', 'gid', 'gecos', 'home', 'shell');
|
||||
|
||||
/**
|
||||
* Whether to use the 'name map' or not
|
||||
*
|
||||
* @var boolean
|
||||
* @access private
|
||||
*/
|
||||
var $_usemap = true;
|
||||
|
||||
/**
|
||||
* Whether the passwords of this passwd file are shadowed in another file
|
||||
*
|
||||
* @var boolean
|
||||
* @access private
|
||||
*/
|
||||
var $_shadowed = false;
|
||||
|
||||
/**
|
||||
* Encryption mode, either md5 or des
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_mode = 'des';
|
||||
|
||||
/**
|
||||
* Supported encryption modes
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $_modes = array('md5' => 'md5', 'des' => 'des');
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @access public
|
||||
* @param string $file path to passwd file
|
||||
*/
|
||||
function File_Passwd_Unix($file = 'passwd')
|
||||
{
|
||||
parent::__construct($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fast authentication of a certain user
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o file doesn't exist
|
||||
* o file couldn't be opened in read mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked (only if auth fails)
|
||||
* o file couldn't be closed (only if auth fails)
|
||||
* o invalid encryption mode <var>$mode</var> was provided
|
||||
*
|
||||
* @static call this method statically for a reasonable fast authentication
|
||||
* @access public
|
||||
* @return mixed true if authenticated, false if not or PEAR_Error
|
||||
* @param string $file path to passwd file
|
||||
* @param string $user user to authenticate
|
||||
* @param string $pass plaintext password
|
||||
* @param string $mode encryption mode to use (des or md5)
|
||||
*/
|
||||
function staticAuth($file, $user, $pass, $mode)
|
||||
{
|
||||
$line = File_Passwd_Common::_auth($file, $user);
|
||||
if (!$line) {
|
||||
return $line;
|
||||
}
|
||||
list(,$real)= explode(':', $line);
|
||||
$crypted = File_Passwd_Unix::_genPass($pass, $real, $mode);
|
||||
|
||||
return ($crypted === $real);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes an rewrite passwd file
|
||||
*
|
||||
* Returns a PEAR_Error if:
|
||||
* o directory in which the file should reside couldn't be created
|
||||
* o file couldn't be opened in write mode
|
||||
* o file couldn't be locked exclusively
|
||||
* o file couldn't be unlocked
|
||||
* o file couldn't be closed
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function save()
|
||||
{
|
||||
$content = '';
|
||||
foreach ($this->_users as $user => $array){
|
||||
$pass = array_shift($array);
|
||||
$extra = implode(':', $array);
|
||||
$content .= $user . ':' . $pass;
|
||||
if (!empty($extra)) {
|
||||
$content .= ':' . $extra;
|
||||
}
|
||||
$content .= "\n";
|
||||
}
|
||||
return $this->_save($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the Unix password file
|
||||
*
|
||||
* Returns a PEAR_Error if passwd file has invalid format.
|
||||
*
|
||||
* @throws PEAR_Error
|
||||
* @access public
|
||||
* @return mixed true on success or PEAR_Error
|
||||
*/
|
||||
function parse()
|
||||
{
|
||||
$this->_users = array();
|
||||
foreach ($this->_contents as $line){
|
||||
$parts = explode(':', $line);
|
||||
if (count($parts) < 2) {
|
||||
throw new File_Passwd_Exception(
|
||||
FILE_PASSWD_E_INVALID_FORMAT_STR,
|
||||
FILE_PASSWD_E_INVALID_FORMAT
|
||||
);
|
||||
}
|
||||
$user = array_shift($parts);
|
||||
$pass = array_shift($parts);
|
||||
if ($pass == 'x') {
|
||||
$this->_shadowed = true;
|
||||
}
|
||||
$values = array();
|
||||
if ($this->_usemap) {
|
||||
$values['pass'] = $pass;
|
||||
foreach ($parts as $i => $value){
|
||||
if (isset($this->_map[$i])) {
|
||||
$values[$this->_map[$i]] = $value;
|
||||
} else {
|
||||
$values[$i+1] = $value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$values = array_merge(array($pass), $parts);
|
||||
}
|
||||
$this->_users[$user] = $values;
|
||||
|
||||
}
|
||||
$this->_contents = array();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the encryption mode
|
||||
*
|
||||
* Supported encryption modes are des and md5.
|
||||
*
|
||||
* Returns a File_Passwd_Exception if supplied encryption mode is not supported.
|
||||
*
|
||||
* @see setMode()
|
||||
* @see listModes()
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true on succes or File_Passwd_Exception
|
||||
* @param string $mode encryption mode to use; either md5 or des
|
||||
*/
|
||||
function setMode($mode)
|
||||
{
|
||||
$mode = strToLower($mode);
|
||||
if (!isset($this->_modes[$mode])) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $mode),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
$this->_mode = $mode;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get supported encryption modes
|
||||
*
|
||||
* <pre>
|
||||
* array
|
||||
* + md5
|
||||
* + des
|
||||
* </pre>
|
||||
*
|
||||
* @see setMode()
|
||||
* @see getMode()
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function listModes()
|
||||
{
|
||||
return $this->_modes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get actual encryption mode
|
||||
*
|
||||
* @see listModes()
|
||||
* @see setMode()
|
||||
*
|
||||
* @access public
|
||||
* @return string
|
||||
*/
|
||||
function getMode()
|
||||
{
|
||||
return $this->_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use the 'name map' of the extra properties or not
|
||||
*
|
||||
* Default Unix passwd files look like:
|
||||
* <pre>
|
||||
* user:password:user_id:group_id:gecos:home_dir:shell
|
||||
* </pre>
|
||||
*
|
||||
* The default 'name map' for properties except user and password looks like:
|
||||
* o uid
|
||||
* o gid
|
||||
* o gecos
|
||||
* o home
|
||||
* o shell
|
||||
*
|
||||
* If you want to change the naming of the standard map use
|
||||
* File_Passwd_Unix::setMap(array()).
|
||||
*
|
||||
* @see setMap()
|
||||
* @see getMap()
|
||||
*
|
||||
* @access public
|
||||
* @return boolean always true if you set a value (true/false) OR
|
||||
* the actual value if called without param
|
||||
*
|
||||
* @param boolean $bool whether to use the 'name map' or not
|
||||
*/
|
||||
function useMap($bool = null)
|
||||
{
|
||||
if (is_null($bool)) {
|
||||
return $this->_usemap;
|
||||
}
|
||||
$this->_usemap = (bool) $bool;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the 'name map' to use with the extra properties of the user
|
||||
*
|
||||
* This map is used for naming the associative array of the extra properties.
|
||||
*
|
||||
* Returns a File_Passwd_Exception if <var>$map</var> was not of type array.
|
||||
*
|
||||
* @see getMap()
|
||||
* @see useMap()
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true on success or File_Passwd_Exception
|
||||
*/
|
||||
function setMap($map = array())
|
||||
{
|
||||
if (!is_array($map)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_PARAM_MUST_BE_ARRAY_STR, '$map'),
|
||||
FILE_PASSWD_E_PARAM_MUST_BE_ARRAY
|
||||
);
|
||||
}
|
||||
$this->_map = $map;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the 'name map' which is used for the extra properties of the user
|
||||
*
|
||||
* @see setMap()
|
||||
* @see useMap()
|
||||
*
|
||||
* @access public
|
||||
* @return array
|
||||
*/
|
||||
function getMap()
|
||||
{
|
||||
return $this->_map;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the passwords of this passwd file are shadowed in another file.
|
||||
*
|
||||
* @access public
|
||||
* @return boolean
|
||||
*/
|
||||
function isShadowed()
|
||||
{
|
||||
return $this->_shadowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an user
|
||||
*
|
||||
* The username must start with an alphabetical character and must NOT
|
||||
* contain any other characters than alphanumerics, the underline and dash.
|
||||
*
|
||||
* If you use the 'name map' you should also use these naming in
|
||||
* the supplied extra array, because your values would get mixed up
|
||||
* if they are in the wrong order, which is always true if you
|
||||
* DON'T use the 'name map'!
|
||||
*
|
||||
* So be warned and USE the 'name map'!
|
||||
*
|
||||
* If the passwd file is shadowed, the user will be added though, but
|
||||
* with an 'x' as password, and a File_Passwd_Exception will be returned, too.
|
||||
*
|
||||
* Returns a File_Passwd_Exception if:
|
||||
* o user already exists
|
||||
* o user contains illegal characters
|
||||
* o encryption mode is not supported
|
||||
* o passwords are shadowed in another file
|
||||
* o any element of the <var>$extra</var> array contains a colon (':')
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true on success or File_Passwd_Exception
|
||||
* @param string $user the name of the user to add
|
||||
* @param string $pass the password of the user to add
|
||||
* @param array $extra extra properties of user to add
|
||||
*/
|
||||
function addUser($user, $pass, $extra = array())
|
||||
{
|
||||
if ($this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_ALREADY_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_ALREADY
|
||||
);
|
||||
}
|
||||
if (!preg_match($this->_pcre, $user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
if (!is_array($extra)) {
|
||||
setType($extra, 'array');
|
||||
}
|
||||
foreach ($extra as $e){
|
||||
if (strstr($e, ':')) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'Property ', $e),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If passwords of the passwd file are shadowed,
|
||||
* the password of the user will be set to 'x'.
|
||||
*/
|
||||
if ($this->_shadowed) {
|
||||
$pass = 'x';
|
||||
} else {
|
||||
$pass = $this->_genPass($pass);
|
||||
}
|
||||
|
||||
/**
|
||||
* If you don't use the 'name map' the user array will be numeric.
|
||||
*/
|
||||
if (!$this->_usemap) {
|
||||
array_unshift($extra, $pass);
|
||||
$this->_users[$user] = $extra;
|
||||
} else {
|
||||
$map = $this->_map;
|
||||
array_unshift($map, 'pass');
|
||||
$extra['pass'] = $pass;
|
||||
foreach ($map as $key){
|
||||
$this->_users[$user][$key] = @$extra[$key];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise a File_Passwd_Exception if passwords are shadowed.
|
||||
*/
|
||||
if ($this->_shadowed) {
|
||||
throw new File_Passwd_Exception(
|
||||
'Password has been set to \'x\' because they are '.
|
||||
'shadowed in another file.', 0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify properties of a certain user
|
||||
*
|
||||
* # DON'T MODIFY THE PASSWORD WITH THIS METHOD!
|
||||
*
|
||||
* You should use this method only if the 'name map' is used, too.
|
||||
*
|
||||
* Returns a File_Passwd_Exception if:
|
||||
* o user doesn't exist
|
||||
* o any property contains a colon (':')
|
||||
*
|
||||
* @see changePasswd()
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true on success or File_Passwd_Exception
|
||||
* @param string $user the user to modify
|
||||
* @param array $properties an associative array of
|
||||
* properties to modify
|
||||
*/
|
||||
function modUser($user, $properties = array())
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
|
||||
if (!is_array($properties)) {
|
||||
setType($properties, 'array');
|
||||
}
|
||||
|
||||
foreach ($properties as $key => $value){
|
||||
if (strstr($value, ':')) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_CHARS_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_INVALID_CHARS
|
||||
);
|
||||
}
|
||||
$this->_users[$user][$key] = $value;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the password of a certain user
|
||||
*
|
||||
* Returns a File_Passwd_Exception if:
|
||||
* o user doesn't exists
|
||||
* o passwords are shadowed in another file
|
||||
* o encryption mode is not supported
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true on success or File_Passwd_Exception
|
||||
* @param string $user the user whose password should be changed
|
||||
* @param string $pass the new plaintext password
|
||||
*/
|
||||
function changePasswd($user, $pass)
|
||||
{
|
||||
if ($this->_shadowed) {
|
||||
throw new File_Passwd_Exception(
|
||||
'Passwords of this passwd file are shadowed.',
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
|
||||
$pass = $this->_genPass($pass);
|
||||
|
||||
if ($this->_usemap) {
|
||||
$this->_users[$user]['pass'] = $pass;
|
||||
} else {
|
||||
$this->_users[$user][0] = $pass;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the password of a certain user
|
||||
*
|
||||
* Returns a File_Passwd_Exception if:
|
||||
* o user doesn't exist
|
||||
* o encryption mode is not supported
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access public
|
||||
* @return mixed true if passwors equal, false if they don't or File_Passwd_Exception
|
||||
* @param string $user the user whose password should be verified
|
||||
* @param string $pass the password to verify
|
||||
*/
|
||||
function verifyPasswd($user, $pass)
|
||||
{
|
||||
if (!$this->userExists($user)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_EXISTS_NOT_STR, 'User ', $user),
|
||||
FILE_PASSWD_E_EXISTS_NOT
|
||||
);
|
||||
}
|
||||
$real =
|
||||
$this->_usemap ?
|
||||
$this->_users[$user]['pass'] :
|
||||
$this->_users[$user][0]
|
||||
;
|
||||
return ($real === $this->_genPass($pass, $real));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate crypted password from the plaintext password
|
||||
*
|
||||
* Returns a File_Passwd_Exception if actual encryption mode is not supported.
|
||||
*
|
||||
* @throws File_Passwd_Exception
|
||||
* @access private
|
||||
* @return mixed the crypted password or File_Passwd_Exception
|
||||
* @param string $pass the plaintext password
|
||||
* @param string $salt the crypted password from which to gain the salt
|
||||
* @param string $mode the encryption mode to use; don't set, because
|
||||
* it's usually taken from File_Passwd_Unix::_mode
|
||||
*/
|
||||
function _genPass($pass, $salt = null, $mode = null)
|
||||
{
|
||||
static $crypters;
|
||||
if (!isset($crypters)) {
|
||||
$crypters = get_class_methods('File_Passwd');
|
||||
}
|
||||
|
||||
$mode = !isset($mode) ? strToLower($this->_mode) : strToLower($mode);
|
||||
$func = 'crypt_' . $mode;
|
||||
|
||||
if (!in_array($func, $crypters)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, $mode),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
|
||||
return call_user_func(array('File_Passwd', $func), $pass, $salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Password
|
||||
*
|
||||
* Returns File_Passwd_Exception FILE_PASSD_E_INVALID_ENC_MODE if the supplied
|
||||
* encryption mode is not supported.
|
||||
*
|
||||
* @see File_Passwd
|
||||
* @static
|
||||
* @access public
|
||||
* @return mixed The crypted password on success or File_Passwd_Exception on failure.
|
||||
* @param string $pass The plaintext password.
|
||||
* @param string $mode The encryption mode to use.
|
||||
* @param string $salt The salt to use.
|
||||
*/
|
||||
function generatePasswd($pass, $mode = FILE_PASSWD_MD5, $salt = null)
|
||||
{
|
||||
if (!isset($mode)) {
|
||||
throw new File_Passwd_Exception(
|
||||
sprintf(FILE_PASSWD_E_INVALID_ENC_MODE_STR, '<NULL>'),
|
||||
FILE_PASSWD_E_INVALID_ENC_MODE
|
||||
);
|
||||
}
|
||||
return File_Passwd_Unix::_genPass($pass, $salt, $mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* @deprecated
|
||||
*/
|
||||
function generatePassword($pass, $mode = FILE_PASSWD_MD5, $salt = null)
|
||||
{
|
||||
return File_Passwd_Unix::generatePasswd($pass, $mode, $salt);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
+1593
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1138
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
namespace MRBS;
|
||||
|
||||
// CLASSIC MRBS 1.2.6 THEME
|
||||
|
||||
// ***** COLOURS ************************
|
||||
// Colours used in MRBS. All the colours are defined here as PHP variables
|
||||
|
||||
$body_background_color = "#ffffed"; // 主体的背景色
|
||||
$standard_font_color = "black"; // 默认字体颜色
|
||||
$header_font_color = "#ffffff"; // 页眉中文本的字体颜色
|
||||
$highlight_font_color = "red"; // 用于突出显示文本(如链接、错误)
|
||||
$color_key_font_color = "#5B69A6"; // 用于颜色键表
|
||||
|
||||
$banner_back_color = "#c0e0ff"; // 横幅的背景色
|
||||
$banner_border_color = "#5B69A6"; // 横幅的边框颜色
|
||||
$banner_font_color = $banner_border_color; // 横幅的字体颜色
|
||||
|
||||
$header_back_color = "#999999"; // 页眉的背景色
|
||||
|
||||
$admin_table_header_back_color = $header_back_color; // 页眉的背景颜色以及表格单元格的边框颜色
|
||||
$admin_table_header_sep_color = $body_background_color; // vertical separator colour in header
|
||||
$admin_table_header_font_color = $header_font_color; // 页眉的字体颜色
|
||||
$admin_table_border_color = "#C3CCD3";
|
||||
|
||||
$main_table_border_color = "#dddddd"; // 日/周/月表格的边框颜色-外部
|
||||
$main_table_header_border_color = "#dddddd"; // 日/周/月表格的边框颜色-标题
|
||||
$main_table_body_h_border_color = "#ffffff"; // 日/周/月表格的边框颜色-正文,水平
|
||||
$main_table_body_v_border_color = "#e4e4e4"; // 日/周/月表格的边框颜色-正文,垂直
|
||||
$main_table_month_color = "#ffffff"; // 月份视图中日期的背景色
|
||||
$main_table_month_weekend_color = "#f4f4f4"; // 月视图中周末的背景色
|
||||
$main_table_month_holiday_color = "#e8e8e8"; // 月份视图中假日的背景色
|
||||
$main_table_month_weekend_holiday_color = "#dfdfdf"; // 月份视图中周末假期的背景色
|
||||
$main_table_month_invalid_color = "#d0d0d0"; // 月份视图中无效日期的背景色
|
||||
$main_table_slot_invalid_color = "#d1d9de"; // 日视图和周视图中无效时段的背景色
|
||||
$main_table_slot_private_type_color = "#d1d9de"; // 字体必须保密时的背景色
|
||||
$main_table_labels_back_color = "#fff0f0"; // 行标签列的背景色
|
||||
$timeline_color = $header_back_color;
|
||||
|
||||
// 打印时主表的边框颜色。这些由mrbs-print.css.php使用
|
||||
$main_table_border_color_print = "#dddddd"; // 主表的边框颜色(打印视图)
|
||||
$main_table_header_border_color_print = "#dddddd"; // 日/周/月表格的边框颜色-页眉(打印视图)
|
||||
$main_table_body_h_border_color_print = "#dddddd"; // 日/周/月表格的边框颜色-正文,水平(打印视图)
|
||||
$main_table_body_v_border_color_print = "#dddddd"; // 日/周/月表格的边框颜色-正文,垂直(打印视图)
|
||||
|
||||
$report_table_border_color = $standard_font_color;
|
||||
$report_h2_border_color = $banner_back_color; // report.php中<h2>的边框颜色
|
||||
$report_h3_border_color = "#879AA8"; // report.php中<h2>的边框颜色
|
||||
|
||||
$search_table_border_color = $standard_font_color;
|
||||
|
||||
$site_faq_entry_border_color = "#C3CCD3"; // 用于分离help.php中的各个常见问题
|
||||
|
||||
$anchor_link_color = "#5B69A6"; // 链接颜色
|
||||
$anchor_visited_color = "#5B69A6"; // 链接颜色(已访问)
|
||||
$anchor_hover_color = "red"; // 链接颜色(悬停)
|
||||
|
||||
$anchor_link_color_banner = $anchor_link_color; // link color
|
||||
$anchor_visited_color_banner = $anchor_visited_color; // link color (visited)
|
||||
$anchor_hover_color_banner = $anchor_hover_color; // link color (hover)
|
||||
|
||||
$anchor_link_color_header = $anchor_link_color; // link color
|
||||
$anchor_visited_color_header = $anchor_visited_color; // link color (visited)
|
||||
$anchor_hover_color_header = $anchor_hover_color; // link color (hover)
|
||||
|
||||
$column_hidden_color = $main_table_month_invalid_color; // hidden days in the week and month views
|
||||
$calendar_hidden_color = "#dae0e4"; // 迷你卡中隐藏的日子
|
||||
$row_highlight_color = "#ffc0da"; // 用于突出显示一行
|
||||
$row_even_color = "#ffffff"; // 日视图和周视图中的偶数行
|
||||
$row_odd_color = "#eeeeee"; // 日视图和周视图中的奇数行
|
||||
$row_even_color_weekend = "#f4f4f4"; // 周末的日视图和周视图中的偶数行
|
||||
$row_odd_color_weekend = "#e4e4e4"; // 周末的日视图和周视图中的奇数行
|
||||
$row_even_color_holiday = "#e8e8e8"; // 假日的日视图和周视图中的偶数行
|
||||
$row_odd_color_holiday = "#d8d8d8"; // 假日的日视图和周视图中的奇数行
|
||||
$row_even_color_weekend_holiday = "#dfdfdf"; // 周末假期的日视图和周视图中的偶数行
|
||||
$row_odd_color_weekend_holiday = "#cfcfcf"; // 周末假期的日视图和周视图中的奇数行
|
||||
|
||||
$zebra_even_color = "#ffffff"; // 其他表中偶数行的颜色(例如搜索、报告和用户)
|
||||
$zebra_odd_color = '#e2e4ff'; // 其他表中偶数行的颜色(例如搜索、报告和用户)
|
||||
|
||||
$help_highlight_color = "#ffe6f0"; // 突出显示帮助页面上的文本
|
||||
|
||||
// Button colours
|
||||
$button_color_stops = array('#eeeeee', '#cccccc'); // 线性渐变色光圈
|
||||
$button_inset_color = '#a7c7e6';
|
||||
|
||||
// 这些颜色主要用于区分不同类型的预订
|
||||
// 在日、周和月视图中显示
|
||||
$color_types = array(
|
||||
'A' => "#FFCCFF",
|
||||
'B' => "#99CCCC",
|
||||
'C' => "#FF9999",
|
||||
'D' => "#FFFF99",
|
||||
'E' => "#C0E0FF",
|
||||
'F' => "#FFCC99",
|
||||
'G' => "#FF6666",
|
||||
'H' => "#66FFFF",
|
||||
'I' => "#DDFFDD",
|
||||
'J' => "#CCCCCC");
|
||||
|
||||
// 用于pending.php和等待批准的预订的颜色
|
||||
$outstanding_color = "#FF4444"; // 标题中未完成预订信息的字体颜色
|
||||
$pending_header_back_color = $header_back_color; // 系列标题的背景色
|
||||
$series_entry_back_color = "#FFFBC2"; // 系列中条目的背景色
|
||||
$pending_control_color = "#FFF36C"; // pending.php中系列+/-控件的背景色
|
||||
$attention_color = '#FFDFBF'; // 等待批准的预订数量的背景色
|
||||
|
||||
// ***** DIMENSIONS *******************
|
||||
$banner_border_width = '0'; // (px) border width for the outside of the banner
|
||||
$banner_border_cell_width = '1'; // (px) border width for the cells of the banner
|
||||
$main_table_border_width = '0'; // (px) the border width for the outside of the main day/week/month tables
|
||||
$main_table_cell_border_width = '1'; // (px) the border width for the cells of the main day/week/month tables
|
||||
$main_cell_height = '1.5em'; // height of the cells in the main day/week tables
|
||||
|
||||
|
||||
// ***** FONTS ************************
|
||||
$standard_font_family = "Arial, 'Arial Unicode MS', Verdana, sans-serif";
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
function print_theme_footer()
|
||||
{
|
||||
// 等保整改(2026-09-08):备案信息固定显示在页面底部
|
||||
$beian_number = "蜀ICP备2023013112号-1";
|
||||
$beian_url = "https://beian.miit.gov.cn/";
|
||||
$gongan_icon = "/img/gongan.png";
|
||||
$gongan_number = "川公网安备51050402000368号";
|
||||
$gongan_url = "https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=51050402000368";
|
||||
$copyright = "© 2026 泸州龙涧假日酒店";
|
||||
|
||||
echo <<<HTML
|
||||
<style>
|
||||
.mrbs-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255,255,255,0.92);
|
||||
border-top: 1px solid #e2e8f0;
|
||||
text-align: center;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.mrbs-footer a { color: #64748b; text-decoration: none; }
|
||||
.mrbs-footer a:hover { text-decoration: underline; }
|
||||
.mrbs-footer img { border: none; vertical-align: middle; margin: 0 3px; width: 18px; height: 18px; }
|
||||
</style>
|
||||
<div class="mrbs-footer">
|
||||
工信部备案号:<a href="{$beian_url}" target="_blank">{$beian_number}</a>
|
||||
|
|
||||
公安网安备案号:<img src="{$gongan_icon}" alt="公安备案图标"> <a href="{$gongan_url}" target="_blank">{$gongan_number}</a>
|
||||
|
|
||||
{$copyright}
|
||||
</div>
|
||||
HTML;
|
||||
|
||||
echo "</div>\n"; // closing the contents div, opened in print_theme_header()
|
||||
echo "</body>\n";
|
||||
echo "</html>\n";
|
||||
}
|
||||
@@ -0,0 +1,741 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use MRBS\Form\ElementInputDate;
|
||||
use MRBS\Form\ElementInputSearch;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
function print_head(bool $simple=false) : void
|
||||
{
|
||||
global $refresh_rate;
|
||||
|
||||
echo "<head>\n";
|
||||
|
||||
echo "<meta charset=\"" . Language::MRBS_CHARSET . "\">\n";
|
||||
|
||||
// Set IE=edge so that IE10 will display MRBS properly, even if compatibility mode is used
|
||||
// on the browser. If we don't do this then MRBS will treat IE10 as an unsupported browser
|
||||
// when compatibility mode is turned on, potentially confusing users who may have forgotten
|
||||
// that they are using compatibility mode. Unfortunately we can't set IE=edge in the header,
|
||||
// which is where we would normally do it, because then we won't be able to detect IE9 using
|
||||
// conditional comments. So we have to do it in a <meta> tag, after the conditional comments
|
||||
// around the <html> tags.
|
||||
echo "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n";
|
||||
|
||||
// Improve scaling on mobile devices
|
||||
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
// Add the CSRF token so that JavaScript can use it
|
||||
echo "<meta name=\"csrf_token\" content=\"" . escape_html(Form::getToken()) . "\">\n";
|
||||
}
|
||||
|
||||
echo "<meta name=\"robots\" content=\"noindex, nofollow, noarchive\">\n";
|
||||
|
||||
if (($refresh_rate != 0) && (this_page(false, '.php') == 'index'))
|
||||
{
|
||||
// If we're using JavaScript we'll do the refresh by getting a new
|
||||
// table using Ajax requests, which means we only have to download
|
||||
// the table not the whole page each time
|
||||
echo "<noscript>\n";
|
||||
echo "<meta http-equiv=\"Refresh\" content=\"$refresh_rate\">\n";
|
||||
echo "</noscript>\n";
|
||||
}
|
||||
|
||||
echo "<title>" . get_vocab("mrbs") . "</title>\n";
|
||||
// 等保整改(2026-09-08):favicon.ico,构造相对于当前脚本的路径
|
||||
echo "<link rel=\"icon\" href=\"/img/favicon.ico\">\n";
|
||||
|
||||
require_once MRBS_ROOT . "/style.inc";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
require_once MRBS_ROOT . "/js.inc";
|
||||
}
|
||||
|
||||
echo "</head>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the basic site information. This function is used for all headers, including
|
||||
// the simple header, and so mustn't require any database access.
|
||||
function print_header_site_info() : void
|
||||
{
|
||||
global $mrbs_company,
|
||||
$mrbs_company_url,
|
||||
$mrbs_company_logo,
|
||||
$mrbs_company_more_info;
|
||||
|
||||
// Company logo, with a link to the company
|
||||
if (!empty($mrbs_company_logo))
|
||||
{
|
||||
echo "<div class=\"logo\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . escape_html($mrbs_company_url) . '">';
|
||||
}
|
||||
// Suppress error messages in case the logo is a URL, in which case getimagesize() can
|
||||
// fail for any number of reasons, eg (a) allow_url_fopen is not enabled in php.ini or
|
||||
// (b) "SSL operation failed with code 1. OpenSSL Error messages: error:1416F086:SSL
|
||||
// routines:tls_process_server_certificate:certificate verify failed". As the image
|
||||
// size is not essential we'll just carry on.
|
||||
$logo_size = @getimagesize($mrbs_company_logo);
|
||||
echo '<img src="' . $mrbs_company_logo . '"';
|
||||
echo ' alt="' . escape_html($mrbs_company) . '"';
|
||||
if (is_array($logo_size))
|
||||
{
|
||||
echo ' ' . $logo_size[3];
|
||||
}
|
||||
echo '>';
|
||||
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
// Company name, any extra company info and MRBS
|
||||
echo "<div class=\"company\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . escape_html($mrbs_company_url) . '">';
|
||||
}
|
||||
echo '<span>' . escape_html($mrbs_company) . '</span>';
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
if (!empty($mrbs_company_more_info))
|
||||
{
|
||||
// Do not put $mrbs_company_more_info through escape_html() as it is
|
||||
// trusted and allowed to contain HTML.
|
||||
echo "<span class=\"company_more_info\">$mrbs_company_more_info</span>\n";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite('index.php')) . '">' . get_vocab('mrbs') . "</a>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
function print_goto_date(array $context) : void
|
||||
{
|
||||
global $multisite, $site;
|
||||
|
||||
if (!checkAuthorised('index.php', true))
|
||||
{
|
||||
// Don't show the goto box if the user isn't allowed to view the calendar
|
||||
return;
|
||||
}
|
||||
|
||||
$form = new Form();
|
||||
|
||||
$form_id = 'form_nav';
|
||||
|
||||
$form->setAttributes(array('id' => $form_id,
|
||||
'class' => 'js_hidden',
|
||||
'action' => multisite('index.php')))
|
||||
->addHiddenInput('view', $context['view']);
|
||||
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
|
||||
if (isset($room))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$form->addHiddenInput('site', $site);
|
||||
}
|
||||
|
||||
$input = new ElementInputDate();
|
||||
// Add the 'navigation' class so that the JavaScript knows it can use hidden days
|
||||
$input->setAttributes(array(
|
||||
'name' => 'page_date',
|
||||
'value' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'class' => 'navigation',
|
||||
'aria-label' => get_vocab('goto'),
|
||||
'required' => true,
|
||||
'data-submit' => $form_id)
|
||||
);
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttribute('value', get_vocab('goto'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_outstanding(string $query) : void
|
||||
{
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide a link to the list of bookings awaiting approval
|
||||
// (if there are any enabled areas where we require bookings to be approved)
|
||||
$approval_somewhere = some_area('approval_enabled', TRUE);
|
||||
if ($approval_somewhere && ($mrbs_user->level > 0))
|
||||
{
|
||||
$n_outstanding = get_entries_n_outstanding($mrbs_user);
|
||||
|
||||
$class = 'notification';
|
||||
|
||||
if ($n_outstanding > 0)
|
||||
{
|
||||
$class .= ' attention';
|
||||
}
|
||||
|
||||
$url = 'pending.php';
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite($url)) . '"' .
|
||||
" class=\"$class\"" .
|
||||
' title="' . get_vocab('outstanding', $n_outstanding) .
|
||||
"\">$n_outstanding</a>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_menu_items(string $query) : void
|
||||
{
|
||||
global $auth, $kiosk_mode_enabled;
|
||||
|
||||
$menu_items = array('help' => 'help.php',
|
||||
'report' => 'report.php',
|
||||
'import' => 'import.php');
|
||||
|
||||
if ($kiosk_mode_enabled)
|
||||
{
|
||||
$menu_items['kiosk'] = 'kiosk.php';
|
||||
}
|
||||
|
||||
$menu_items['rooms'] = 'admin.php';
|
||||
|
||||
if (auth()->canCreateUsers())
|
||||
{
|
||||
$menu_items['user_list'] = 'edit_users.php';
|
||||
}
|
||||
|
||||
// 等保整改:自助修改密码入口(对已登录及未登录用户均显示)
|
||||
$menu_items['change_password'] = 'change_password.php';
|
||||
|
||||
foreach ($menu_items as $token => $page)
|
||||
{
|
||||
// Only print menu items for which the user is allowed to access the page
|
||||
if (checkAuthorised($page, true))
|
||||
{
|
||||
$url = $page;
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . escape_html(multisite($url)) . '">' . get_vocab($token) . "</a>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function print_search(array $context) : void
|
||||
{
|
||||
if (!checkAuthorised('search.php', true))
|
||||
{
|
||||
// Don't show the search box if the user isn't allowed to search
|
||||
return;
|
||||
}
|
||||
|
||||
echo "<div>\n";
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array(
|
||||
'id' => 'header_search',
|
||||
'action' => multisite('search.php'))
|
||||
)
|
||||
->addHiddenInputs(array(
|
||||
'view' => $context['view'],
|
||||
'year' => $context['year'],
|
||||
'month' => $context['month'],
|
||||
'day' => $context['day'],
|
||||
'from_date' => format_iso_date($context['year'], $context['month'], $context['day'])
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
if (!empty($context['room']))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
$input = new ElementInputSearch();
|
||||
$search_vocab = get_vocab('search');
|
||||
|
||||
$input->setAttributes(array('name' => 'search_str',
|
||||
'placeholder' => $search_vocab,
|
||||
'aria-label' => $search_vocab,
|
||||
'required' => true));
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('value' => get_vocab('search_button'),
|
||||
'class' => 'js_none'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Generate the username link, which gives a report on the user's upcoming bookings.
|
||||
function print_report_link(User $user) : void
|
||||
{
|
||||
// If possible, provide a link to the Report page, otherwise the Search page
|
||||
// and if that's not possible just print the username with no link. (Note that
|
||||
// the Search page isn't the perfect solution because it searches for any bookings
|
||||
// containing the search string, not just those created by the user.)
|
||||
if (checkAuthorised('report.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('report.php'));
|
||||
$hidden_inputs = array('phase' => '2',
|
||||
'creatormatch' => $user->username);
|
||||
}
|
||||
elseif (checkAuthorised('search.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('search.php'));
|
||||
$date_now = new DateTime();
|
||||
$hidden_inputs = array(
|
||||
'search_str' => $user->username,
|
||||
'from_date' => $date_now->getISODate()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<span>' . escape_html($user->display_name) . '</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// We're authorised for either Report or Search so print the form.
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes['id'] = 'show_my_entries';
|
||||
$form->setAttributes($attributes)
|
||||
->addHiddenInputs($hidden_inputs);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('title' => get_vocab('show_my_entries'),
|
||||
'value' => $user->display_name));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logonoff_button(array $params, string $value) : void
|
||||
{
|
||||
$form = new Form($params['method']);
|
||||
$form->setAttributes(array('action' => $params['action']));
|
||||
|
||||
// A Get method will replace the query string in the action URL with a query
|
||||
// string made up of the hidden inputs. So put any parameters in the action
|
||||
// query string into hidden inputs.
|
||||
if ($params['method'] == Form::METHOD_GET)
|
||||
{
|
||||
$query_string = parse_url($params['action'], PHP_URL_QUERY);
|
||||
if (isset($query_string))
|
||||
{
|
||||
parse_str($query_string, $query_parameters);
|
||||
$form->addHiddenInputs($query_parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the hidden fields
|
||||
if (isset($params['hidden_inputs']))
|
||||
{
|
||||
$form->addHiddenInputs($params['hidden_inputs']);
|
||||
}
|
||||
|
||||
// The submit button
|
||||
$element = new ElementInputSubmit();
|
||||
$element->setAttribute('value', $value);
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logon() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogonFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogonFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('login'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_logoff() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogoffFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogoffFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('logoff'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room'.
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_banner(?array $context, $simple=false, $omit_login=false) : void
|
||||
{
|
||||
global $kiosk_QR_code, $auth;
|
||||
|
||||
echo '<header class="banner' . (($simple) ? ' simple' : '') . "\">\n";
|
||||
|
||||
$vars = array();
|
||||
|
||||
if (isset($context['view']))
|
||||
{
|
||||
$vars['view'] = $context['view'];
|
||||
}
|
||||
if (isset($context['year']) && isset($context['month']) && isset($context['day']))
|
||||
{
|
||||
$vars['page_date'] = format_iso_date($context['year'], $context['month'], $context['day']);
|
||||
}
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$vars['area'] = $context['area'];
|
||||
}
|
||||
if (isset($context['room']))
|
||||
{
|
||||
$vars['room'] = $context['room'];
|
||||
}
|
||||
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
print_header_site_info();
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
echo "<nav class=\"container\">\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
|
||||
echo "<nav class=\"menu\">\n";
|
||||
print_menu_items($query);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav class=\"logon\">\n";
|
||||
print_outstanding($query);
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
// The empty string username is a special case when using anonymous booking
|
||||
if (isset($mrbs_user) && (!$auth['allow_anonymous_booking'] || ($mrbs_user->username !== '')))
|
||||
{
|
||||
print_report_link($mrbs_user);
|
||||
print_logoff();
|
||||
}
|
||||
elseif (!$omit_login)
|
||||
{
|
||||
print_logon();
|
||||
}
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
print_goto_date($context);
|
||||
print_search($context);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
// Add in a QR code for kiosk mode
|
||||
// (The QR code library requires PHP 7.4 or greater and the mbstring extension)
|
||||
if (isset($context['kiosk']) &&
|
||||
$kiosk_QR_code &&
|
||||
(version_compare(PHP_VERSION, '7.4') >= 0) &&
|
||||
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
|
||||
defined('MB_CASE_UPPER'))
|
||||
{
|
||||
$url = multisite(url_base() . "/index.php?$query");
|
||||
echo '<nav class="qr" title="' . escape_html($url) . "\">\n";
|
||||
$options = new QROptions([
|
||||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
||||
'imageBase64' => false,
|
||||
]);
|
||||
$qrcode = new QRCode($options);
|
||||
echo $qrcode->render($url);
|
||||
echo "</nav>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</header>\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Print a message which will only be displayed (thanks to CSS) if the user is
|
||||
// using an unsupported browser.
|
||||
function print_unsupported_message(?array $context) : void
|
||||
{
|
||||
echo "<div class=\"unsupported_message\">\n";
|
||||
print_banner($context, true);
|
||||
echo "<div class=\"contents\">\n";
|
||||
echo "<p>" . get_vocab('browser_not_supported', get_vocab('mrbs_abbr')) . "</p>\n";
|
||||
echo "</div>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the page header
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room',
|
||||
// any of which can be NULL.
|
||||
// If $simple is true, then just print a simple header that doesn't require any database
|
||||
// access or JavaScript (useful for fatal errors and database upgrades).
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_theme_header(?array $context=null, bool $simple=false, bool $omit_login=false) : void
|
||||
{
|
||||
global $multisite, $site, $default_view, $default_view_all, $view_week_number, $style_weekends, $watermark_enabled;
|
||||
|
||||
if ($simple)
|
||||
{
|
||||
$data = array();
|
||||
$classes = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the context values if they haven't been given
|
||||
if (!isset($context))
|
||||
{
|
||||
$context = array();
|
||||
}
|
||||
|
||||
if (empty($context['area']))
|
||||
{
|
||||
$context['area'] = get_default_area();
|
||||
}
|
||||
|
||||
if (empty($context['room']))
|
||||
{
|
||||
$context['room'] = get_default_room($context['area']);
|
||||
}
|
||||
|
||||
if (!isset($context['view']))
|
||||
{
|
||||
$context['view'] = (isset($default_view)) ? $default_view : 'day';
|
||||
}
|
||||
|
||||
if (!isset($context['view_all']))
|
||||
{
|
||||
$context['view_all'] = (isset($default_view_all)) ? $default_view_all : true;
|
||||
}
|
||||
|
||||
// Need to set the timezone before we can use date()
|
||||
// This will set the correct timezone for the area
|
||||
get_area_settings($context['area']);
|
||||
|
||||
// If we don't know the right date then use today's
|
||||
if (!isset($context['year']))
|
||||
{
|
||||
$context['year'] = (int) date('Y');
|
||||
}
|
||||
|
||||
if (!isset($context['month']))
|
||||
{
|
||||
$context['month'] = (int) date('m');
|
||||
}
|
||||
|
||||
if (!isset($context['day']))
|
||||
{
|
||||
$context['day'] = (int) date('d');
|
||||
}
|
||||
|
||||
// Get the form token now, before any headers are sent, in case we are using the 'cookie'
|
||||
// session scheme. Otherwise we won't be able to store the Form token.
|
||||
Form::getToken();
|
||||
|
||||
$page = this_page(false, '.php');
|
||||
|
||||
// Put some data attributes in the body element for the benefit of JavaScript. Note that we
|
||||
// cannot use these PHP variables directly in the JavaScript files as those files are cached.
|
||||
|
||||
// Get the language preferences
|
||||
$lang_preferences = Language::getInstance()->getPreferences();
|
||||
// Add to the beginning of the list the best fit locale (which may not necessarily have been the first choice)
|
||||
array_unshift($lang_preferences, mb_strtolower(Language::getInstance()->getWebLocale()));
|
||||
// Remove duplicates and renumber keys
|
||||
$lang_preferences = array_values(array_unique($lang_preferences));
|
||||
|
||||
$data = [
|
||||
'view' => $context['view'],
|
||||
'view_all' => $context['view_all'],
|
||||
'area' => $context['area'],
|
||||
'room' => $context['room'],
|
||||
'page' => $page,
|
||||
'page-date' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'is-admin' => (is_admin()) ? 'true' : 'false',
|
||||
'is-book-admin' => (is_book_admin()) ? 'true' : 'false',
|
||||
'lang-prefs' => json_encode($lang_preferences)
|
||||
];
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$data['site'] = $site;
|
||||
}
|
||||
|
||||
if (isset($context['kiosk']))
|
||||
{
|
||||
$data['kiosk'] = $context['kiosk'];
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$data['username'] = $mrbs_user->username;
|
||||
}
|
||||
|
||||
// We need $timetohighlight for the day and week views
|
||||
$timetohighlight = get_form_var('timetohighlight', 'int');
|
||||
if (isset($timetohighlight))
|
||||
{
|
||||
$data['timetohighlight'] = $timetohighlight;
|
||||
}
|
||||
|
||||
// Put the filename in as a class to aid styling.
|
||||
$classes[] = $page;
|
||||
|
||||
// And if the user is logged in, add another class to aid styling
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$classes[] = 'logged_in';
|
||||
}
|
||||
|
||||
// To help styling
|
||||
if ($view_week_number)
|
||||
{
|
||||
$classes[] = 'view_week_number';
|
||||
}
|
||||
if ($style_weekends)
|
||||
{
|
||||
$classes[] = 'style_weekends';
|
||||
}
|
||||
|
||||
// ===== 等保整改:口令到期 / 首次登录 → 强制跳转改密页(改密页自身除外) =====
|
||||
if (isset($mrbs_user) && !empty($_SESSION['mrbs_force_pwd_change']) &&
|
||||
($page !== 'change_password'))
|
||||
{
|
||||
// 关闭会话写入后重定向,确保强制改密标记已持久化
|
||||
session_write_close();
|
||||
location_header('change_password.php?target_url=' . urlencode(this_page(true)));
|
||||
exit;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$headers = array("Content-Type: text/html; charset=" . Language::MRBS_CHARSET);
|
||||
http_headers($headers);
|
||||
|
||||
echo DOCTYPE . "\n";
|
||||
|
||||
// We produce two <html> tags: one for versions of IE that we don't support and one for all
|
||||
// other browsers. This enables us to use CSS to hide and show the appropriate text.
|
||||
$mrbs_lang = Language::getInstance()->getWebLang();
|
||||
echo "<!--[if lte IE 9]>\n";
|
||||
echo "<html lang=\"" . escape_html($mrbs_lang) . "\" class=\"unsupported_browser\">\n";
|
||||
echo "<![endif]-->\n";
|
||||
echo "<!--[if (!IE)|(gt IE 9)]><!-->\n";
|
||||
echo "<html lang=\"" . escape_html($mrbs_lang) . "\">\n";
|
||||
echo "<!--<![endif]-->\n";
|
||||
|
||||
print_head($simple);
|
||||
|
||||
echo '<body class="' . escape_html(implode(' ', $classes)) . '"';
|
||||
foreach ($data as $key => $value)
|
||||
{
|
||||
if (isset($value))
|
||||
{
|
||||
// Convert booleans to 0 or 1
|
||||
if (is_bool($value))
|
||||
{
|
||||
$value = (int)$value;
|
||||
}
|
||||
echo " data-$key=\"" . escape_html($value) . '"';
|
||||
}
|
||||
}
|
||||
echo ">\n";
|
||||
|
||||
// ===== 等保整改:屏幕水印(防截图/拍照泄密溯源;config.inc.php $watermark_enabled 控制开关) =====
|
||||
if (!$simple && isset($data['username']) && !empty($watermark_enabled))
|
||||
{
|
||||
$wm_user = htmlspecialchars((string)$data['username']);
|
||||
$wm_ip = htmlspecialchars((string)($_SERVER['REMOTE_ADDR'] ?? '-'));
|
||||
echo <<<WM_EOT
|
||||
<div id="screen_watermark" data-user="{$wm_user}" data-ip="{$wm_ip}" aria-hidden="true"></div>
|
||||
<style>
|
||||
#screen_watermark{position:fixed;inset:0;z-index:99999;pointer-events:none;overflow:hidden;opacity:.09}
|
||||
#screen_watermark span{position:absolute;font-size:15px;line-height:1;color:#000;white-space:nowrap;user-select:none;transform:rotate(-28deg);letter-spacing:1px}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
var w=document.getElementById('screen_watermark');if(!w)return;
|
||||
var u=w.getAttribute('data-user')||'';var ip=w.getAttribute('data-ip')||'';if(!u)return;
|
||||
function pad(n){return (n<10)?'0'+n:''+n;}
|
||||
function ts(){var d=new Date();return d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+' '+pad(d.getHours())+':'+pad(d.getMinutes())+':'+pad(d.getSeconds());}
|
||||
function tile(){return u+' '+ip+' '+ts();}
|
||||
function draw(){
|
||||
var cw=Math.max(6,Math.ceil(window.innerWidth/320)),ch=Math.max(4,Math.ceil(window.innerHeight/160));
|
||||
var t=tile();
|
||||
for(var r=0;r<ch;r++){for(var c=0;c<cw;c++){var s=document.createElement('span');s.textContent=t;
|
||||
s.style.left=(c*320+((r%2)*80))+'px';s.style.top=(r*160)+'px';w.appendChild(s);}}
|
||||
}
|
||||
draw();
|
||||
setInterval(function(){var t=tile();var s=w.querySelectorAll('span');for(var i=0;i<s.length;i++){s[i].textContent=t;}},1000);
|
||||
})();
|
||||
</script>
|
||||
WM_EOT;
|
||||
}
|
||||
|
||||
print_unsupported_message($context);
|
||||
|
||||
print_banner($context, $simple, $omit_login);
|
||||
|
||||
// This <div> should really be moved out of here so that we can always see
|
||||
// the matching closing </div>
|
||||
// 等保整改(2026-09-08):背景图从导航栏下方开始(contents div 在 banner 导航栏之下)
|
||||
$contents_class = 'contents';
|
||||
if (!isset($mrbs_user)) { $contents_class .= ' login-bg'; }
|
||||
echo "<div class=\"$contents_class\">\n";
|
||||
|
||||
|
||||
} // end of print_theme_header()
|
||||
|
||||
@@ -0,0 +1,673 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use MRBS\Form\ElementInputDate;
|
||||
use MRBS\Form\ElementInputSearch;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
function print_head(bool $simple=false) : void
|
||||
{
|
||||
global $refresh_rate;
|
||||
|
||||
echo "<head>\n";
|
||||
|
||||
echo "<meta charset=\"" . get_charset() . "\">\n";
|
||||
|
||||
// Set IE=edge so that IE10 will display MRBS properly, even if compatibility mode is used
|
||||
// on the browser. If we don't do this then MRBS will treat IE10 as an unsupported browser
|
||||
// when compatibility mode is turned on, potentially confusing users who may have forgotten
|
||||
// that they are using compatibility mode. Unfortunately we can't set IE=edge in the header,
|
||||
// which is where we would normally do it, because then we won't be able to detect IE9 using
|
||||
// conditional comments. So we have to do it in a <meta> tag, after the conditional comments
|
||||
// around the <html> tags.
|
||||
echo "<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n";
|
||||
|
||||
// Improve scaling on mobile devices
|
||||
echo "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
// Add the CSRF token so that JavaScript can use it
|
||||
echo "<meta name=\"csrf_token\" content=\"" . htmlspecialchars(Form::getToken()) . "\">\n";
|
||||
}
|
||||
|
||||
echo "<meta name=\"robots\" content=\"noindex, nofollow, noarchive\">\n";
|
||||
|
||||
if (($refresh_rate != 0) && (this_page(false, '.php') == 'index'))
|
||||
{
|
||||
// If we're using JavaScript we'll do the refresh by getting a new
|
||||
// table using Ajax requests, which means we only have to download
|
||||
// the table not the whole page each time
|
||||
echo "<noscript>\n";
|
||||
echo "<meta http-equiv=\"Refresh\" content=\"$refresh_rate\">\n";
|
||||
echo "</noscript>\n";
|
||||
}
|
||||
|
||||
echo "<title>" . get_vocab("mrbs") . "</title>\n";
|
||||
|
||||
require_once MRBS_ROOT . "/style.inc";
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
require_once MRBS_ROOT . "/js.inc";
|
||||
}
|
||||
|
||||
echo "</head>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the basic site information. This function is used for all headers, including
|
||||
// the simple header, and so mustn't require any database access.
|
||||
function print_header_site_info() : void
|
||||
{
|
||||
global $mrbs_company,
|
||||
$mrbs_company_url,
|
||||
$mrbs_company_logo,
|
||||
$mrbs_company_more_info;
|
||||
|
||||
// Company logo, with a link to the company
|
||||
if (!empty($mrbs_company_logo))
|
||||
{
|
||||
echo "<div class=\"logo\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . htmlspecialchars($mrbs_company_url) . '">';
|
||||
}
|
||||
// Suppress error messages in case the logo is a URL, in which case getimagesize() can
|
||||
// fail for any number of reasons, eg (a) allow_url_fopen is not enabled in php.ini or
|
||||
// (b) "SSL operation failed with code 1. OpenSSL Error messages: error:1416F086:SSL
|
||||
// routines:tls_process_server_certificate:certificate verify failed". As the image
|
||||
// size is not essential we'll just carry on.
|
||||
$logo_size = @getimagesize($mrbs_company_logo);
|
||||
echo '<img src="' . $mrbs_company_logo . '"';
|
||||
echo ' alt="' . htmlspecialchars($mrbs_company) . '"';
|
||||
if (is_array($logo_size))
|
||||
{
|
||||
echo ' ' . $logo_size[3];
|
||||
}
|
||||
echo '>';
|
||||
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
// Company name, any extra company info and MRBS
|
||||
echo "<div class=\"company\">\n";
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo '<a href="' . htmlspecialchars($mrbs_company_url) . '">';
|
||||
}
|
||||
echo '<span>' . htmlspecialchars($mrbs_company) . '</span>';
|
||||
if (!empty($mrbs_company_url))
|
||||
{
|
||||
echo "</a>\n";
|
||||
}
|
||||
if (!empty($mrbs_company_more_info))
|
||||
{
|
||||
// Do not put $mrbs_company_more_info through htmlspecialchars() as it is
|
||||
// trusted and allowed to contain HTML.
|
||||
echo "<span class=\"company_more_info\">$mrbs_company_more_info</span>\n";
|
||||
}
|
||||
echo '<a href="' . htmlspecialchars(multisite('index.php')) . '">' . get_vocab('mrbs') . "</a>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
function print_goto_date(array $context) : void
|
||||
{
|
||||
global $multisite, $site;
|
||||
|
||||
if (!checkAuthorised('index.php', true))
|
||||
{
|
||||
// Don't show the goto box if the user isn't allowed to view the calendar
|
||||
return;
|
||||
}
|
||||
|
||||
$form = new Form();
|
||||
|
||||
$form_id = 'form_nav';
|
||||
|
||||
$form->setAttributes(array('id' => $form_id,
|
||||
'class' => 'js_hidden',
|
||||
'action' => multisite('index.php')))
|
||||
->addHiddenInput('view', $context['view']);
|
||||
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
|
||||
if (isset($room))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$form->addHiddenInput('site', $site);
|
||||
}
|
||||
|
||||
$input = new ElementInputDate();
|
||||
$input->setAttributes(array('name' => 'page_date',
|
||||
'value' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'aria-label' => get_vocab('goto'),
|
||||
'required' => true,
|
||||
'data-submit' => $form_id));
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttribute('value', get_vocab('goto'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_outstanding(string $query) : void
|
||||
{
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide a link to the list of bookings awaiting approval
|
||||
// (if there are any enabled areas where we require bookings to be approved)
|
||||
$approval_somewhere = some_area('approval_enabled', TRUE);
|
||||
if ($approval_somewhere && ($mrbs_user->level > 0))
|
||||
{
|
||||
$n_outstanding = get_entries_n_outstanding($mrbs_user);
|
||||
|
||||
$class = 'notification';
|
||||
|
||||
if ($n_outstanding > 0)
|
||||
{
|
||||
$class .= ' attention';
|
||||
}
|
||||
|
||||
$url = 'pending.php';
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . htmlspecialchars(multisite($url)) . '"' .
|
||||
" class=\"$class\"" .
|
||||
' title="' . get_vocab('outstanding', $n_outstanding) .
|
||||
"\">$n_outstanding</a>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_menu_items(string $query) : void
|
||||
{
|
||||
global $auth, $kiosk_mode_enabled;
|
||||
|
||||
$menu_items = array('help' => 'help.php',
|
||||
'report' => 'report.php',
|
||||
'import' => 'import.php');
|
||||
|
||||
if ($kiosk_mode_enabled)
|
||||
{
|
||||
$menu_items['kiosk'] = 'kiosk.php';
|
||||
}
|
||||
|
||||
$menu_items['rooms'] = 'admin.php';
|
||||
|
||||
if ($auth['type'] == 'db')
|
||||
{
|
||||
$menu_items['user_list'] = 'edit_users.php';
|
||||
}
|
||||
|
||||
foreach ($menu_items as $token => $page)
|
||||
{
|
||||
// Only print menu items for which the user is allowed to access the page
|
||||
if (checkAuthorised($page, true))
|
||||
{
|
||||
$url = $page;
|
||||
if ($query !== '')
|
||||
{
|
||||
$url .= "?$query";
|
||||
}
|
||||
echo '<a href="' . htmlspecialchars(multisite($url)) . '">' . get_vocab($token) . "</a>\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function print_search(array $context) : void
|
||||
{
|
||||
if (!checkAuthorised('search.php', true))
|
||||
{
|
||||
// Don't show the search box if the user isn't allowed to search
|
||||
return;
|
||||
}
|
||||
|
||||
echo "<div>\n";
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array(
|
||||
'id' => 'header_search',
|
||||
'action' => multisite('search.php'))
|
||||
)
|
||||
->addHiddenInputs(array(
|
||||
'view' => $context['view'],
|
||||
'year' => $context['year'],
|
||||
'month' => $context['month'],
|
||||
'day' => $context['day'],
|
||||
'from_date' => format_iso_date($context['year'], $context['month'], $context['day'])
|
||||
)
|
||||
);
|
||||
|
||||
if (!empty($context['area']))
|
||||
{
|
||||
$form->addHiddenInput('area', $context['area']);
|
||||
}
|
||||
if (!empty($context['room']))
|
||||
{
|
||||
$form->addHiddenInput('room', $context['room']);
|
||||
}
|
||||
|
||||
$input = new ElementInputSearch();
|
||||
$search_vocab = get_vocab('search');
|
||||
|
||||
$input->setAttributes(array('name' => 'search_str',
|
||||
'placeholder' => $search_vocab,
|
||||
'aria-label' => $search_vocab,
|
||||
'required' => true));
|
||||
|
||||
$form->addElement($input);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('value' => get_vocab('search_button'),
|
||||
'class' => 'js_none'));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Generate the username link, which gives a report on the user's upcoming bookings.
|
||||
function print_report_link(User $user) : void
|
||||
{
|
||||
// If possible, provide a link to the Report page, otherwise the Search page
|
||||
// and if that's not possible just print the username with no link. (Note that
|
||||
// the Search page isn't the perfect solution because it searches for any bookings
|
||||
// containing the search string, not just those created by the user.)
|
||||
if (checkAuthorised('report.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('report.php'));
|
||||
$hidden_inputs = array('phase' => '2',
|
||||
'creatormatch' => $user->username);
|
||||
}
|
||||
elseif (checkAuthorised('search.php', true))
|
||||
{
|
||||
$attributes = array('action' => multisite('search.php'));
|
||||
$date_now = new DateTime();
|
||||
$hidden_inputs = array(
|
||||
'search_str' => $user->username,
|
||||
'from_date' => $date_now->getISODate()
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<span>' . htmlspecialchars($user->display_name) . '</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// We're authorised for either Report or Search so print the form.
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes['id'] = 'show_my_entries';
|
||||
$form->setAttributes($attributes)
|
||||
->addHiddenInputs($hidden_inputs);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array('title' => get_vocab('show_my_entries'),
|
||||
'value' => $user->display_name));
|
||||
$form->addElement($submit);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logonoff_button(array $params, string $value) : void
|
||||
{
|
||||
$form = new Form($params['method']);
|
||||
$form->setAttributes(array('action' => $params['action']));
|
||||
|
||||
// A Get method will replace the query string in the action URL with a query
|
||||
// string made up of the hidden inputs. So put any parameters in the action
|
||||
// query string into hidden inputs.
|
||||
if ($params['method'] == Form::METHOD_GET)
|
||||
{
|
||||
$query_string = parse_url($params['action'], PHP_URL_QUERY);
|
||||
if (isset($query_string))
|
||||
{
|
||||
parse_str($query_string, $query_parameters);
|
||||
$form->addHiddenInputs($query_parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the hidden fields
|
||||
if (isset($params['hidden_inputs']))
|
||||
{
|
||||
$form->addHiddenInputs($params['hidden_inputs']);
|
||||
}
|
||||
|
||||
// The submit button
|
||||
$element = new ElementInputSubmit();
|
||||
$element->setAttribute('value', $value);
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function print_logon() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogonFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogonFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('login'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function print_logoff() : void
|
||||
{
|
||||
if (method_exists(session(), 'getLogoffFormParams'))
|
||||
{
|
||||
$form_params = session()->getLogoffFormParams();
|
||||
if (isset($form_params))
|
||||
{
|
||||
print_logonoff_button($form_params, get_vocab('logoff'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room'.
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_banner(?array $context, $simple=false, $omit_login=false) : void
|
||||
{
|
||||
global $kiosk_QR_code, $auth;
|
||||
|
||||
echo '<header class="banner' . (($simple) ? ' simple' : '') . "\">\n";
|
||||
|
||||
$vars = array();
|
||||
|
||||
if (isset($context['view']))
|
||||
{
|
||||
$vars['view'] = $context['view'];
|
||||
}
|
||||
if (isset($context['year']) && isset($context['month']) && isset($context['day']))
|
||||
{
|
||||
$vars['page_date'] = format_iso_date($context['year'], $context['month'], $context['day']);
|
||||
}
|
||||
if (isset($context['area']))
|
||||
{
|
||||
$vars['area'] = $context['area'];
|
||||
}
|
||||
if (isset($context['room']))
|
||||
{
|
||||
$vars['room'] = $context['room'];
|
||||
}
|
||||
|
||||
$query = http_build_query($vars, '', '&');
|
||||
|
||||
print_header_site_info();
|
||||
|
||||
if (!$simple)
|
||||
{
|
||||
echo "<nav class=\"container\">\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
|
||||
echo "<nav class=\"menu\">\n";
|
||||
print_menu_items($query);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav class=\"logon\">\n";
|
||||
print_outstanding($query);
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
|
||||
// The empty string username is a special case when using anonymous booking
|
||||
if (isset($mrbs_user) && (!$auth['allow_anonymous_booking'] || ($mrbs_user->username !== '')))
|
||||
{
|
||||
print_report_link($mrbs_user);
|
||||
print_logoff();
|
||||
}
|
||||
elseif (!$omit_login)
|
||||
{
|
||||
print_logon();
|
||||
}
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "<nav>\n";
|
||||
print_goto_date($context);
|
||||
print_search($context);
|
||||
echo "</nav>\n";
|
||||
|
||||
echo "</nav>\n";
|
||||
|
||||
// Add in a QR code for kiosk mode
|
||||
// (The QR code library requires PHP 7.4 or greater and the mbstring extension)
|
||||
if (isset($context['kiosk']) &&
|
||||
$kiosk_QR_code &&
|
||||
(version_compare(PHP_VERSION, '7.4') >= 0) &&
|
||||
//Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled
|
||||
defined('MB_CASE_UPPER'))
|
||||
{
|
||||
$url = multisite(url_base() . "/index.php?$query");
|
||||
echo '<nav class="qr" title="' . htmlspecialchars($url) . "\">\n";
|
||||
$options = new QROptions([
|
||||
'outputType' => QRCode::OUTPUT_MARKUP_SVG,
|
||||
'imageBase64' => false,
|
||||
]);
|
||||
$qrcode = new QRCode($options);
|
||||
echo $qrcode->render($url);
|
||||
echo "</nav>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</header>\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Print a message which will only be displayed (thanks to CSS) if the user is
|
||||
// using an unsupported browser.
|
||||
function print_unsupported_message(?array $context) : void
|
||||
{
|
||||
echo "<div class=\"unsupported_message\">\n";
|
||||
print_banner($context, true);
|
||||
echo "<div class=\"contents\">\n";
|
||||
echo "<p>" . get_vocab('browser_not_supported', get_vocab('mrbs_abbr')) . "</p>\n";
|
||||
echo "</div>\n";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// Print the page header
|
||||
// $context is an associative array indexed by 'view', 'view_all', 'year', 'month', 'day', 'area' and 'room',
|
||||
// any of which can be NULL.
|
||||
// If $simple is true, then just print a simple header that doesn't require any database
|
||||
// access or JavaScript (useful for fatal errors and database upgrades).
|
||||
// When $omit_login is true the Login link is omitted.
|
||||
function print_theme_header(?array $context=null, bool $simple=false, bool $omit_login=false) : void
|
||||
{
|
||||
global $multisite, $site, $default_view, $default_view_all, $view_week_number, $style_weekends;
|
||||
|
||||
if ($simple)
|
||||
{
|
||||
$data = array();
|
||||
$classes = array();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Set the context values if they haven't been given
|
||||
if (!isset($context))
|
||||
{
|
||||
$context = array();
|
||||
}
|
||||
|
||||
if (empty($context['area']))
|
||||
{
|
||||
$context['area'] = get_default_area();
|
||||
}
|
||||
|
||||
if (empty($context['room']))
|
||||
{
|
||||
$context['room'] = get_default_room($context['area']);
|
||||
}
|
||||
|
||||
if (!isset($context['view']))
|
||||
{
|
||||
$context['view'] = (isset($default_view)) ? $default_view : 'day';
|
||||
}
|
||||
|
||||
if (!isset($context['view_all']))
|
||||
{
|
||||
$context['view_all'] = (isset($default_view_all)) ? $default_view_all : true;
|
||||
}
|
||||
|
||||
// Need to set the timezone before we can use date()
|
||||
// This will set the correct timezone for the area
|
||||
get_area_settings($context['area']);
|
||||
|
||||
// If we don't know the right date then use today's
|
||||
if (!isset($context['year']))
|
||||
{
|
||||
$context['year'] = (int) date('Y');
|
||||
}
|
||||
|
||||
if (!isset($context['month']))
|
||||
{
|
||||
$context['month'] = (int) date('m');
|
||||
}
|
||||
|
||||
if (!isset($context['day']))
|
||||
{
|
||||
$context['day'] = (int) date('d');
|
||||
}
|
||||
|
||||
// Get the form token now, before any headers are sent, in case we are using the 'cookie'
|
||||
// session scheme. Otherwise we won't be able to store the Form token.
|
||||
Form::getToken();
|
||||
|
||||
$page = this_page(false, '.php');
|
||||
|
||||
// Put some data attributes in the body element for the benefit of JavaScript. Note that we
|
||||
// cannot use these PHP variables directly in the JavaScript files as those files are cached.
|
||||
$data = array(
|
||||
'view' => $context['view'],
|
||||
'view_all' => $context['view_all'],
|
||||
'area' => $context['area'],
|
||||
'room' => $context['room'],
|
||||
'page' => $page,
|
||||
'page-date' => format_iso_date($context['year'], $context['month'], $context['day']),
|
||||
'is-admin' => (is_admin()) ? 'true' : 'false',
|
||||
'is-book-admin' => (is_book_admin()) ? 'true' : 'false',
|
||||
'lang-prefs' => json_encode(get_lang_preferences())
|
||||
);
|
||||
|
||||
if ($multisite && isset($site) && ($site !== ''))
|
||||
{
|
||||
$data['site'] = $site;
|
||||
}
|
||||
|
||||
if (isset($context['kiosk']))
|
||||
{
|
||||
$data['kiosk'] = $context['kiosk'];
|
||||
}
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$data['username'] = $mrbs_user->username;
|
||||
}
|
||||
|
||||
// We need $timetohighlight for the day and week views
|
||||
$timetohighlight = get_form_var('timetohighlight', 'int');
|
||||
if (isset($timetohighlight))
|
||||
{
|
||||
$data['timetohighlight'] = $timetohighlight;
|
||||
}
|
||||
|
||||
// Put the filename in as a class to aid styling.
|
||||
$classes[] = $page;
|
||||
// And if the user is logged in, add another class to aid styling
|
||||
if (isset($mrbs_user))
|
||||
{
|
||||
$classes[] = 'logged_in';
|
||||
}
|
||||
|
||||
// To help styling
|
||||
if ($view_week_number)
|
||||
{
|
||||
$classes[] = 'view_week_number';
|
||||
}
|
||||
if ($style_weekends)
|
||||
{
|
||||
$classes[] = 'style_weekends';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$headers = array("Content-Type: text/html; charset=" . get_charset());
|
||||
http_headers($headers);
|
||||
|
||||
echo DOCTYPE . "\n";
|
||||
|
||||
// We produce two <html> tags: one for versions of IE that we don't support and one for all
|
||||
// other browsers. This enables us to use CSS to hide and show the appropriate text.
|
||||
echo "<!--[if lte IE 9]>\n";
|
||||
echo "<html lang=\"" . htmlspecialchars(get_mrbs_lang()) . "\" class=\"unsupported_browser\">\n";
|
||||
echo "<![endif]-->\n";
|
||||
echo "<!--[if (!IE)|(gt IE 9)]><!-->\n";
|
||||
echo "<html lang=\"" . htmlspecialchars(get_mrbs_lang()) . "\">\n";
|
||||
echo "<!--<![endif]-->\n";
|
||||
|
||||
print_head($simple);
|
||||
|
||||
echo '<body class="' . htmlspecialchars(implode(' ', $classes)) . '"';
|
||||
foreach ($data as $key => $value)
|
||||
{
|
||||
if (isset($value))
|
||||
{
|
||||
echo " data-$key=\"" . htmlspecialchars((string)$value) . '"';
|
||||
}
|
||||
}
|
||||
echo ">\n";
|
||||
|
||||
print_unsupported_message($context);
|
||||
|
||||
print_banner($context, $simple, $omit_login);
|
||||
|
||||
// This <div> should really be moved out of here so that we can always see
|
||||
// the matching closing </div>
|
||||
echo "<div class=\"contents\">\n";
|
||||
|
||||
|
||||
} // end of print_theme_header()
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
namespace MRBS;
|
||||
|
||||
// DEFAULT THEME
|
||||
|
||||
// ***** COLOURS ************************
|
||||
// Colours used in MRBS. All the colours are defined here as PHP variables
|
||||
|
||||
$body_background_color = "#f9fbfc"; // background colour for the main body
|
||||
$login_background_image = "img/login_bg.png"; // 等保优化(2026-09-08):登录页背景图(留空则不用)
|
||||
$login_background_overlay = "rgba(255,255,255,0.55)"; // 背景上方遮罩,提升登录表单可读性
|
||||
$standard_font_color = "#0B263B"; // default font color
|
||||
$header_font_color = "#ffffff"; // font color for text in headers
|
||||
$highlight_font_color = "#ff0066"; // used for highlighting text (eg links, errors)
|
||||
$color_key_font_color = $standard_font_color; // used in the colour key table
|
||||
|
||||
$banner_back_color = "#b598a1"; // background colour for banner
|
||||
$banner_border_color = $body_background_color; // border colour for banner
|
||||
$banner_font_color = $header_font_color; // font colour for banner
|
||||
$banner_nav_hover_color = 'darkblue'; // background colour when header links are hovered over
|
||||
|
||||
$minical_view_color = "#2b3a42"; // used for highlighting the dates in the current view
|
||||
$minical_today_color = "#bdd4de"; // used for highlighting today
|
||||
|
||||
$header_back_color = $banner_back_color; // background colour for headers
|
||||
$flatpickr_highlight_color = $banner_back_color; // used for highlighting the date
|
||||
|
||||
$admin_table_header_back_color = $header_back_color; // background colour for header and also border colour for table cells
|
||||
$admin_table_header_sep_color = $body_background_color; // vertical separator colour in header
|
||||
$admin_table_header_font_color = $header_font_color; // font colour for header
|
||||
$admin_table_border_color = "#C3CCD3";
|
||||
|
||||
$main_table_border_color = "#dddddd"; // border colour for day/week/month tables - outside
|
||||
$main_table_header_border_color = "#dddddd"; // border colour for day/week/month tables - header
|
||||
$main_table_body_h_border_color = "#ffffff"; // border colour for day/week/month tables - body, horizontal
|
||||
$main_table_body_v_border_color = "#e4e4e4"; // border colour for day/week/month tables - body, vertical
|
||||
$main_table_month_color = "#ffffff"; // background colour for days in the month view
|
||||
$main_table_month_weekend_color = "#f4f4f4"; // background colour for weekends in the month view
|
||||
$main_table_month_holiday_color = "#e8e8e8"; // background colour for holidays in the month view
|
||||
$main_table_month_weekend_holiday_color = "#dfdfdf"; // background colour for weekend holidays in the month view
|
||||
$main_table_month_invalid_color = "#d1d9de"; // background colour for invalid days in the month view
|
||||
$main_table_slot_invalid_color = "#d1d9de"; // background colour for invalid slots in the day and week views
|
||||
$main_table_slot_private_type_color = "#d1d9de"; // background colour when the type has to kept private
|
||||
$main_table_labels_back_color = $header_back_color; // background colour for the row labels column
|
||||
$timeline_color = $header_back_color;
|
||||
|
||||
// border colours for the main table when it is printed. These are used by mrbs-print.css.php
|
||||
$main_table_border_color_print = "#dddddd"; // border colour for the main table (print view)
|
||||
$main_table_header_border_color_print = "#dddddd"; // border colour for day/week/month tables - header (print view)
|
||||
$main_table_body_h_border_color_print = "#dddddd"; // border colour for day/week/month tables - body, horizontal (print view)
|
||||
$main_table_body_v_border_color_print = "#dddddd"; // border colour for day/week/month tables - body, vertical (print view)
|
||||
|
||||
// font colours for the main table when it is printed
|
||||
$header_font_color_print = "#0B263B";
|
||||
$anchor_link_color_header_print = "#0B263B";
|
||||
|
||||
$report_table_border_color = $standard_font_color;
|
||||
$report_h2_border_color = $banner_back_color; // border colour for <h2> in report.php
|
||||
$report_h3_border_color = "#879AA8"; // border colour for <h2> in report.php
|
||||
|
||||
$search_table_border_color = $standard_font_color;
|
||||
|
||||
$site_faq_entry_border_color = "#C3CCD3"; // used to separate individual FAQ's in help.php
|
||||
|
||||
$anchor_link_color = $standard_font_color; // link color
|
||||
$anchor_visited_color = $anchor_link_color; // link color (visited)
|
||||
$anchor_hover_color = $anchor_link_color; // link color (hover)
|
||||
|
||||
$anchor_link_color_banner = $header_font_color; // link color
|
||||
$anchor_visited_color_banner = $anchor_link_color_banner; // link color (visited)
|
||||
$anchor_hover_color_banner = $anchor_link_color_banner; // link color (hover)
|
||||
|
||||
$anchor_link_color_header = $standard_font_color; // link color
|
||||
$anchor_visited_color_header = $anchor_link_color_header; // link color (visited)
|
||||
$anchor_hover_color_header = $anchor_link_color_header; // link color (hover)
|
||||
|
||||
$column_hidden_color = $main_table_month_invalid_color; // hidden days in the week and month views
|
||||
$calendar_hidden_color = "#dae0e4"; // hidden days in the mini-cals
|
||||
$row_highlight_color = $banner_back_color; // used for highlighting a row
|
||||
$row_even_color = "#ffffff"; // even rows in the day and week views
|
||||
$row_odd_color = "#efefef"; // odd rows in the day and week views
|
||||
$row_even_color_weekend = "#f4f4f4"; // even rows in the day and week views for weekends
|
||||
$row_odd_color_weekend = "#e4e4e4"; // odd rows in the day and week views for weekends
|
||||
$row_even_color_holiday = "#e8e8e8"; // even rows in the day and week views for holidays
|
||||
$row_odd_color_holiday = "#d8d8d8"; // odd rows in the day and week views for holidays
|
||||
$row_even_color_weekend_holiday = "#dfdfdf"; // even rows in the day and week views for weekend holidays
|
||||
$row_odd_color_weekend_holiday = "#cfcfcf"; // odd rows in the day and week views for weekend holidays
|
||||
|
||||
$zebra_even_color = "#ffffff"; // Colour for even rows in other tables (eg Search, Report and Users)
|
||||
$zebra_odd_color = '#e2e4ff'; // Colour for odd rows in other tables (eg Search, Report and Users)
|
||||
|
||||
$help_highlight_color = "#ffe6f0"; // highlighting text on the help page
|
||||
|
||||
// Button colours
|
||||
$button_color_stops = array('#eeeeee', '#cccccc'); // linear gradient colour stops
|
||||
$button_inset_color = 'darkblue';
|
||||
|
||||
// These are the colours used for distinguishing between the different types of bookings in the main
|
||||
// displays in the day, week and month views
|
||||
$color_types = array(
|
||||
'A' => "#ffff99",
|
||||
'B' => "#99cccc",
|
||||
'C' => "#ffffcd",
|
||||
'D' => "#cde6e6",
|
||||
'E' => "#ee3f4d", //外部使用
|
||||
'F' => "#82adad",
|
||||
'G' => "#ccffcc",
|
||||
'H' => "#d9d982",
|
||||
'I' => "#0096FF", //内部使用
|
||||
'J' => "#e6ffe6");
|
||||
|
||||
// colours used for pending.php and bookings awaiting approval
|
||||
$outstanding_color = "#FFF36C"; // font colour for the outstanding reservations message in the header
|
||||
$pending_header_back_color = $header_back_color; // background colour for series headers
|
||||
$series_entry_back_color = "#FFFCDA"; // background colour for entries in a series
|
||||
$pending_control_color = "#FFF36C"; // background colour for the series +/- controls in pending.php
|
||||
$attention_color = 'darkorange'; // background colour for the number of bookings awaiting approval
|
||||
|
||||
// ***** DIMENSIONS *******************
|
||||
$banner_border_width = '0'; // (px) border width for the outside of the banner
|
||||
$banner_border_cell_width = '1'; // (px) border width for the cells of the banner
|
||||
$main_table_border_width = '0'; // (px) border width for the outside of the main day/week/month tables
|
||||
$main_table_cell_border_width = '1'; // (px) vertical border width for the cells of the main day/week/month tables
|
||||
$main_cell_height = '1.5em'; // height of the cells in the main day/week tables
|
||||
|
||||
|
||||
// ***** FONTS ************************
|
||||
$standard_font_family = "Arial, 'Arial Unicode MS', Verdana, sans-serif";
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
require_once "mrbs_sql.inc";
|
||||
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Get non-standard form variables
|
||||
$name = get_form_var('name', 'string', null, INPUT_POST);
|
||||
$description = get_form_var('description', 'string', null, INPUT_POST);
|
||||
$capacity = get_form_var('capacity', 'int', null, INPUT_POST);
|
||||
$room_admin_email = get_form_var('room_admin_email', 'string', null, INPUT_POST);
|
||||
$type = get_form_var('type', 'string', null, INPUT_POST);
|
||||
|
||||
// This file is for adding new areas/rooms
|
||||
$error = '';
|
||||
|
||||
// First of all check that we've got an area or room name
|
||||
if (!isset($name) || ($name === ''))
|
||||
{
|
||||
$error = "empty_name";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Strip out any extra whitespace that the user may accidentally have typed in the name
|
||||
$name = remove_extra_whitespace($name);
|
||||
// We need to do different things depending on if it's a room
|
||||
// or an area
|
||||
if ($type == "area")
|
||||
{
|
||||
$area = mrbsAddArea($name, $error);
|
||||
}
|
||||
elseif ($type == "room")
|
||||
{
|
||||
$room = mrbsAddRoom($name, $area, $error, $description, $capacity, $room_admin_email);
|
||||
}
|
||||
}
|
||||
|
||||
$returl = "admin.php?area=$area" . (!empty($error) ? "&error=$error" : "");
|
||||
location_header($returl);
|
||||
+606
@@ -0,0 +1,606 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\ElementButton;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementImg;
|
||||
use MRBS\Form\ElementInputImage;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\FieldInputEmail;
|
||||
use MRBS\Form\FieldInputNumber;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\FieldInputText;
|
||||
use MRBS\Form\FieldSelect;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
|
||||
function generate_room_delete_form(int $room, int $area) : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('action' => multisite('del.php'));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('type' => 'room',
|
||||
'area' => $area,
|
||||
'room' => $room);
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// The button
|
||||
$element = new ElementInputImage();
|
||||
$element->setAttributes(array('class' => 'button',
|
||||
'src' => 'images/delete.png',
|
||||
'width' => '16',
|
||||
'height' => '16',
|
||||
'title' => get_vocab('delete'),
|
||||
'alt' => get_vocab('delete')));
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_area_change_form(array $enabled_areas, array $disabled_areas) : void
|
||||
{
|
||||
global $area, $day, $month, $year;
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('class' => 'areaChangeForm',
|
||||
'action' => multisite(this_page()));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden inputs for page day, month, year
|
||||
$hidden_inputs = array('day' => $day,
|
||||
'month' => $month,
|
||||
'year' => $year);
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// Now the visible fields
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend('');
|
||||
|
||||
// The area select
|
||||
if (is_admin())
|
||||
{
|
||||
$options = array(get_vocab("enabled") => $enabled_areas,
|
||||
get_vocab("disabled") => $disabled_areas);
|
||||
}
|
||||
else
|
||||
{
|
||||
$options = $enabled_areas;
|
||||
}
|
||||
|
||||
$field = new FieldSelect();
|
||||
$field->setLabel(get_vocab('area'))
|
||||
->setControlAttributes(array('id' => 'area_select',
|
||||
'name' => 'area',
|
||||
'class' => 'room_area_select',
|
||||
'onchange' => 'this.form.submit()'))
|
||||
->addSelectOptions($options, $area, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The change area button (won't be needed or displayed if JavaScript is enabled)
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setAttribute('class', 'js_none')
|
||||
->setControlAttributes(array('value' => get_vocab('change'),
|
||||
'name' => 'change'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// If they're an admin then give them edit and delete buttons for the area
|
||||
if (is_admin())
|
||||
{
|
||||
$img = new ElementImg();
|
||||
$img->setAttributes(array('src' => 'images/edit.png',
|
||||
'alt' => get_vocab('edit')));
|
||||
$button = new ElementButton();
|
||||
$button->setAttributes(array('class' => 'image',
|
||||
'title' => get_vocab('edit'),
|
||||
'formaction' => multisite('edit_area.php')))
|
||||
->addElement($img);
|
||||
$fieldset->addElement($button);
|
||||
|
||||
$img = new ElementImg();
|
||||
$img->setAttributes(array('src' => 'images/delete.png',
|
||||
'alt' => get_vocab('delete')));
|
||||
$button = new ElementButton();
|
||||
$button->setAttributes(array('class' => 'image',
|
||||
'title' => get_vocab('delete'),
|
||||
'formaction' => multisite('del.php?type=area')))
|
||||
->addElement($img);
|
||||
$fieldset->addElement($button);
|
||||
}
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_new_area_form() : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('id' => 'add_area',
|
||||
'class' => 'form_admin standard',
|
||||
'action' => multisite('add.php'));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden field for the type of operation
|
||||
$form->addHiddenInput('type', 'area');
|
||||
|
||||
// Now the visible fields
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('addarea'));
|
||||
|
||||
// The name field
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('name'))
|
||||
->setControlAttributes(array('id' => 'area_name',
|
||||
'name' => 'name',
|
||||
'required' => true,
|
||||
'maxlength' => maxlength('area.area_name')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The submit button
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('addarea'),
|
||||
'class' => 'submit'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_new_room_form() : void
|
||||
{
|
||||
global $area;
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('id' => 'add_room',
|
||||
'class' => 'form_admin standard',
|
||||
'action' => multisite('add.php'));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('type' => 'room',
|
||||
'area' => $area);
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// Visible fields
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('addroom'));
|
||||
|
||||
// The name field
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('name'))
|
||||
->setControlAttributes(array('id' => 'room_name',
|
||||
'name' => 'name',
|
||||
'required' => true,
|
||||
'maxlength' => maxlength('room.room_name')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The description field
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('description'))
|
||||
->setControlAttributes(array('id' => 'room_description',
|
||||
'name' => 'description',
|
||||
'maxlength' => maxlength('room.description')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Capacity
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('capacity'))
|
||||
->setControlAttributes(array('name' => 'capacity',
|
||||
'min' => '0'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The email field
|
||||
$field = new FieldInputEmail();
|
||||
$field->setLabel(get_vocab('room_admin_email'))
|
||||
->setLabelAttribute('title', get_vocab('email_list_note'))
|
||||
->setControlAttributes(array('id' => 'room_admin_email',
|
||||
'name' => 'room_admin_email',
|
||||
'multiple' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The submit button
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('addroom'),
|
||||
'class' => 'submit'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
// Check the CSRF token.
|
||||
// Only check the token if the page is accessed via a POST request. Therefore
|
||||
// this page should not take any action, but only display data.
|
||||
Form::checkToken(true);
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Get non-standard form variables
|
||||
$error = get_form_var('error', 'string');
|
||||
|
||||
// If we haven't got an area id (because the default area normally has to be enabled), then just get the first
|
||||
// area of any kind at all.
|
||||
if (empty($area))
|
||||
{
|
||||
$area_ids = array_keys(get_area_names(true));
|
||||
if (count($area_ids) > 0)
|
||||
{
|
||||
$area = $area_ids[0];
|
||||
}
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area ?? null,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Get the details we need for this area
|
||||
if (isset($area))
|
||||
{
|
||||
$sql = "SELECT area_name, custom_html
|
||||
FROM " . _tbl('area') . "
|
||||
WHERE id=?
|
||||
LIMIT 1";
|
||||
|
||||
$res = db()->query($sql, array($area));
|
||||
|
||||
if ($res->count() == 1)
|
||||
{
|
||||
$row = $res->next_row_keyed();
|
||||
$area_name = $row['area_name'];
|
||||
$custom_html = $row['custom_html'];
|
||||
}
|
||||
}
|
||||
|
||||
// Add in the link for editing the message
|
||||
if (is_book_admin())
|
||||
{
|
||||
echo "<h2>" . get_vocab("message") . "</h2>\n";
|
||||
// Display the message, if any
|
||||
$message = Message::getInstance();
|
||||
$message->load();
|
||||
if ($message->getText() !== '')
|
||||
{
|
||||
$from_string = $message->getFromLocalString();
|
||||
$until_string = $message->getUntilLocalString();
|
||||
if (empty($from_string))
|
||||
{
|
||||
$text = (empty($until_string)) ? get_vocab("this_message") : get_vocab("this_message_until", $until_string);
|
||||
}
|
||||
else
|
||||
{
|
||||
$text = (empty($until_string)) ? get_vocab("this_message_from", $from_string) : get_vocab("this_message_from_until", $from_string, $until_string);
|
||||
}
|
||||
echo '<p>' . escape_html($text) . "</p>\n";
|
||||
echo '<p class="message_top">' . $message->getEscapedText() . "</p>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
echo '<p>' . escape_html(get_vocab("no_message")) . "</p>\n";
|
||||
}
|
||||
// Add an edit button
|
||||
$url = 'edit_message.php?' . http_build_query($context, '', '&');
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
$form->setAttributes(array(
|
||||
'id' => 'edit_message',
|
||||
'action' => multisite($url)
|
||||
)
|
||||
);
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttribute('value', get_vocab('edit_message'));
|
||||
$form->addElement($submit);
|
||||
$form->render();
|
||||
}
|
||||
|
||||
echo "<h2>" . get_vocab("administration") . "</h2>\n";
|
||||
if (!empty($error))
|
||||
{
|
||||
echo "<p class=\"error\">" . escape_html(get_vocab($error)) . "</p>\n";
|
||||
}
|
||||
|
||||
// TOP SECTION: THE FORM FOR SELECTING AN AREA
|
||||
echo "<div id=\"area_form\">\n";
|
||||
|
||||
$sql = "SELECT id, area_name, disabled
|
||||
FROM " . _tbl('area') . "
|
||||
ORDER BY disabled, sort_key";
|
||||
$res = db()->query($sql);
|
||||
|
||||
$enabled_areas = array();
|
||||
$disabled_areas = array();
|
||||
|
||||
while (false !== ($row = $res->next_row_keyed()))
|
||||
{
|
||||
if ($row['disabled'])
|
||||
{
|
||||
$disabled_areas[$row['id']] = $row['area_name'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$enabled_areas[$row['id']] = $row['area_name'];
|
||||
}
|
||||
}
|
||||
|
||||
$areas_defined = !empty($enabled_areas) || !empty($disabled_areas);
|
||||
|
||||
if (!$areas_defined)
|
||||
{
|
||||
echo "<p>" . get_vocab("noareas") . "</p>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!is_admin() && empty($enabled_areas))
|
||||
{
|
||||
echo "<p>" . get_vocab("noareas_enabled") . "</p>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// If there are some areas to display, then show the area form
|
||||
generate_area_change_form($enabled_areas, $disabled_areas);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_admin())
|
||||
{
|
||||
// New area form
|
||||
generate_new_area_form();
|
||||
}
|
||||
echo "</div>"; // area_form
|
||||
|
||||
|
||||
// Now the custom HTML
|
||||
if ($auth['allow_custom_html'])
|
||||
{
|
||||
echo "<div id=\"div_custom_html\">\n";
|
||||
// no escape_html() because we want the HTML!
|
||||
echo (isset($custom_html)) ? "$custom_html\n" : "";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
// BOTTOM SECTION: ROOMS IN THE SELECTED AREA
|
||||
// Only display the bottom section if the user is an admin or
|
||||
// else if there are some areas that can be displayed
|
||||
if (is_admin() || !empty($enabled_areas))
|
||||
{
|
||||
echo "<h2>\n";
|
||||
echo get_vocab("rooms");
|
||||
if(isset($area_name))
|
||||
{
|
||||
echo " " . get_vocab("in") . " " . escape_html($area_name);
|
||||
}
|
||||
echo "</h2>\n";
|
||||
|
||||
echo "<div id=\"room_form\">\n";
|
||||
if (isset($area))
|
||||
{
|
||||
$rooms = get_rooms($area, true);
|
||||
|
||||
if (count($rooms) == 0)
|
||||
{
|
||||
echo "<p>" . get_vocab("norooms") . "</p>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the information about the fields in the room table
|
||||
$fields = db()->field_info(_tbl('room'));
|
||||
|
||||
// See if there are going to be any rooms to display (in other words rooms if
|
||||
// you are not an admin whether any rooms are enabled)
|
||||
$n_displayable_rooms = 0;
|
||||
foreach ($rooms as $r)
|
||||
{
|
||||
if (is_admin() || !$r['disabled'])
|
||||
{
|
||||
$n_displayable_rooms++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($n_displayable_rooms == 0)
|
||||
{
|
||||
echo "<p>" . get_vocab("norooms_enabled") . "</p>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
echo "<div id=\"room_info\" class=\"datatable_container\">\n";
|
||||
// Build the table. We deal with the name and disabled columns
|
||||
// first because they are not necessarily the first two columns in
|
||||
// the table (eg if you are running PostgreSQL and have upgraded your
|
||||
// database)
|
||||
echo "<table id=\"rooms_table\" class=\"admin_table display\">\n";
|
||||
|
||||
// The header
|
||||
echo "<thead>\n";
|
||||
echo "<tr>\n";
|
||||
|
||||
echo '<th><span class="normal" data-type="string">' . get_vocab("name") . "</span></th>\n";
|
||||
if (is_admin())
|
||||
{
|
||||
// Don't show ordinary users the disabled status: they are only going to see enabled rooms
|
||||
echo "<th>" . get_vocab("enabled") . "</th>\n";
|
||||
}
|
||||
// ignore these columns, either because we don't want to display them,
|
||||
// or because we have already displayed them in the header column
|
||||
$ignore = array('id', 'area_id', 'room_name', 'disabled', 'sort_key', 'custom_html');
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $ignore))
|
||||
{
|
||||
switch ($field['name'])
|
||||
{
|
||||
// the standard MRBS fields
|
||||
case 'capacity':
|
||||
case 'description':
|
||||
case 'invalid_types':
|
||||
case 'room_admin_email':
|
||||
$text = get_vocab($field['name']);
|
||||
break;
|
||||
// any user defined fields
|
||||
default:
|
||||
$text = get_loc_field_name(_tbl('room'), $field['name']);
|
||||
break;
|
||||
}
|
||||
// Add a data-type to help JavaScript sort
|
||||
if ($field['nature'] == 'character')
|
||||
{
|
||||
$text = '<span class="normal" data-type="string">' . $text . '</span>';
|
||||
}
|
||||
// We don't use escape_html() here because (a) the column names are
|
||||
// trusted and some of them may deliberately contain HTML entities (eg )
|
||||
// (b) $text could contain the span above.
|
||||
echo "<th>$text</th>\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (is_admin())
|
||||
{
|
||||
echo "<th> </th>\n";
|
||||
}
|
||||
|
||||
echo "</tr>\n";
|
||||
echo "</thead>\n";
|
||||
|
||||
// The body
|
||||
echo "<tbody>\n";
|
||||
$row_class = "odd";
|
||||
foreach ($rooms as $r)
|
||||
{
|
||||
// Don't show ordinary users disabled rooms
|
||||
if (is_admin() || !$r['disabled'])
|
||||
{
|
||||
$row_class = ($row_class == "even") ? "odd" : "even";
|
||||
echo "<tr class=\"$row_class\">\n";
|
||||
|
||||
$html_name = escape_html($r['room_name']);
|
||||
$href = multisite('edit_room.php?room=' . $r['id']);
|
||||
// We insert a data attribute containing the sort key so that the rooms will
|
||||
// be sorted properly
|
||||
echo '<td data-order="' . escape_html($r['sort_key']) . '"><div>' .
|
||||
"<a title=\"$html_name\" href=\"" . escape_html($href) . "\">$html_name</a>" .
|
||||
"</div></td>\n";
|
||||
if (is_admin())
|
||||
{
|
||||
// Don't show ordinary users the disabled status: they are only going to see enabled rooms
|
||||
echo "<td class=\"boolean\"><div>" . ((!$r['disabled']) ? "<img src=\"images/check.png\" alt=\"check mark\" width=\"16\" height=\"16\">" : " ") . "</div></td>\n";
|
||||
}
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $ignore))
|
||||
{
|
||||
switch ($field['name'])
|
||||
{
|
||||
// the standard MRBS fields
|
||||
case 'description':
|
||||
case 'room_admin_email':
|
||||
echo "<td><div>" . escape_html($r[$field['name']] ?? '') . "</div></td>\n";
|
||||
break;
|
||||
case 'capacity':
|
||||
$value = $r[$field['name']] ?? '';
|
||||
echo "<td class=\"int\"><div>" . escape_html($value) . "</div></td>\n";
|
||||
break;
|
||||
case 'invalid_types':
|
||||
echo "<td><div>" . get_type_names($r[$field['name']]) . "</div></td>\n";
|
||||
break;
|
||||
// any user defined fields
|
||||
default:
|
||||
if (($field['nature'] == 'boolean') ||
|
||||
(($field['nature'] == 'integer') && isset($field['length']) && ($field['length'] <= 2)) )
|
||||
{
|
||||
// booleans: represent by a checkmark
|
||||
echo "<td class=\"boolean\"><div>";
|
||||
echo (!empty($r[$field['name']])) ? "<img src=\"images/check.png\" alt=\"check mark\" width=\"16\" height=\"16\">" : " ";
|
||||
echo "</div></td>\n";
|
||||
}
|
||||
elseif (($field['nature'] == 'integer') && isset($field['length']) && ($field['length'] > 2))
|
||||
{
|
||||
// integer values
|
||||
$value = $r[$field['name']] ?? '';
|
||||
echo "<td class=\"int\"><div>" . escape_html($value) . "</div></td>\n";
|
||||
}
|
||||
elseif ($field['nature'] == 'real')
|
||||
{
|
||||
// floats
|
||||
// TODO: check whether these sort properly and, if not, the best way to do so
|
||||
$value = $r[$field['name']] ?? '';
|
||||
echo "<td><div>" . escape_html($value) . "</div></td>\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// strings
|
||||
$value = $r[$field['name']] ?? '';
|
||||
$html = "<td title=\"" . escape_html($value) . "\"><div>";
|
||||
// Truncate before conversion, otherwise you could chop off in the middle of an entity
|
||||
$html .= escape_html(mb_substr($value, 0, $max_content_length));
|
||||
$html .= (mb_strlen($value) > $max_content_length) ? '…' : '';
|
||||
$html .= "</div></td>\n";
|
||||
echo $html;
|
||||
}
|
||||
break;
|
||||
} // switch
|
||||
} // if
|
||||
} // foreach
|
||||
|
||||
// Give admins a delete button
|
||||
if (is_admin())
|
||||
{
|
||||
echo "<td>\n<div>\n";
|
||||
generate_room_delete_form($r['id'], $area);
|
||||
|
||||
|
||||
echo "</div>\n</td>\n";
|
||||
}
|
||||
|
||||
echo "</tr>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</tbody>\n";
|
||||
echo "</table>\n";
|
||||
echo "</div>\n";
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
echo get_vocab("noarea");
|
||||
}
|
||||
|
||||
// Give admins a form for adding rooms to the area - provided
|
||||
// there's an area selected
|
||||
if (is_admin() && $areas_defined && !empty($area))
|
||||
{
|
||||
generate_new_room_form();
|
||||
}
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
// An Ajax function to check which of an array of time slots is invalid. (We need to do
|
||||
// this server side because the client does not have sophisticated enough timezone
|
||||
// handling facilities)
|
||||
//
|
||||
// Input parameters:
|
||||
// $id the request id so that the client can match results to requests
|
||||
// $slots an array of slot times in seconds from the start of the calendar day
|
||||
// $day
|
||||
// $month
|
||||
// $year
|
||||
// $tz
|
||||
//
|
||||
// Returns an array of slots which are invalid
|
||||
|
||||
require '../defaultincludes.inc';
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Get the non-standard form variables ($day, $month and $year are standard)
|
||||
$id = get_form_var('id', 'string');
|
||||
$slots = get_form_var('slots', 'array');
|
||||
$tz = get_form_var('tz', 'string');
|
||||
|
||||
$result = array('id' => $id, 'slots' => array());
|
||||
|
||||
foreach ($slots as $s)
|
||||
{
|
||||
if (is_invalid_datetime(0, 0, $s, $month, $day, $year, $tz))
|
||||
{
|
||||
$result['slots'][] = $s;
|
||||
}
|
||||
}
|
||||
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
|
||||
echo json_encode($result);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
// A page designed to be used in Ajax POST calls for bulk deletion of entries.
|
||||
// It takes an array of ids to be deleted as input. These are always assumed
|
||||
// to be single entries. Returns the number of entries deleted, or some
|
||||
// kind of string on failure (most likely a login page).
|
||||
//
|
||||
// If deleting lots of entries you may need to split the Ajax requests into
|
||||
// multiple smaller requests in order to avoid exceeding the system limit
|
||||
// for POST requests, and also the limit on the size of the SQL query once
|
||||
// the ids are imploded.
|
||||
//
|
||||
// Note that:
|
||||
// (1) the code assumes that you are an admin with powers to delete anything.
|
||||
// It checks that you are an admin and so does not bother checking that
|
||||
// you have rights in that particular area or room, nor does it check that
|
||||
// the proposed deletion conforms to any policy in force.
|
||||
// (2) email notifications are not sent, even if they are normally configured
|
||||
// to be sent. Sending many thousands of emails in the space of a few
|
||||
// seconds could overwhelm many mail servers, or break the usage policies
|
||||
// on hosted systems.
|
||||
|
||||
require '../defaultincludes.inc';
|
||||
require_once '../mrbs_sql.inc';
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Check that the user is a booking admin
|
||||
if (!is_book_admin())
|
||||
{
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get non-standard form variables
|
||||
$ids = get_form_var('ids', 'string', '[]', INPUT_POST);
|
||||
// The ids are JSON encoded to avoid hitting the php.ini max_input_vars limit
|
||||
$ids = json_decode($ids);
|
||||
|
||||
// Check that $ids consists of an array of integers, to guard against SQL injection
|
||||
foreach ($ids as $id)
|
||||
{
|
||||
if (!is_numeric($id) || (intval($id) != $id) || ($id < 0))
|
||||
{
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Everything looks OK - go ahead and delete the entries
|
||||
|
||||
// Note on performance. It is much quicker to delete entries using the
|
||||
// WHERE id IN method below than looping through mrbsDelEntry(). Testing
|
||||
// for 100 entries gave 2.5ms for the IN method against 37.6s for the looping
|
||||
// method - ie approx 15 times faster. For 1,000 rows the IN method was 19
|
||||
// times faster.
|
||||
//
|
||||
// Because we are not using mrbsDelEntry() we have to delete any orphaned
|
||||
// rows in the repeat table ourselves - but this does not take long.
|
||||
|
||||
$sql = "DELETE FROM " . _tbl('entry') . "
|
||||
WHERE id IN (" . implode(',', $ids) . ")";
|
||||
$result = db()->command($sql);
|
||||
|
||||
// And delete any orphaned rows in the repeat table
|
||||
$sql = "DELETE FROM " . _tbl('repeat') . "
|
||||
WHERE id NOT IN (SELECT repeat_id FROM " . _tbl('entry') . ")";
|
||||
$orphan_result = db()->command($sql);
|
||||
|
||||
|
||||
echo $result;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// An Ajax function to record user activity on the client side. (If there is some activity then
|
||||
// this will be picked up and used by the appropriate session file).
|
||||
|
||||
require '../defaultincludes.inc';
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// An Ajax page to update the current page in the server. Called by the client when it switches URL
|
||||
// on the fly.
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require '../defaultincludes.inc';
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
$page = get_form_var('page', 'string');
|
||||
|
||||
if (isset($page) && ($page !== ''))
|
||||
{
|
||||
session()->updatePage($page);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// Returns an object containing all the usernames available for use by the Select2
|
||||
// tool on the edit_entry page.
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require '../defaultincludes.inc';
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Check that the user has a legitimate reason for accessing this page
|
||||
if (!can_register_others() && !is_book_admin())
|
||||
{
|
||||
exit;
|
||||
}
|
||||
|
||||
$result = array();
|
||||
|
||||
if (method_exists(auth(), 'getUsernames'))
|
||||
{
|
||||
try
|
||||
{
|
||||
$result = auth()->getUsernames();
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
trigger_error($e->getMessage(), E_USER_WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
|
||||
echo json_encode($result);
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
// Handles actions on bookings awaiting approval
|
||||
|
||||
require "defaultincludes.inc";
|
||||
require_once "mrbs_sql.inc";
|
||||
require_once "functions_mail.inc";
|
||||
|
||||
// Get non-standard form variables
|
||||
$action = get_form_var('action', 'string');
|
||||
$id = get_form_var('id', 'int');
|
||||
$series = get_form_var('series', 'bool');
|
||||
$returl = get_form_var('returl', 'url_local', 'index.php');
|
||||
$note = get_form_var('note', 'string');
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$mrbs_username = (isset($mrbs_user)) ? $mrbs_user->username : null;
|
||||
|
||||
// Retrieve the booking details
|
||||
$data = get_booking_info($id, $series);
|
||||
$room_id = $data['room_id'];
|
||||
|
||||
// Initialise $mail_previous so that we can use it as a parameter for notifyAdminOnBooking
|
||||
$mail_previous = array();
|
||||
$start_times = array();
|
||||
|
||||
// Give the return URL a query string if it doesn't already have one
|
||||
if (mb_strpos($returl, '?') === false)
|
||||
{
|
||||
$returl .= "?year=$year&month=$month&day=$day&area=$area&room=$room";
|
||||
}
|
||||
|
||||
|
||||
if (isset($action))
|
||||
{
|
||||
if (need_to_send_mail())
|
||||
{
|
||||
$is_new_entry = TRUE; // Treat it as a new entry unless told otherwise
|
||||
}
|
||||
|
||||
// If we have to approve or reject a booking, check that we have rights to do so
|
||||
// for this room
|
||||
if ((($action == "approve") || ($action == "reject"))
|
||||
&& !is_book_admin($room_id))
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, isset($room) ? $room : null);
|
||||
exit;
|
||||
}
|
||||
|
||||
switch ($action)
|
||||
{
|
||||
// ACTION = "APPROVE"
|
||||
case 'approve':
|
||||
if (need_to_send_mail())
|
||||
{
|
||||
$is_new_entry = FALSE;
|
||||
// Get the current booking data, before we change anything, for use in emails
|
||||
$mail_previous = get_booking_info($id, $series);
|
||||
}
|
||||
$start_times = mrbsApproveEntry($id, $series);
|
||||
$result = ($start_times !== FALSE);
|
||||
if ($result === FALSE)
|
||||
{
|
||||
$returl .= "&error=approve_failed";
|
||||
}
|
||||
// Get the new data, which will have the status changed
|
||||
$data = get_booking_info($id, $series);
|
||||
break;
|
||||
|
||||
|
||||
// ACTION = "MORE_INFO"
|
||||
case 'more_info':
|
||||
// update the last reminded time (the ball is back in the
|
||||
// originator's court, so the clock gets reset)
|
||||
update_last_reminded($id, $series);
|
||||
// update the more info field
|
||||
update_more_info($id, $series, $mrbs_user->username, $note);
|
||||
$result = TRUE; // We'll assume success and end an email anyway
|
||||
break;
|
||||
|
||||
|
||||
// ACTION = "REMIND"
|
||||
case 'remind':
|
||||
// update the last reminded time
|
||||
update_last_reminded($id, $series);
|
||||
$result = TRUE; // We'll assume success and end an email anyway
|
||||
break;
|
||||
|
||||
default:
|
||||
$result = FALSE; // should not get here
|
||||
break;
|
||||
|
||||
} // switch ($action)
|
||||
|
||||
|
||||
|
||||
// Now send an email if required and the operation was successful
|
||||
if ($result && need_to_send_mail())
|
||||
{
|
||||
// Get the area settings for this area (we will need to know if periods are enabled
|
||||
// so that we will know whether to include iCalendar information in the email)
|
||||
get_area_settings($data['area_id']);
|
||||
// Send the email
|
||||
notify_by_email($data, $mail_previous, $series, $action, $start_times, $note);
|
||||
}
|
||||
}
|
||||
|
||||
// Now it's all done go back to the previous view
|
||||
location_header($returl);
|
||||
@@ -0,0 +1,333 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS area defaults file (default settings for NEW areas)
|
||||
*
|
||||
* DO _NOT_ MODIFY THIS FILE YOURSELF. IT IS FOR _INTERNAL_ USE ONLY.
|
||||
*
|
||||
* TO CONFIGURE MRBS FOR YOUR SYSTEM ADD CONFIGURATION PARAMETERS FROM
|
||||
* THIS FILE INTO config.inc.php, DO _NOT_ EDIT THIS FILE.
|
||||
*
|
||||
* This file contains the default settings for configuration parameters that
|
||||
* can be set on a per-area basis by using a web browser and following the
|
||||
* "Rooms" link in MRBS. The settings in this file just determine the
|
||||
* default values that are used when new areas are created. They are kept
|
||||
* in a separate file to system defaults to draw attention to the fact that
|
||||
* they are merely the default settings for new areas: it can be a little
|
||||
* frustrating sometimes to edit these values and find they have no effect
|
||||
* on existing areas
|
||||
**************************************************************************/
|
||||
|
||||
|
||||
|
||||
/*******************
|
||||
* Calendar settings
|
||||
*******************/
|
||||
|
||||
// This setting controls whether to use "clock" or "times" based intervals
|
||||
// (FALSE and the default) or user defined periods (TRUE).
|
||||
|
||||
// $enable_periods is settable on a per-area basis.
|
||||
|
||||
$enable_periods = FALSE; // Default value for new areas
|
||||
|
||||
|
||||
// TIMES SETTINGS
|
||||
// --------------
|
||||
|
||||
// These settings are all set per area through MRBS. These are the default
|
||||
// settings that are used when a new area is created.
|
||||
|
||||
// The "Times" settings are ignored if $enable_periods is TRUE.
|
||||
|
||||
// Note: Be careful to avoid specifying options that display blocks overlapping
|
||||
// the next day, since it is not properly handled.
|
||||
|
||||
// Resolution - what blocks can be booked, in seconds.
|
||||
// Default is half an hour: 1800 seconds.
|
||||
$resolution = (30 * 60); // DEFAULT VALUE FOR NEW AREAS
|
||||
|
||||
// If the following variable is set to TRUE, the resolution of bookings
|
||||
// is forced to be the value of $resolution, rather than the resolution set
|
||||
// for the area in the database.
|
||||
$force_resolution = FALSE;
|
||||
|
||||
// Default duration - default length (in seconds) of a booking.
|
||||
// Defaults to (60 * 60) seconds, i.e. an hour
|
||||
$default_duration = (60 * 60); // DEFAULT VALUE FOR NEW AREAS
|
||||
// Whether the "All Day" checkbox should be checked by default. (Note
|
||||
// that even if this is set to true, $default_duration should still
|
||||
// be set as that is the duration that will be used when the All Day
|
||||
// checkbox is unchecked)
|
||||
$default_duration_all_day = FALSE; // DEFAULT VALUE FOR NEW AREAS
|
||||
|
||||
// Start and end of day.
|
||||
// NOTE: The time between the beginning of the last and first
|
||||
// slots of the day must be an integral multiple of the resolution.
|
||||
// If the last slot is before the first slot, then the booking day is
|
||||
// assumed to span midnight and the last slot is on the day after the
|
||||
// first slot.
|
||||
|
||||
|
||||
// The default settings below (along with the 30 minute resolution above)
|
||||
// give you 24 half-hourly slots starting at 07:00, with the last slot
|
||||
// being 18:30 -> 19:00
|
||||
|
||||
// The beginning of the first slot of the day (DEFAULT VALUES FOR NEW AREAS)
|
||||
$morningstarts = 7; // must be integer in range 0-23
|
||||
$morningstarts_minutes = 0; // must be integer in range 0-59
|
||||
|
||||
// The beginning of the last slot of the day (DEFAULT VALUES FOR NEW AREAS)
|
||||
$eveningends = 18; // must be integer in range 0-23
|
||||
$eveningends_minutes = 30; // must be integer in range 0-59
|
||||
|
||||
// Example 1.
|
||||
// If resolution=3600 (1 hour), morningstarts = 8 and morningstarts_minutes = 30
|
||||
// then for the last period to start at say 4:30pm you would need to set eveningends = 16
|
||||
// and eveningends_minutes = 30
|
||||
|
||||
// Example 2.
|
||||
// To get a full 24-hour display with 15-minute steps, set morningstarts=0; eveningends=23;
|
||||
// eveningends_minutes=45; and resolution=900.
|
||||
//
|
||||
// Example 3.
|
||||
// To get a booking day running from 6.00 pm to 2.00 am with 30 minute steps, set
|
||||
// morningstarts=18, morningstarts_minutes = 0, eveningends = 1, eveningends_minutes = 30
|
||||
// and resolution = 1800.
|
||||
|
||||
|
||||
// PERIODS SETTINGS
|
||||
// ----------------
|
||||
|
||||
// The "Periods" settings are used only in areas where the mode has
|
||||
// been set to "Periods".
|
||||
|
||||
// Define the name or description for your periods in chronological order
|
||||
// For example:
|
||||
// $periods[] = "Period 1"
|
||||
// $periods[] = "Period 2"
|
||||
// ...
|
||||
// or
|
||||
// $periods[] = "09:15 - 09:50"
|
||||
// $periods[] = "09:55 - 10:35"
|
||||
// ...
|
||||
|
||||
// Period names are encoded in UTF-8
|
||||
|
||||
// NOTE: The maximum number of periods is 60. Do not define more than this.
|
||||
unset($periods); // Include this line when copying to config.inc.php
|
||||
$periods[] = "Period 1";
|
||||
$periods[] = "Period 2";
|
||||
|
||||
// Periods can also be defined as an associative array with the key being the period name and
|
||||
// the value being an array with two elements, the start and end times for that period in
|
||||
// 'hh:mm-hh:mm' format. For example:
|
||||
//
|
||||
// $periods = [
|
||||
// 'Period 1' => ['09:15', '09:50'],
|
||||
// 'Period 2' => ['09:55', '10:35']
|
||||
// ];
|
||||
//
|
||||
// This can be useful when MRBS needs to know when in the day a period stats and ends, for example
|
||||
// when converting a booking into an iCalendar event.
|
||||
|
||||
// NOTE: The maximum number of periods is 60. Do not define more than this.
|
||||
|
||||
// NOTE: See INSTALL for information on how to add or remove periods in an
|
||||
// existing database.
|
||||
|
||||
|
||||
/******************
|
||||
* Booking policies
|
||||
******************/
|
||||
|
||||
// It is possible to set policies that restrict how far in advance ordinary users can make
|
||||
// bookings. Both minimum and maximum values can be set. It is also possible to distinguish
|
||||
// between creating new bookings and deleting existing bookings. Editing an existing booking
|
||||
// involves deleting the existing booking and creating a new booking at the (possibly) new time.
|
||||
// So if for example you want to stop people editing existing bookings, but allow the creation
|
||||
// of new bookings, then you will need to prevent deletion but allow creation.
|
||||
|
||||
// If the variables below are set to TRUE, MRBS will force a minimum and/or maximum advance
|
||||
// booking time on ordinary users (admins can make bookings for whenever they like). The
|
||||
// minimum advance booking time allows you to set a policy saying that users must book
|
||||
// at least so far in advance. The maximum allows you to set a policy saying that they cannot
|
||||
// book more than so far in advance. How the times are determined depends on whether Periods
|
||||
// or Times are being used.
|
||||
|
||||
// DEFAULT VALUES FOR NEW AREAS
|
||||
|
||||
// Creating new bookings
|
||||
$min_create_ahead_enabled = FALSE; // set to TRUE to enforce a minimum advance booking time
|
||||
$max_create_ahead_enabled = FALSE; // set to TRUE to enforce a maximum advance booking time
|
||||
|
||||
// Deleting existing bookings
|
||||
$min_delete_ahead_enabled = FALSE; // set to TRUE to enforce a minimum advance booking time
|
||||
$max_delete_ahead_enabled = FALSE; // set to TRUE to enforce a maximum advance booking time
|
||||
|
||||
// The advance booking limits are measured in seconds and are set by the two variables below.
|
||||
// The relevant time for determining whether a booking is allowed is the start time of the
|
||||
// booking. Values may be negative: for example setting $min_delete_ahead_secs = -300 means
|
||||
// that users cannot delete (and this will include editing) a booking more than 5 minutes in
|
||||
// the past.
|
||||
|
||||
|
||||
// DEFAULT VALUES FOR NEW AREAS
|
||||
|
||||
// Creating new bookings
|
||||
$min_create_ahead_secs = 0; // (seconds) cannot book in the past
|
||||
$max_create_ahead_secs = 60*60*24*7; // (seconds) no more than one week ahead
|
||||
|
||||
// Deleting existing bookings
|
||||
$min_delete_ahead_secs = 0; // (seconds) cannot book in the past
|
||||
$max_delete_ahead_secs = 60*60*24*7; // (seconds) no more than one week ahead
|
||||
|
||||
// NOTE: If you are using periods, MRBS has no notion of when the periods occur during the
|
||||
// day, and so cannot impose policies of the kind "users must book at least one period
|
||||
// in advance". However it can impose policies such as "users must book at least
|
||||
// one day in advance". The two values above are rounded down to the nearest whole
|
||||
// number of days when using periods. For example 86401 will be rounded down to 86400
|
||||
// (one day) and 1 will be rounded down to 0.
|
||||
//
|
||||
// As MRBS does not know when the periods occur in the day, there is no way of specifying, for example,
|
||||
// that bookings must be made at least 24 hours in advance. Setting $min_create_ahead_secs=86400
|
||||
// will allow somebody to make a booking at 11:59 pm for the first period the next day, which
|
||||
// may occur at 8.00 am.
|
||||
|
||||
|
||||
// Set a maximum duration for bookings
|
||||
$max_duration_enabled = FALSE; // Set to TRUE if you want to enforce a maximum duration
|
||||
$max_duration_secs = 60*60*2; // (seconds) - when using "times"
|
||||
$max_duration_periods = 2; // (periods) - when using "periods"
|
||||
|
||||
|
||||
|
||||
// DEFAULT VALUES FOR NEW AREAS
|
||||
// Set the maximum number of bookings that can be made in an area by any one user, in an
|
||||
// interval, which can be a day, week, month or year, or else in the future. (A week is
|
||||
// defined by the $weekstarts setting). These are per-area settings but you can use them
|
||||
// in conjunction with the global settings. This would allow you to set policies such as
|
||||
// allowing a maximum of 10 bookings per month in total with a maximum of 1 per day in Area A.
|
||||
$max_per_interval_area_enabled['day'] = FALSE;
|
||||
$max_per_interval_area['day'] = 1; // max 1 bookings per day in an area
|
||||
|
||||
$max_per_interval_area_enabled['week'] = FALSE;
|
||||
$max_per_interval_area['week'] = 5; // max 5 bookings per week in an area
|
||||
|
||||
$max_per_interval_area_enabled['month'] = FALSE;
|
||||
$max_per_interval_area['month'] = 10; // max 10 bookings per month in an area
|
||||
|
||||
$max_per_interval_area_enabled['year'] = FALSE;
|
||||
$max_per_interval_area['year'] = 50; // max 50 bookings per year in an area
|
||||
|
||||
$max_per_interval_area_enabled['future'] = FALSE;
|
||||
$max_per_interval_area['future'] = 100; // max 100 bookings in the future in an area
|
||||
|
||||
// Set the maximum total *length* of bookings that can be made by any one user, in an interval,
|
||||
// which can be a day, week, month or year, or else in the future. (A week is defined
|
||||
// by the $weekstarts setting). These are per-area settings but you can use them
|
||||
// in conjunction with the global settings. This would allow you to set policies such as
|
||||
// allowing a maximum of 10 hours per week in total with a maximum of 1 hour per day in Area A.
|
||||
// These settings only apply to areas in "times" mode.
|
||||
|
||||
$max_secs_per_interval_area_enabled['day'] = false;
|
||||
$max_secs_per_interval_area['day'] = 60*60*2; // max 2 hours per day in total
|
||||
|
||||
$max_secs_per_interval_area_enabled['week'] = false;
|
||||
$max_secs_per_interval_area['week'] = 60*60*10; // max 10 hours per week in total
|
||||
|
||||
$max_secs_per_interval_area_enabled['month'] = false;
|
||||
$max_secs_per_interval_area['month'] = 60*60*25; // max 25 hours per month in total
|
||||
|
||||
$max_secs_per_interval_area_enabled['year'] = false;
|
||||
$max_secs_per_interval_area['year'] = 60*60*100; // max 100 hours per year in total
|
||||
|
||||
$max_secs_per_interval_area_enabled['future'] = false;
|
||||
$max_secs_per_interval_area['future'] = 60*60*100; // max 100 hours in the future in total
|
||||
|
||||
|
||||
/******************
|
||||
* Display settings
|
||||
******************/
|
||||
|
||||
// In the day view, to display times on the x-axis (along the top) and rooms on the y-axis (down
|
||||
// the side set to true; the default/traditional version of MRBS has rooms along the top and
|
||||
// times down the side. Transposing the table can be useful if you have a large number of
|
||||
// rooms and not many time slots.
|
||||
$times_along_top = false;
|
||||
|
||||
|
||||
/************************
|
||||
* Miscellaneous settings
|
||||
************************/
|
||||
|
||||
// PRIVATE BOOKINGS SETTINGS
|
||||
|
||||
// These settings are all set per area through MRBS. These are the default
|
||||
// settings that are used when a new area is created.
|
||||
|
||||
// Only administrators or the person who booked a private event can see
|
||||
// details of the event. Everyone else just sees that the time/period
|
||||
// is booked on the schedule.
|
||||
|
||||
$private_enabled = FALSE; // DEFAULT VALUE FOR NEW AREAS
|
||||
// Display checkbox in entry page to make
|
||||
// the booking private.
|
||||
|
||||
$private_mandatory = FALSE; // DEFAULT VALUE FOR NEW AREAS
|
||||
// If TRUE all new/edited entries will
|
||||
// use the value from $private_default when saved.
|
||||
// If checkbox is displayed it will be disabled.
|
||||
|
||||
$private_default = FALSE; // DEFAULT VALUE FOR NEW AREAS
|
||||
// Set default value for "Private" flag on new/edited entries.
|
||||
// Used if the $private_enabled checkbox is displayed
|
||||
// or if $private_mandatory is set.
|
||||
|
||||
$private_override = "none"; // DEFAULT VALUE FOR NEW AREAS
|
||||
// Override default privacy behavior.
|
||||
// "none" - Private flag on entry is used
|
||||
// "private" - ALL entries are treated as private regardless
|
||||
// of private flag on the entry.
|
||||
// "public" - NO entry is treated as private, regardless of
|
||||
// private flag on the entry.
|
||||
// Overrides $private_default and $private_mandatory
|
||||
// Consider your users' expectations of privacy before
|
||||
// changing to "public" or from "private" to "none"
|
||||
|
||||
|
||||
// SETTINGS FOR APPROVING BOOKINGS - PER-AREA
|
||||
|
||||
// These settings control whether bookings made by ordinary users need to be
|
||||
// approved by an admin. The settings here are the default settings for new
|
||||
// areas. The settings for individual areas can be changed from within MRBS.
|
||||
|
||||
$approval_enabled = FALSE; // Set to TRUE to enable booking approval
|
||||
|
||||
// Set to FALSE if you don't want users to be able to send reminders
|
||||
// to admins when bookings are still awaiting approval.
|
||||
$reminders_enabled = TRUE;
|
||||
|
||||
|
||||
// SETTINGS FOR BOOKING CONFIRMATION
|
||||
|
||||
// Allows bookings to be marked as "tentative", ie not yet 100% certain,
|
||||
// and confirmed later. Useful if you want to reserve a slot but at the same
|
||||
// time let other people know that there's a possibility it may not be needed.
|
||||
$confirmation_enabled = TRUE;
|
||||
|
||||
// The default confirmation status for new bookings. (TRUE: confirmed, FALSE: tentative)
|
||||
// Only used if $confirmation_enabled is TRUE. If $confirmation_enabled is
|
||||
// FALSE, then all new bookings are confirmed automatically.
|
||||
$confirmed_default = TRUE;
|
||||
|
||||
|
||||
/*************
|
||||
* Entry Types
|
||||
*************/
|
||||
|
||||
// Default type for new bookings
|
||||
// (Note that the default type does not apply if the type field is mandatory)
|
||||
$default_type = "I";
|
||||
@@ -0,0 +1,8 @@
|
||||
# 等保整改:审计日志目录禁止 Web 直接访问
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
</IfModule>
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
global $auth;
|
||||
|
||||
define('_JEXEC', 1);
|
||||
|
||||
$joomla_path = realpath(MRBS_ROOT . '/' . $auth['joomla']['rel_path']);
|
||||
|
||||
if ($joomla_path === false)
|
||||
{
|
||||
$message = MRBS_ROOT . '/' . $auth['joomla']['rel_path'] . ' does not exist. Check the setting ' .
|
||||
'of $auth["joomla"]["rel_path"] in your config file.';
|
||||
die($message); // Too early for Errors::fatalError()
|
||||
}
|
||||
|
||||
define('JPATH_BASE', $joomla_path);
|
||||
|
||||
require_once JPATH_BASE . '/includes/defines.php';
|
||||
require_once JPATH_BASE . '/includes/framework.php';
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
global $theme, $auth;
|
||||
|
||||
define('WP_USE_THEMES', false);
|
||||
|
||||
// WordPress unsets the $theme variable so we need to save it and restore it afterwards.
|
||||
$mrbs_theme = $theme;
|
||||
require_once MRBS_ROOT . '/'. $auth['wordpress']['rel_path'] . '/wp-load.php';
|
||||
$theme = $mrbs_theme;
|
||||
@@ -0,0 +1,276 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Audit;
|
||||
use MRBS\Form\Element;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputPassword;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
|
||||
// ===== 等保2.0二级整改:自助修改密码页 =====
|
||||
// - 仅限已登录用户(未登录自动引导到登录页,登录后回到本页)
|
||||
// - 校验当前密码 → 新密码复杂度($pwd_policy)→ 两次一致 → 新旧不同
|
||||
// - 成功后写 password_changed_at(供 90 天有效期计算)并清除强制改密标记
|
||||
// - 所有成功/失败动作写入安全审计日志(Audit)
|
||||
|
||||
|
||||
function generate_change_password_form(?string $error = null, string $target_url = 'index.php') : void
|
||||
{
|
||||
global $pwd_policy;
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
$form->setAttributes(array(
|
||||
'class' => 'standard',
|
||||
'id' => 'change_password',
|
||||
'action' => multisite('change_password.php')
|
||||
));
|
||||
|
||||
$form->addHiddenInputs(array(
|
||||
'action' => 'change_password',
|
||||
'target_url' => $target_url
|
||||
));
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('change_password'));
|
||||
|
||||
// 顶部提示 / 错误消息
|
||||
$field = new FieldDiv();
|
||||
$p = new ElementP();
|
||||
|
||||
if (isset($error))
|
||||
{
|
||||
switch ($error)
|
||||
{
|
||||
case 'old_pwd_invalid':
|
||||
$p->setText(get_vocab('old_pwd_invalid'));
|
||||
break;
|
||||
case 'pwd_not_match':
|
||||
$p->setText(get_vocab('passwords_not_eq'));
|
||||
break;
|
||||
case 'pwd_same':
|
||||
$p->setText(get_vocab('pwd_same_as_old'));
|
||||
break;
|
||||
case 'pwd_invalid':
|
||||
$p->setText(get_vocab('password_invalid'));
|
||||
break;
|
||||
default:
|
||||
$p->setText(get_vocab('unknown_user'));
|
||||
break;
|
||||
}
|
||||
$p->setAttribute('class', 'error');
|
||||
$field->addControlElement($p);
|
||||
|
||||
// 策略不满足时列出具体规则
|
||||
if (($error == 'pwd_invalid') && isset($pwd_policy))
|
||||
{
|
||||
$ul = new Element('ul');
|
||||
$ul->setAttribute('class', 'error');
|
||||
foreach ($pwd_policy as $rule => $value)
|
||||
{
|
||||
if ($value != 0)
|
||||
{
|
||||
$li = new Element('li');
|
||||
$li->setText(get_vocab('policy_' . $rule, $value));
|
||||
$ul->addElement($li);
|
||||
}
|
||||
}
|
||||
$field->addControlElement($ul);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 提示(强制改密或常规自助修改)
|
||||
$p->setText(get_vocab('pwd_expired_msg'));
|
||||
$field->addControlElement($p);
|
||||
}
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// 当前密码
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel(get_vocab('current_password'))
|
||||
->setControlAttributes(array('id' => 'password_old',
|
||||
'name' => 'password_old',
|
||||
'autocomplete' => 'current-password',
|
||||
'required' => true,
|
||||
'autofocus' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// 新密码(输入两次)
|
||||
$labels = array(get_vocab('new_password'), get_vocab('confirm_password'));
|
||||
for ($i = 0; $i < 2; $i++)
|
||||
{
|
||||
$field = new FieldInputPassword();
|
||||
$field->setLabel($labels[$i])
|
||||
->setControlAttributes(array('id' => "password$i",
|
||||
'name' => "password$i",
|
||||
'autocomplete' => 'new-password',
|
||||
'required' => true));
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// 口令策略说明
|
||||
if (isset($pwd_policy))
|
||||
{
|
||||
$field = new FieldDiv();
|
||||
$p = new ElementP();
|
||||
$p->setText(get_vocab('pwd_must_contain'));
|
||||
$field->addControlElement($p);
|
||||
$ul = new Element('ul');
|
||||
$ul->setAttribute('id', 'pwd_policy');
|
||||
foreach ($pwd_policy as $rule => $value)
|
||||
{
|
||||
if ($value != 0)
|
||||
{
|
||||
$li = new Element('li');
|
||||
$li->setText(get_vocab('policy_' . $rule, $value));
|
||||
$ul->addElement($li);
|
||||
}
|
||||
}
|
||||
$field->addControlElement($ul);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
$form->addElement($fieldset);
|
||||
|
||||
// 提交按钮
|
||||
$fieldset = new ElementFieldset();
|
||||
$field = new FieldInputSubmit();
|
||||
$field->setControlAttributes(array('value' => get_vocab('change_password')));
|
||||
$fieldset->addElement($field);
|
||||
$form->addElement($fieldset);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_change_password_success(string $target_url) : void
|
||||
{
|
||||
echo "<h2>" . get_vocab('change_password') . "</h2>\n";
|
||||
echo "<p class=\"notice\">" . get_vocab('password_changed') . "</p>\n";
|
||||
echo '<p><a href="' . htmlspecialchars(multisite($target_url)) . '">' . get_vocab('back') . "</a></p>\n";
|
||||
}
|
||||
|
||||
|
||||
// ===== 主流程 =====
|
||||
|
||||
// 必须是已登录用户
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
if (!isset($mrbs_user))
|
||||
{
|
||||
// 未登录:引导到登录页,登录成功后回到本页
|
||||
session()->authGet(null, 'change_password.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// 跳转目标(仅允许站内相对 URL)
|
||||
$target_url = get_form_var('target_url', 'url_local', null, INPUT_GET);
|
||||
if (!isset($target_url) || ($target_url == ''))
|
||||
{
|
||||
$target_url = 'index.php';
|
||||
}
|
||||
// 防止把改密页自身作为跳转目标(避免循环)
|
||||
if ($target_url == 'change_password.php')
|
||||
{
|
||||
$target_url = 'index.php';
|
||||
}
|
||||
|
||||
// 处理提交(action 只从 POST 读取,且必须通过 CSRF 校验)
|
||||
$action = get_form_var('action', 'string', null, INPUT_POST);
|
||||
|
||||
if (isset($action) && ($action == 'change_password'))
|
||||
{
|
||||
Form::checkToken();
|
||||
|
||||
$old_password = get_form_var('password_old', 'string', null, INPUT_POST);
|
||||
$password0 = get_form_var('password0', 'string', null, INPUT_POST);
|
||||
$password1 = get_form_var('password1', 'string', null, INPUT_POST);
|
||||
|
||||
$post_target = get_form_var('target_url', 'url_local', null, INPUT_POST);
|
||||
if (isset($post_target) && ($post_target != ''))
|
||||
{
|
||||
$target_url = $post_target;
|
||||
}
|
||||
|
||||
$error = null;
|
||||
|
||||
// 1. 校验当前密码
|
||||
// (注意:validateUser 失败会计入失败次数;连续 5 次错误当前账号将被临时锁定,
|
||||
// 与登录通道行为一致,属预期安全设计)
|
||||
if (($old_password === null) || ($old_password === '') ||
|
||||
!auth()->validateUser($mrbs_user->username, $old_password))
|
||||
{
|
||||
$error = 'old_pwd_invalid';
|
||||
Audit::log('PWD_CHANGE_FAIL', $mrbs_user->username, 'old password incorrect');
|
||||
}
|
||||
// 2. 两次输入一致
|
||||
elseif ($password0 !== $password1)
|
||||
{
|
||||
$error = 'pwd_not_match';
|
||||
}
|
||||
// 3. 符合复杂度策略
|
||||
elseif (($password0 === null) || ($password0 === '') ||
|
||||
!auth()->validatePassword($password0))
|
||||
{
|
||||
$error = 'pwd_invalid';
|
||||
}
|
||||
// 4. 新旧密码不同
|
||||
elseif ($password0 === $old_password)
|
||||
{
|
||||
$error = 'pwd_same';
|
||||
}
|
||||
else
|
||||
{
|
||||
// 成功:更新口令并记录修改时间
|
||||
auth()->updatePassword($mrbs_user->username, $password0);
|
||||
Audit::log('PWD_CHANGE', $mrbs_user->username, 'self-service change');
|
||||
|
||||
// 清除“强制改密”标记(须在 session_write_close 前完成)
|
||||
unset($_SESSION['mrbs_force_pwd_change']);
|
||||
session_write_close();
|
||||
|
||||
location_header('change_password.php?result=ok&target_url=' . urlencode($target_url));
|
||||
exit;
|
||||
}
|
||||
|
||||
// 校验失败:回到表单显示错误(PRG 模式,防止表单重复提交)
|
||||
location_header('change_password.php?error=' . urlencode($error) . '&target_url=' . urlencode($target_url));
|
||||
exit;
|
||||
}
|
||||
|
||||
// ===== 渲染页面 =====
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => isset($area) ? $area : null,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
$result = get_form_var('result', 'string', null, INPUT_GET);
|
||||
$error = get_form_var('error', 'string', null, INPUT_GET);
|
||||
|
||||
echo "<div class=\"contents\">\n";
|
||||
|
||||
if (isset($result) && ($result == 'ok'))
|
||||
{
|
||||
generate_change_password_success($target_url);
|
||||
}
|
||||
else
|
||||
{
|
||||
generate_change_password_form($error, $target_url);
|
||||
}
|
||||
|
||||
echo "</div>\n";
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MRBS;
|
||||
|
||||
use IntlDateFormatter;
|
||||
|
||||
require_once 'lib/autoload.inc';
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS 配置文件(精简完整版)
|
||||
* 仅保留必要配置 + 您的自定义设置
|
||||
**************************************************************************/
|
||||
|
||||
/**********
|
||||
* 时区与语言
|
||||
**********/
|
||||
$timezone = "Asia/Shanghai";
|
||||
$override_locale = 'zh-CN';
|
||||
|
||||
/*******************
|
||||
* 数据库设置
|
||||
*******************/
|
||||
$dbsys = "mysql";
|
||||
$db_host = "localhost";
|
||||
$db_database = "hotel";
|
||||
$db_login = "hotel";
|
||||
$db_password = 'i4wt5yn2'; // ← 请替换为实际数据库密码
|
||||
$db_tbl_prefix = "mrbs_";
|
||||
$db_persist = false;
|
||||
|
||||
/* ====================== 以下为自定义配置 ====================== */
|
||||
$mrbs_company = "LZOLJ";
|
||||
$vocab_override['zh']['mrbs'] = "会议预定系统V1.11.6";
|
||||
|
||||
/**********************************************
|
||||
* 邮件设置(企业163 SMTP)
|
||||
**********************************************/
|
||||
$mail_settings = array_merge($mail_settings ?? [], [ // 基于 systemdefaults 默认键合并:勿整体覆盖(会丢失 disabled/rate_limit/debug_output 等 1.12 新增默认键,导致发信报 Undefined array key)
|
||||
'from' => 'admin@hi-luzhou-lj.com',
|
||||
'use_from_for_all_mail' => true,
|
||||
'use_reply_to' => true,
|
||||
'organizer' => 'admin@hi-luzhou-lj.com',
|
||||
'recipients' => 'admin@hi-luzhou-lj.com',
|
||||
'cc' => '',
|
||||
'treat_cc_as_to' => false,
|
||||
|
||||
'admin_on_bookings' => false,
|
||||
'area_admin_on_bookings'=> true,
|
||||
'room_admin_on_bookings'=> true,
|
||||
'booker' => false, // 改成 true 可让预订者本人也收到邮件
|
||||
'on_new' => true,
|
||||
'on_change' => false,
|
||||
'on_delete' => false,
|
||||
|
||||
'allow_no_mail' => false,
|
||||
'no_mail_default' => false,
|
||||
'details' => false,
|
||||
'html' => false,
|
||||
'icalendar' => false,
|
||||
|
||||
'admin_lang' => 'zh',
|
||||
'admin_backend' => 'smtp',
|
||||
'debug' => false, // 邮件调试输出(生产必须关闭)
|
||||
]);
|
||||
|
||||
/*******************
|
||||
* SMTP 设置
|
||||
*******************/
|
||||
$smtp_settings = [
|
||||
'host' => 'smtphz.qiye.163.com',
|
||||
'port' => 465,
|
||||
'auth' => true,
|
||||
'secure' => 'ssl',
|
||||
'username' => 'admin@hi-luzhou-lj.com',
|
||||
'password' => 'NwxJ%fNmLguah%k2', // ← 请替换为实际 SMTP 密码
|
||||
'hostname' => '',
|
||||
'helo' => '',
|
||||
'disable_opportunistic_tls' => false,
|
||||
'ssl_verify_peer' => true,
|
||||
'ssl_verify_peer_name' => true,
|
||||
'ssl_allow_self_signed' => false,
|
||||
];
|
||||
|
||||
/* ====================== 推荐附加设置 ====================== */
|
||||
// 认证方式(最常用)
|
||||
$auth['type'] = 'db';
|
||||
|
||||
// 默认显示区域和房间(根据您实际的 area_id 和 room_id 修改)
|
||||
$default_area = 1;
|
||||
$default_room = 1;
|
||||
|
||||
// 最大重复预订天数(1年)
|
||||
$max_rep_interval = 365;
|
||||
|
||||
// 其他常用优化(可按需取消注释)
|
||||
// $refresh_rate = 0; // 关闭自动刷新
|
||||
// $enable_periods = false; // 使用时间段模式(而非分钟)
|
||||
|
||||
/* ====================== 等保二级整改配置(2026-09-08) ====================== */
|
||||
|
||||
// ---- C1: 口令复杂度策略 ----
|
||||
// MRBS 1.12 内建校验框架:管理端设密(edit_users.php)、自助重置均自动执行本策略
|
||||
$pwd_policy = [
|
||||
'length' => 8, // 最小长度 8 位
|
||||
'lower' => 1, // 至少 1 个小写字母
|
||||
'upper' => 1, // 至少 1 个大写字母
|
||||
'numeric' => 1, // 至少 1 个数字
|
||||
'special' => 1, // 至少 1 个特殊字符(用户自改密码仍需满足)
|
||||
];
|
||||
|
||||
// ---- C2: 会话超时(登录连接超时自动退出)----
|
||||
// 使用默认 'php' session 方案($auth['session'] 未单独设置)
|
||||
$auth['session_php']['session_name'] = 'MRBS_SESSID'; // 会话名
|
||||
$auth['session_php']['session_expire_time'] = 12 * 60 * 60; // 绝对过期:12 小时(原默认 30 天)
|
||||
$auth['session_php']['inactivity_expire_time'] = 30 * 60; // 空闲 30 分钟自动退出(原默认 0 = 永不)
|
||||
|
||||
// ---- C6: 禁止匿名访问(全站必须登录)----
|
||||
// 官方机制:所有页面最低访问级别提升为需登录;忘记密码流程除外;需确认 kiosk 模式未启用
|
||||
$auth['deny_public_access'] = true;
|
||||
|
||||
// ---- 屏幕水印开关(防截图泄密溯源;配合 Themes/default/header.inc 输出)----
|
||||
$watermark_enabled = true;
|
||||
|
||||
// ---- 代码改造参数(登录锁定 / 口令有效期 / 审计日志)----
|
||||
// 配套代码:lib/MRBS/Auth/AuthDb.php、lib/MRBS/Session/SessionWithLogin.php、
|
||||
// lib/MRBS/Audit.php、change_password.php
|
||||
$login_lock_threshold = 5; // 连续登录失败阈值(次),达到后临时锁定
|
||||
$login_lock_duration = 15 * 60; // 锁定时间(秒)= 15 分钟
|
||||
$pwd_max_age = 90 * 24 * 60 * 60; // 口令最长有效期(秒)= 90 天
|
||||
$audit_log_file = __DIR__ . '/audit/security_audit.log'; // 安全审计日志文件
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php // -*-mode: PHP; coding:utf-8;-*-
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use IntlDateFormatter;
|
||||
|
||||
require_once 'lib/autoload.inc';
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS Configuration File
|
||||
* Configure this file for your site.
|
||||
* You shouldn't have to modify anything outside this file.
|
||||
*
|
||||
* This file has already been populated with the minimum set of configuration
|
||||
* variables that you will need to change to get your system up and running.
|
||||
* If you want to change any of the other settings in systemdefaults.inc.php
|
||||
* or areadefaults.inc.php, then copy the relevant lines into this file
|
||||
* and edit them here. This file will override the default settings and
|
||||
* when you upgrade to a new version of MRBS the config file is preserved.
|
||||
*
|
||||
* NOTE: if you include or require other files from this file, for example
|
||||
* to store your database details in a separate location, then you should
|
||||
* use an absolute and not a relative pathname.
|
||||
**************************************************************************/
|
||||
|
||||
/**********
|
||||
* Timezone
|
||||
**********/
|
||||
|
||||
// The timezone your meeting rooms run in. It is especially important
|
||||
// to set this if you're using PHP 5 on Linux. In this configuration
|
||||
// if you don't, meetings in a different DST than you are currently
|
||||
// in are offset by the DST offset incorrectly.
|
||||
//
|
||||
// Note that timezones can be set on a per-area basis, so strictly speaking this
|
||||
// setting should be in areadefaults.inc.php, but as it is so important to set
|
||||
// the right timezone it is included here.
|
||||
//
|
||||
// When upgrading an existing installation, this should be set to the
|
||||
// timezone the web server runs in. See the INSTALL document for more information.
|
||||
//
|
||||
// A list of valid timezones can be found at http://php.net/manual/timezones.php
|
||||
// The following line must be uncommented by removing the '//' at the beginning
|
||||
//$timezone = "Europe/London";
|
||||
|
||||
|
||||
/*******************
|
||||
* Database settings
|
||||
******************/
|
||||
|
||||
// If you are using cPanel on your web server, make sure you include the prefix,
|
||||
// typically 8 characters followed by an underscore, in your database name and
|
||||
// database username. For example $db_database = "abcdefgh_mrbs". (Note: this
|
||||
// prefix is not the same as the table prefix below.)
|
||||
|
||||
// Which database system: "pgsql"=PostgreSQL, "mysql"=MySQL
|
||||
$dbsys = "mysql";
|
||||
// Hostname of database server. For pgsql, can use "" instead of localhost
|
||||
// to use Unix Domain Sockets instead of TCP/IP. For mysql "localhost"
|
||||
// tells the system to use Unix Domain Sockets, and $db_port will be ignored;
|
||||
// if you want to force TCP connection you can use "127.0.0.1".
|
||||
$db_host = "localhost";
|
||||
// If you need to use a non standard port for the database connection you
|
||||
// can uncomment the following line and specify the port number
|
||||
// $db_port = 1234;
|
||||
// Database name:
|
||||
$db_database = "mrbs";
|
||||
// Schema name. This only applies to PostgreSQL and is only necessary if you have more
|
||||
// than one schema in your database and also you are using the same MRBS table names in
|
||||
// multiple schemas.
|
||||
//$db_schema = "public";
|
||||
// Database login user name:
|
||||
$db_login = "mrbs";
|
||||
// Database login password:
|
||||
$db_password = 'mrbs-password';
|
||||
// Prefix for table names. This will allow multiple installations where only
|
||||
// one database is available
|
||||
$db_tbl_prefix = "mrbs_";
|
||||
// Set $db_persist to TRUE to use PHP persistent (pooled) database connections. Note
|
||||
// that persistent connections are not recommended unless your system suffers significant
|
||||
// performance problems without them. They can cause problems with transactions and
|
||||
// locks (see http://php.net/manual/en/features.persistent-connections.php) and although
|
||||
// MRBS tries to avoid those problems, it is generally better not to use persistent
|
||||
// connections if you can.
|
||||
$db_persist = false;
|
||||
|
||||
|
||||
/* Add lines from systemdefaults.inc.php and areadefaults.inc.php below here
|
||||
to change the default configuration. Do _NOT_ modify systemdefaults.inc.php
|
||||
or areadefaults.inc.php. */
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MRBS;
|
||||
|
||||
use IntlDateFormatter;
|
||||
|
||||
require_once 'lib/autoload.inc';
|
||||
|
||||
/**************************************************************************
|
||||
* MRBS 配置文件(精简完整版)
|
||||
* 仅保留必要配置 + 您的自定义设置
|
||||
**************************************************************************/
|
||||
|
||||
/**********
|
||||
* 时区与语言
|
||||
**********/
|
||||
$timezone = "Asia/Shanghai";
|
||||
$override_locale = 'zh-CN';
|
||||
|
||||
/*******************
|
||||
* 数据库设置
|
||||
*******************/
|
||||
$dbsys = "mysql";
|
||||
$db_host = "localhost";
|
||||
$db_database = "hotel";
|
||||
$db_login = "hotel";
|
||||
$db_password = 'i4wt5yn2'; // ← 请替换为实际数据库密码
|
||||
$db_tbl_prefix = "mrbs_";
|
||||
$db_persist = false;
|
||||
|
||||
/* ====================== 以下为自定义配置 ====================== */
|
||||
$mrbs_company = "LZOLJ";
|
||||
$vocab_override['zh']['mrbs'] = "会议预定系统V1.11.6";
|
||||
|
||||
/**********************************************
|
||||
* 邮件设置(企业163 SMTP)
|
||||
**********************************************/
|
||||
$mail_settings = [
|
||||
'from' => 'admin@hi-luzhou-lj.com',
|
||||
'use_from_for_all_mail' => true,
|
||||
'use_reply_to' => true,
|
||||
'organizer' => 'admin@hi-luzhou-lj.com',
|
||||
'recipients' => 'admin@hi-luzhou-lj.com',
|
||||
'cc' => '',
|
||||
'treat_cc_as_to' => false,
|
||||
|
||||
'admin_on_bookings' => false,
|
||||
'area_admin_on_bookings'=> true,
|
||||
'room_admin_on_bookings'=> true,
|
||||
'booker' => false, // 改成 true 可让预订者本人也收到邮件
|
||||
'on_new' => true,
|
||||
'on_change' => false,
|
||||
'on_delete' => false,
|
||||
|
||||
'allow_no_mail' => false,
|
||||
'no_mail_default' => false,
|
||||
'details' => false,
|
||||
'html' => false,
|
||||
'icalendar' => false,
|
||||
|
||||
'admin_lang' => 'zh',
|
||||
'admin_backend' => 'smtp',
|
||||
];
|
||||
|
||||
/*******************
|
||||
* SMTP 设置
|
||||
*******************/
|
||||
$smtp_settings = [
|
||||
'host' => 'smtphz.qiye.163.com',
|
||||
'port' => 465,
|
||||
'auth' => true,
|
||||
'secure' => 'ssl',
|
||||
'username' => 'admin@hi-luzhou-lj.com',
|
||||
'password' => 'NwxJ%fNmLguah%k2', // ← 请替换为实际 SMTP 密码
|
||||
'hostname' => '',
|
||||
'helo' => '',
|
||||
'disable_opportunistic_tls' => false,
|
||||
'ssl_verify_peer' => true,
|
||||
'ssl_verify_peer_name' => true,
|
||||
'ssl_allow_self_signed' => false,
|
||||
];
|
||||
|
||||
/* ====================== 推荐附加设置 ====================== */
|
||||
// 认证方式(最常用)
|
||||
$auth['type'] = 'db';
|
||||
|
||||
// 默认显示区域和房间(根据您实际的 area_id 和 room_id 修改)
|
||||
$default_area = 1;
|
||||
$default_room = 1;
|
||||
|
||||
// 最大重复预订天数(1年)
|
||||
$max_rep_interval = 365;
|
||||
|
||||
// 其他常用优化(可按需取消注释)
|
||||
// $refresh_rate = 0; // 关闭自动刷新
|
||||
// $enable_periods = false; // 使用时间段模式(而非分钟)
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
/* Modifications to the standard CSS when using RTL languages (eg Hebrew) */
|
||||
|
||||
/* ------------ EDIT_USERS.PHP ------------------*/
|
||||
|
||||
#edit_room fieldset.submit_buttons {
|
||||
padding-top: 460px;
|
||||
margin-top: 1em
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
|
||||
/* Fixes for Internet Explorer (all versions) */
|
||||
|
||||
|
||||
/* ------------ ADMIN.PHP ---------------------------*/
|
||||
#admin ul {
|
||||
margin-top: 1.0em;
|
||||
}
|
||||
|
||||
form.form_admin {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form_admin legend {
|
||||
margin-bottom: 1.0em; /* by default IE gives no gap between legend and first form element */
|
||||
}
|
||||
|
||||
|
||||
/* ------------ INDEX.PHP ------------------*/
|
||||
table.dwm_main {
|
||||
border-collapse: collapse; /* separate gives better corners in Firefox, but doesn't work in IE6/7 */
|
||||
}
|
||||
|
||||
div.booking_list {
|
||||
overflow-x: hidden; /* we don't want IE to give us the horizontal scrollbar */
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require_once "systemdefaults.inc.php";
|
||||
require_once "config.inc.php";
|
||||
require_once "theme.inc";
|
||||
|
||||
global $body_background_color, $standard_font_color, $standard_font_family;
|
||||
global $banner_back_color, $banner_font_color;
|
||||
?>
|
||||
|
||||
/* CSS to be used for email messages */
|
||||
|
||||
body#mrbs {
|
||||
background-color: <?php echo $body_background_color ?>;
|
||||
color: <?php echo $standard_font_color ?>;
|
||||
font-family: <?php echo $standard_font_family ?>;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div#header {
|
||||
width: 100%;
|
||||
background-color: <?php echo $banner_back_color ?>;
|
||||
color: <?php echo $banner_font_color ?>;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
div#contents {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
#mrbs a:link {
|
||||
color: #0B263B;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#mrbs a:visited {
|
||||
color: #0B263B;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#mrbs a:hover {
|
||||
color: #ff0066;
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#mrbs tr {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#mrbs th, #mrbs td {
|
||||
text-align: left;
|
||||
padding: 1px 1em;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require_once "../systemdefaults.inc.php";
|
||||
require_once "../config.inc.php";
|
||||
require_once "../functions.inc";
|
||||
require_once "../theme.inc";
|
||||
|
||||
http_headers(array("Content-type: text/css"),
|
||||
60*30); // 30 minute cache expiry
|
||||
?>
|
||||
|
||||
.screenonly, .banner, div.minicalendars.formed,
|
||||
nav:not(.main_calendar):not(.arrow):not(.location):not(.view) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
nav.arrow, nav.view {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
td.new a, a.new_booking img { display: none; }
|
||||
|
||||
.dwm_main :not(tbody) th {
|
||||
color: <?php echo $header_font_color_print ?>;
|
||||
}
|
||||
|
||||
.dwm_main th a:link {
|
||||
color: <?php echo $anchor_link_color_header_print ?>;
|
||||
}
|
||||
|
||||
<?php
|
||||
// redefine table and cell border colours so that they are visible in the print view
|
||||
// (in the screen view the boundaries are visible due to the different background colours)
|
||||
?>
|
||||
|
||||
table.dwm_main {
|
||||
border-width: 1px 0 1px 1px;
|
||||
border-color: <?php echo $main_table_border_color_print ?>;
|
||||
}
|
||||
|
||||
.dwm_main td,
|
||||
.dwm_main tbody th {
|
||||
box-shadow: 0 -1px 0 <?php echo $main_table_body_h_border_color_print ?>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
<?php
|
||||
// In the month view, get rid of horizontal and vertical scrollbars. Make
|
||||
// horizontal overflow hidden and allow the table cell to grow to accommodate
|
||||
// vertical overflow.
|
||||
?>
|
||||
|
||||
div.cell_container {
|
||||
min-height: 100px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
div.cell_header {
|
||||
min-height: 1.4em;
|
||||
height: 1.4em;
|
||||
max-height: 1.4em;
|
||||
}
|
||||
|
||||
div.booking_list {
|
||||
overflow: hidden;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
div.booking_list div {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
<?php
|
||||
// Generate the rules to give the colour coding by booking type in the day/week/month views
|
||||
// and the colour key
|
||||
foreach ($color_types as $type => $col)
|
||||
{
|
||||
echo "div.$type a, div.$type {outline: 2px solid $col; outline-offset: -2px}\n";
|
||||
}
|
||||
|
||||
// hide DataTable buttons in print
|
||||
?>
|
||||
|
||||
.ColVis_Button, .dataTables_filter, .dataTables_length, .dataTables_paginate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-resizable-handle {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// Modifications to the standard CSS when using RTL languages (eg Hebrew)
|
||||
|
||||
require_once "../systemdefaults.inc.php";
|
||||
require_once "../config.inc.php";
|
||||
require_once "../functions.inc";
|
||||
require_once "../theme.inc";
|
||||
|
||||
http_headers(array("Content-type: text/css"),
|
||||
60*30); // 30 minute cache expiry
|
||||
?>
|
||||
|
||||
|
||||
/* ------------ GENERAL -----------------------------*/
|
||||
|
||||
h1, h2, td, th {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* ------------ ADMIN.PHP ---------------------------*/
|
||||
|
||||
form.form_admin, .form_admin div, div#div_custom_html,
|
||||
#area_form form, #area_form label[for="area_select"],
|
||||
.areaChangeForm select, .areaChangeForm input, .areaChangeForm input.button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.form_admin label, .form_admin fieldset, .form_admin input,
|
||||
div#area_form, div#room_form {
|
||||
float: inherit;
|
||||
}
|
||||
|
||||
.form_admin label {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.form_admin fieldset {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.form_admin legend {
|
||||
padding-left: 36px;
|
||||
}
|
||||
|
||||
.admin h2 {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
div#area_form, div.header_columns, div.body_columns {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
/* ------------ INDEX.PHP ------------------*/
|
||||
|
||||
div#dwm_header, div.cell_container {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.date_before {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.date_now {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.date_after {
|
||||
float: left;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.date_before, .date_after, table.dwm_main {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
#dwm_header ul {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
<?php
|
||||
foreach ($color_types as $type => $col)
|
||||
{
|
||||
echo ".month div.$type {float: right}\n"; // used in the month view
|
||||
}
|
||||
?>
|
||||
|
||||
/* ------------ EDIT_AREA.PHP ------------------*/
|
||||
|
||||
#book_ahead_periods_note span {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* ------------ FUNCTIONS.INC -------------------*/
|
||||
|
||||
.banner {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.banner li {
|
||||
border-width: 0 <?php echo $banner_border_cell_width ?>px 0 0;
|
||||
}
|
||||
|
||||
/* ------------ MINCALS.PHP ---------------------*/
|
||||
|
||||
div#cal_last, div#cal_this, div#cal_next {
|
||||
float: right;
|
||||
}
|
||||
|
||||
div#cal_last {
|
||||
margin-left: 1.0em;
|
||||
}
|
||||
|
||||
table.calendar {
|
||||
margin-right: 30px
|
||||
}
|
||||
|
||||
.calendar th {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* ------------ PENDING.PHP ------------------*/
|
||||
|
||||
table#pending_list {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
#pending_list form {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#pending_list td, #pending_list td.control + td,
|
||||
#pending_list th.header_name, #pending_list th.header_create, #pending_list th.header_area,
|
||||
#pending_list th.header_room, #pending_list th.header_action {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#pending_list th.control + th, #pending_list td.control + td {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
/* ------------ REPORT.PHP ----------------------*/
|
||||
|
||||
.div_report h3, .div_report table,
|
||||
div.report_entry_name {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
div.report_entry_title, div.report_entry_name, p.report_entries {
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/* ------------ VIEW_ENTRY.PHP ------------------*/
|
||||
|
||||
.view_entry div#view_entry_nav {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.view_entry #approve_buttons form {
|
||||
float: right;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
// Returns the full table name including schema and prefix for a given table.
|
||||
// Needs to be dynamic rather than static in case we are running a multisite
|
||||
// installation and want to switch sites, eg during a database upgrade.
|
||||
|
||||
use MRBS\DB\DB;
|
||||
use MRBS\DB\DBFactory;
|
||||
use MRBS\Errors\Errors;
|
||||
use PDO;
|
||||
|
||||
function _tbl(string $short_name, bool $include_schema=true) : string
|
||||
{
|
||||
global $dbsys, $db_tbl_prefix, $db_schema;
|
||||
|
||||
// Do some sanity checking
|
||||
if (!isset($short_name))
|
||||
{
|
||||
throw new \Exception('$short_name not set.');
|
||||
}
|
||||
|
||||
$result = $db_tbl_prefix . $short_name;
|
||||
|
||||
// Prepend the schema name if set and form a qualified name for all databases
|
||||
// other than MySQL, which is one of the few that doesn't support schemas.
|
||||
// (Although in practice this means PostgreSQL at the moment, it's possible that
|
||||
// in the future support for more databases may be added)
|
||||
if ($include_schema && (mb_strpos($dbsys, 'mysql') === false) && isset($db_schema))
|
||||
{
|
||||
$result = $db_schema . '.' . $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
function get_table_short_name(string $table) : string
|
||||
{
|
||||
global $db_tbl_prefix;
|
||||
|
||||
// Get everything after the last '.', ie strip off any database
|
||||
// and schema names
|
||||
if (false !== ($pos = mb_strrpos($table, '.')))
|
||||
{
|
||||
$result = mb_substr($table, $pos + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
$result = $table;
|
||||
}
|
||||
|
||||
// Strip the prefix off the table name
|
||||
return mb_substr($result, mb_strlen($db_tbl_prefix));
|
||||
}
|
||||
|
||||
|
||||
// Convenience wrapper function to provide access to a DB object for
|
||||
// default MRBS database
|
||||
function db() : DB
|
||||
{
|
||||
global $db_persist, $db_host, $db_login, $db_password,
|
||||
$db_database, $db_port, $dbsys, $db_options;
|
||||
|
||||
static $db_obj = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (is_null($db_obj))
|
||||
{
|
||||
throw new \Exception("DB object not yet created");
|
||||
}
|
||||
// Check to see if the connection is still there. If it isn't - perhaps because it
|
||||
// has been timed out, eg by MySQL's wait_timeout - then we throw an exception which
|
||||
// will cause us to re-create a connection.
|
||||
// (Note that we cannot try a "SELECT 1" query or something similar to check if the
|
||||
// connection is still there, because if it is then the query will cause the last insert
|
||||
// id to be lost.)
|
||||
// TODO: sometimes the symptom of a lost connection is a warning of the form "Packets
|
||||
// TODO: out of order. Expected 1 received 0. Packet size=145". Need to work out why
|
||||
// TODO: this happens and do something about it.
|
||||
if ($db_obj->getAttribute(PDO::ATTR_SERVER_INFO) == 'MySQL server has gone away')
|
||||
{
|
||||
// On most recent versions of PHP the call to getAttribute() on a lost connection will
|
||||
// throw an exception anyway, but just in case it doesn't we'll throw one. See
|
||||
// https://stackoverflow.com/questions/21595402/php-pdo-how-to-get-the-current-connection-status
|
||||
throw new \Exception('MySQL server has gone away');
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
try
|
||||
{
|
||||
$db_obj = DBFactory::create(
|
||||
$dbsys,
|
||||
$db_host,
|
||||
$db_login,
|
||||
$db_password,
|
||||
$db_database,
|
||||
(bool)$db_persist,
|
||||
$db_port,
|
||||
$db_options
|
||||
);
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
trigger_error($e->getMessage(), E_USER_WARNING);
|
||||
Errors::fatalError(get_vocab('fatal_db_error'));
|
||||
}
|
||||
}
|
||||
|
||||
return $db_obj;
|
||||
}
|
||||
|
||||
|
||||
// Returns the db schema version as recorded in the database. If there is no version
|
||||
// recorded then returns 0. If $local is true then the local db schema version is returned.
|
||||
function db_schema_version(DB $handle, bool $local=false) : int
|
||||
{
|
||||
if ($handle->table_exists(_tbl('variables')))
|
||||
{
|
||||
$sql_params = [':variable_name' => ($local) ? 'local_db_version' : 'db_version'];
|
||||
$sql = "SELECT variable_content
|
||||
FROM " . _tbl('variables') . "
|
||||
WHERE variable_name=:variable_name";
|
||||
$result = $handle->query1($sql, $sql_params);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default version is 0, before we had schema versions
|
||||
$result = 0;
|
||||
}
|
||||
|
||||
return max($result, 0);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
define ('MRBS_MIN_PHP_VERSION', '7.2.5');
|
||||
|
||||
// Check PHP version
|
||||
// Do it now before we start including code that might fail with a syntax error,
|
||||
// for example if anonymous functions are being used.
|
||||
if (!function_exists('version_compare') || version_compare(PHP_VERSION, MRBS_MIN_PHP_VERSION) < 0)
|
||||
{
|
||||
die("MRBS requires PHP " . MRBS_MIN_PHP_VERSION . " or above. This server is running version " . PHP_VERSION . ".");
|
||||
}
|
||||
|
||||
define('MRBS_ROOT', __DIR__); // Root of MRBS installation
|
||||
|
||||
// We use require for some files rather than require_once because the values that
|
||||
// are assigned to variables will change depending on the context in which the file
|
||||
// is called.
|
||||
|
||||
require_once 'lib/autoload.inc';
|
||||
require_once 'grab_globals.inc.php'; // this must be included before mrbs_auth.inc (due to WordPress - see comment in file)
|
||||
require_once 'systemdefaults.inc.php';
|
||||
require_once 'areadefaults.inc.php';
|
||||
require_once 'config.inc.php';
|
||||
require_once 'site_config.inc';
|
||||
require_once 'internalconfig.inc.php';
|
||||
require_once 'functions_global.inc';
|
||||
require_once 'functions.inc';
|
||||
require_once 'theme.inc';
|
||||
require_once 'dbsys.inc';
|
||||
require_once 'mrbs_auth.inc';
|
||||
require_once 'init.inc';
|
||||
require_once 'upgrade.inc';
|
||||
require 'standard_vars.inc.php';
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\DB\DBException;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
|
||||
function generate_no_form(int $room, int $area) : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('action' => multisite('admin.php'));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('area' => $area,
|
||||
'room' => $room);
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// The button
|
||||
$element = new ElementInputSubmit();
|
||||
$element->setAttribute('value', get_vocab("NO"));
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
function generate_yes_form(int $room, int $area) : void
|
||||
{
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('action' => multisite('del.php'));
|
||||
|
||||
$form->setAttributes($attributes);
|
||||
|
||||
// Hidden inputs
|
||||
$hidden_inputs = array('type' => 'room',
|
||||
'area' => $area,
|
||||
'room' => $room,
|
||||
'confirm' => '1');
|
||||
$form->addHiddenInputs($hidden_inputs);
|
||||
|
||||
// The button
|
||||
$element = new ElementInputSubmit();
|
||||
$element->setAttribute('value', get_vocab("YES"));
|
||||
$form->addElement($element);
|
||||
|
||||
$form->render();
|
||||
}
|
||||
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Get non-standard form variables
|
||||
$type = get_form_var('type', 'string');
|
||||
$confirm = get_form_var('confirm', 'string', null, INPUT_POST);
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
// This is gonna blast away something. We want them to be really
|
||||
// really sure that this is what they want to do.
|
||||
if ($type == "room")
|
||||
{
|
||||
// We are supposed to delete a room
|
||||
if (!empty($confirm))
|
||||
{
|
||||
// They have confirmed it already, so go blast!
|
||||
db()->begin();
|
||||
try
|
||||
{
|
||||
// First take out all appointments for this room
|
||||
$sql = "DELETE FROM " . _tbl('entry') . " WHERE room_id=?";
|
||||
db()->command($sql, array($room));
|
||||
|
||||
$sql = "DELETE FROM " . _tbl('repeat') . " WHERE room_id=?";
|
||||
db()->command($sql, array($room));
|
||||
|
||||
// Now take out the room itself
|
||||
$sql = "DELETE FROM " . _tbl('room') . " WHERE id=?";
|
||||
db()->command($sql, array($room));
|
||||
}
|
||||
catch (DBException $e)
|
||||
{
|
||||
db()->rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
db()->commit();
|
||||
|
||||
// Go back to the admin page
|
||||
location_header("admin.php?area=$area");
|
||||
}
|
||||
else
|
||||
{
|
||||
print_header($context);
|
||||
|
||||
// We tell them how bad what they're about to do is
|
||||
// Find out how many appointments would be deleted
|
||||
// Do a quick count of the number of entries
|
||||
$n_entries = get_n_entries_by_room($room);
|
||||
|
||||
if ($n_entries > 0)
|
||||
{
|
||||
$limit = 20;
|
||||
// Order in descending order because the latest bookings are probably the most important.
|
||||
$entries = get_entries_by_room($room, null, null, true, $limit);
|
||||
|
||||
// We can't rely on ($n_entries > 0) because there's a very small chance the number of entries
|
||||
// may have changed between the two queries
|
||||
if (count($entries) > 0)
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo get_vocab("deletefollowing") . ":\n";
|
||||
echo "</p>\n";
|
||||
|
||||
echo "<ul>\n";
|
||||
|
||||
foreach ($entries as $entry)
|
||||
{
|
||||
$interval = new EntryInterval($entry['start_time'], $entry['end_time'], $enable_periods);
|
||||
echo "<li>" . escape_html($entry['name']) . " (" . $interval . ")</li>\n";
|
||||
}
|
||||
|
||||
echo "</ul>\n";
|
||||
}
|
||||
|
||||
if ($n_entries > $limit)
|
||||
{
|
||||
echo "<p>";
|
||||
$formatter = new \NumberFormatter(Language::getInstance()->getWebLocale(), \NumberFormatter::DEFAULT_STYLE);
|
||||
echo get_vocab("and_n_more", $formatter->format($n_entries - $limit)) . '.';
|
||||
echo "</p>";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<div id=\"del_room_confirm\">\n";
|
||||
echo "<p>" . get_vocab("sure") . "</p>\n";
|
||||
|
||||
generate_yes_form($room, $area);
|
||||
generate_no_form($room, $area);
|
||||
|
||||
echo "</div>\n";
|
||||
print_footer();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if ($type == "area")
|
||||
{
|
||||
// We are only going to let them delete an area if there are
|
||||
// no rooms. its easier
|
||||
$sql = "SELECT COUNT(*)
|
||||
FROM " . _tbl('room') . "
|
||||
WHERE area_id=?";
|
||||
|
||||
$n = db()->query1($sql, array($area));
|
||||
if ($n === 0)
|
||||
{
|
||||
// OK, nothing there, let's blast it away
|
||||
$sql = "DELETE FROM " . _tbl('area') . "
|
||||
WHERE id=?";
|
||||
|
||||
db()->command($sql, array($area));
|
||||
|
||||
// Redirect back to the admin page
|
||||
location_header('admin.php');
|
||||
}
|
||||
else
|
||||
{
|
||||
// There are rooms left in the area
|
||||
print_header($context);
|
||||
echo "<p>\n";
|
||||
echo get_vocab("delarea");
|
||||
$query_vars = array('area' => $area);
|
||||
$query = http_build_query($query_vars, '', '&');
|
||||
echo '<a href="' . escape_html(multisite("admin.php?$query")) . '">' . get_vocab('back') . '</a>';
|
||||
echo "</p>\n";
|
||||
print_footer();
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception ("Unknown type");
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
// Deletes an entry, or a series. The $id is always the id of
|
||||
// an individual entry. If $series is set then the entire series
|
||||
// of which $id is a member should be deleted. [Note - this use of
|
||||
// $series is inconsistent with use in the rest of MRBS where it
|
||||
// means that $id is the id of an entry in the repeat table. This
|
||||
// should be fixed sometime.]
|
||||
|
||||
require "defaultincludes.inc";
|
||||
require_once "mrbs_sql.inc";
|
||||
require_once "functions_mail.inc";
|
||||
|
||||
// Get non-standard form variables
|
||||
$id = get_form_var('id', 'int', null, INPUT_POST);
|
||||
$series = get_form_var('series', 'bool', null, INPUT_POST);
|
||||
$returl = get_form_var('returl', 'url_local', null, INPUT_POST);
|
||||
$action = get_form_var('action', 'string', 'delete', INPUT_POST);
|
||||
$note = get_form_var('note', 'string', '', INPUT_POST);
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
if (empty($returl))
|
||||
{
|
||||
$vars = array('view' => $default_view,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room);
|
||||
|
||||
$returl .= 'index.php?' . http_build_query($vars, '', '&');
|
||||
}
|
||||
|
||||
if ($info = get_booking_info($id, FALSE, TRUE))
|
||||
{
|
||||
// check that the user is allowed to delete this entry
|
||||
if (isset($action) && ($action == "reject"))
|
||||
{
|
||||
$authorised = is_book_admin($info['room_id']);
|
||||
}
|
||||
else
|
||||
{
|
||||
$authorised = getWritable($info['create_by'], $info['room_id']);
|
||||
}
|
||||
if ($authorised)
|
||||
{
|
||||
$day = (int) date('d', $info['start_time']);
|
||||
$month = (int) date('m', $info['start_time']);
|
||||
$year = (int) date('Y', $info['start_time']);
|
||||
$area = get_area($info["room_id"]);
|
||||
if (empty($area))
|
||||
{
|
||||
throw new \Exception("Room " . $info['room_id'] . " does not exist");
|
||||
}
|
||||
// Get the settings for this area (they will be needed for policy checking)
|
||||
get_area_settings($area);
|
||||
|
||||
$notify_by_email = $mail_settings['on_delete'] && need_to_send_mail();
|
||||
|
||||
if ($notify_by_email)
|
||||
{
|
||||
// Gather all fields values for use in emails.
|
||||
$mail_previous = get_booking_info($id, FALSE);
|
||||
// If this is an individual entry of a series then force the entry_type
|
||||
// to be a changed entry, so that when we create the iCalendar object we know that
|
||||
// we only want to delete the individual entry
|
||||
if (!$series && ($mail_previous['repeat_rule']->getType() != RepeatRule::NONE))
|
||||
{
|
||||
$mail_previous['entry_type'] = ENTRY_RPT_CHANGED;
|
||||
}
|
||||
}
|
||||
|
||||
$start_times = mrbsDelEntry($id, $series, true);
|
||||
|
||||
// [At the moment MRBS does not inform the user if it was not able to delete
|
||||
// an entry, or, for a series, some entries in a series. This could happen for
|
||||
// example if a booking policy is in force that prevents the deletion of entries
|
||||
// in the past. It would be better to inform the user that the operation has
|
||||
// been unsuccessful or only partially successful]
|
||||
if (($start_times !== FALSE) && (count($start_times) > 0))
|
||||
{
|
||||
// Send a mail to the Administrator
|
||||
if ($notify_by_email)
|
||||
{
|
||||
// Now that we've finished with mrbsDelEntry, change the id so that it's
|
||||
// the repeat_id if we're looking at a series. (This is a complete hack,
|
||||
// but brings us back into line with the rest of MRBS until the anomaly
|
||||
// of del_entry is fixed)
|
||||
if ($series)
|
||||
{
|
||||
$mail_previous['id'] = $mail_previous['repeat_id'];
|
||||
}
|
||||
notify_by_email($mail_previous, [], $series, $action, $start_times, $note);
|
||||
}
|
||||
|
||||
}
|
||||
location_header($returl);
|
||||
}
|
||||
}
|
||||
|
||||
// If you got this far then we got an access denied.
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area);
|
||||
|
||||
@@ -0,0 +1,851 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Errors\Errors;
|
||||
use MRBS\Form\ElementDiv;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementInputCheckbox;
|
||||
use MRBS\Form\ElementInputNumber;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\ElementInputTime;
|
||||
use MRBS\Form\ElementLegend;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\ElementSelect;
|
||||
use MRBS\Form\ElementSpan;
|
||||
use MRBS\Form\FieldButton;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputCheckbox;
|
||||
use MRBS\Form\FieldInputCheckboxGroup;
|
||||
use MRBS\Form\FieldInputEmail;
|
||||
use MRBS\Form\FieldInputNumber;
|
||||
use MRBS\Form\FieldInputRadioGroup;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\FieldInputText;
|
||||
use MRBS\Form\FieldInputTime;
|
||||
use MRBS\Form\FieldSelect;
|
||||
use MRBS\Form\FieldSpan;
|
||||
use MRBS\Form\FieldTextarea;
|
||||
use MRBS\Form\FieldTimeWithUnits;
|
||||
use MRBS\Form\Form;
|
||||
use stdClass;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
require_once "mrbs_sql.inc";
|
||||
|
||||
function get_timezone_options() : array
|
||||
{
|
||||
global $zoneinfo_outlook_compatible;
|
||||
|
||||
$special_group = "Others";
|
||||
$timezones = array();
|
||||
$timezone_identifiers = timezone_identifiers_list();
|
||||
|
||||
foreach ($timezone_identifiers as $value)
|
||||
{
|
||||
if (mb_strpos($value, '/') === FALSE)
|
||||
{
|
||||
// There are some timezone identifiers (eg 'UTC') on some operating
|
||||
// systems that don't fit the Continent/City model. We'll put them
|
||||
// into the special group
|
||||
$continent = $special_group;
|
||||
$city = $value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: timezone identifiers can have three components, eg
|
||||
// America/Argentina/Tucuman. To keep things simple we will
|
||||
// treat anything after the first '/' as a single city and
|
||||
// limit the explosion to two
|
||||
list($continent, $city) = explode('/', $value, 2);
|
||||
}
|
||||
// Check that there's a VTIMEZONE definition
|
||||
$tz_dir = ($zoneinfo_outlook_compatible) ? TZDIR_OUTLOOK : TZDIR;
|
||||
$tz_file = "$tz_dir/$value.ics";
|
||||
// UTC is a special case because we can always produce UTC times in iCalendar
|
||||
if (($city=='UTC') || is_readable($tz_file))
|
||||
{
|
||||
$key = ($continent == $special_group) ? $city : "$continent/$city";
|
||||
$timezones[$continent][$key] = $city;
|
||||
}
|
||||
}
|
||||
|
||||
return $timezones;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_errors(array $errors) : ElementFieldset
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend('')
|
||||
->setAttribute('class', 'error');
|
||||
|
||||
foreach ($errors as $error)
|
||||
{
|
||||
$element = new ElementP();
|
||||
$element->setText(get_vocab($error));
|
||||
$fieldset-> addElement($element);
|
||||
}
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_general(array $data) : ElementFieldset
|
||||
{
|
||||
global $timezone, $auth;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('general_settings'));
|
||||
|
||||
// Area name
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('name'))
|
||||
->setControlAttributes(array('id' => 'area_name',
|
||||
'name' => 'area_name',
|
||||
'required' => true,
|
||||
'maxlength' => maxlength('area.area_name'),
|
||||
'value' => $data['area_name']));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Sort key
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('sort_key'))
|
||||
->setLabelAttributes(array('title' => get_vocab('sort_key_note')))
|
||||
->setControlAttributes(array('id' => 'sort_key',
|
||||
'name' => 'sort_key',
|
||||
'value' => $data['sort_key'],
|
||||
'maxlength' => maxlength('area.sort_key')));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Area admin email
|
||||
$field = new FieldInputEmail();
|
||||
$field->setLabel(get_vocab('area_admin_email'))
|
||||
->setLabelAttribute('title', get_vocab('email_list_note'))
|
||||
->setControlAttributes(array('id' => 'area_admin_email',
|
||||
'name' => 'area_admin_email',
|
||||
'value' => $data['area_admin_email'],
|
||||
'multiple' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// The custom HTML
|
||||
if ($auth['allow_custom_html'])
|
||||
{
|
||||
$field = new FieldTextarea();
|
||||
$field->setLabel(get_vocab('custom_html'))
|
||||
->setLabelAttribute('title', get_vocab('custom_html_note'))
|
||||
->setControlAttribute('name', 'custom_html')
|
||||
->setControlText($data['custom_html'] ?? '');
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// Timezone
|
||||
$field = new FieldSelect();
|
||||
$field->setLabel(get_vocab('timezone'))
|
||||
->setControlAttributes(array('id' => 'area_timezone',
|
||||
'name' => 'area_timezone'))
|
||||
->addSelectOptions(get_timezone_options(), $timezone, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Default type
|
||||
$options = get_type_options(false);
|
||||
if (count($options)>0)
|
||||
{
|
||||
$field = new FieldSelect();
|
||||
$field->setLabel(get_vocab('default_type'))
|
||||
->setControlAttribute('name', 'area_default_type')
|
||||
->addSelectOptions($options, $data['default_type'], true);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// Status - Enabled or Disabled
|
||||
$options = array('0' => get_vocab('enabled'),
|
||||
'1' => get_vocab('disabled'));
|
||||
$value = ($data['disabled']) ? '1' : '0';
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setAttribute('id', 'status')
|
||||
->setLabel(get_vocab('status'))
|
||||
->setLabelAttributes(array('title' => get_vocab('disabled_area_note')))
|
||||
->addRadioOptions($options, 'area_disabled', $value, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Mode - Times or Periods
|
||||
$options = array('1' => get_vocab('mode_periods'),
|
||||
'0' => get_vocab('mode_times'));
|
||||
$value = ($data['enable_periods']) ? '1' : '0';
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setAttribute('id', 'mode')
|
||||
->setLabel(get_vocab('mode'))
|
||||
->addRadioOptions($options, 'area_enable_periods', $value, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Times along the top
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('times_along_top'))
|
||||
->setControlAttribute('name', 'area_times_along_top')
|
||||
->setControlChecked($data['times_along_top']);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_times() : ElementFieldset
|
||||
{
|
||||
global $enable_periods;
|
||||
global $morningstarts, $morningstarts_minutes;
|
||||
global $eveningends, $eveningends_minutes;
|
||||
global $resolution, $default_duration, $default_duration_all_day;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->setAttribute('id', 'time_settings');
|
||||
|
||||
// If we're using JavaScript, don't display the time settings section
|
||||
// if we're using periods (the JavaScript will display it if we change)
|
||||
if ($enable_periods)
|
||||
{
|
||||
$fieldset->setAttribute('class', 'js_none');
|
||||
}
|
||||
|
||||
$span = new ElementSpan();
|
||||
$span->setAttribute('class', 'js_none')
|
||||
->setText(' (' . get_vocab('times_only') . ')');
|
||||
|
||||
$legend = new ElementLegend();
|
||||
$legend->setText(get_vocab('time_settings'), true)
|
||||
->addElement($span);
|
||||
|
||||
$fieldset->addLegend($legend);
|
||||
|
||||
// First slot start
|
||||
$field = new FieldInputTime();
|
||||
$value = sprintf('%02d:%02d', $morningstarts, $morningstarts_minutes);
|
||||
$field->setLabel(get_vocab('area_first_slot_start'))
|
||||
->setControlAttributes(array('id' => 'area_start_first_slot',
|
||||
'name' => 'area_start_first_slot',
|
||||
'value' => $value,
|
||||
'required' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Resolution
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('area_res_mins'))
|
||||
->setControlAttributes(array('id' => 'area_res_mins',
|
||||
'name' => 'area_res_mins',
|
||||
'min' => '1',
|
||||
'value' => (int) $resolution/60,
|
||||
'required' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Duration
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('area_def_duration_mins'))
|
||||
->setControlAttributes(array('id' => 'area_def_duration_mins',
|
||||
'name' => 'area_def_duration_mins',
|
||||
'min' => '1',
|
||||
'value' => (int) $default_duration/60,
|
||||
'required' => true));
|
||||
$options = array('1' => get_vocab('all_day'));
|
||||
$checked = ($default_duration_all_day) ? '1' : null;
|
||||
$checkbox_group = new FieldInputCheckboxGroup();
|
||||
$checkbox_group->addCheckboxOptions($options, 'area_def_duration_all_day', $checked, true);
|
||||
$field->addElement($checkbox_group);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Last slot start
|
||||
// The contents of this field will be overwritten by JavaScript if enabled. The JavaScript version is a drop-down
|
||||
// select input with options limited to those times for the last slot start that are valid. The options are
|
||||
// dynamically regenerated if the start of the first slot or the resolution change. The code below is
|
||||
// therefore an alternative for non-JavaScript browsers.
|
||||
$field = new FieldInputTime();
|
||||
$value = sprintf('%02d:%02d', $eveningends, $eveningends_minutes);
|
||||
$field->setAttributes(array('id' => 'last_slot',
|
||||
'class' => 'js_hidden'))
|
||||
->setLabel(get_vocab('area_last_slot_start'))
|
||||
->setControlAttributes(array('id' => 'area_start_last_slot',
|
||||
'name' => 'area_start_last_slot',
|
||||
'value' => $value,
|
||||
'required' => true));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_periods() : ElementFieldset
|
||||
{
|
||||
global $enable_periods, $area;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->setAttribute('id', 'period_settings');
|
||||
|
||||
// If we're using JavaScript, don't display the periods settings section
|
||||
// if we're using rimes (the JavaScript will display it if we change)
|
||||
if (!$enable_periods)
|
||||
{
|
||||
$fieldset->setAttribute('class', 'js_none');
|
||||
}
|
||||
$fieldset->addLegend(get_vocab('period_settings'));
|
||||
|
||||
$this_area_periods = Periods::getForArea($area);
|
||||
// For the JavaScript to work, and MRBS to make sense, there has to be at least
|
||||
// one period defined. So if for some reason, which shouldn't happen, there aren't
|
||||
// any periods defined, then force there to be one by creating a single period name
|
||||
// with an empty string. Because the input is a required input, then it will have
|
||||
// to be saved with a period name.
|
||||
if (empty($this_area_periods))
|
||||
{
|
||||
$this_area_periods = [new Period('')];
|
||||
$using_period_times = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
$using_period_times = isset($this_area_periods->current()->start);
|
||||
}
|
||||
|
||||
// TODO: Store use_period_times in the database, so that the times are not lost.
|
||||
// TODO: At the moment it's just used by the JavaScript to toggle the display.
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('use_period_times'))
|
||||
->setControlAttribute('name', 'use_period_times')
|
||||
->setControlChecked($using_period_times);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
foreach ($this_area_periods as $period)
|
||||
{
|
||||
// The period name
|
||||
$field = new FieldInputText();
|
||||
|
||||
// The period times
|
||||
$period_times = new ElementDiv();
|
||||
$period_times->setAttribute('class', 'period_times');
|
||||
|
||||
// The period start time
|
||||
$start = new ElementInputTime();
|
||||
$start->setAttributes(['name' => 'period_starts[]', 'required' => true]);
|
||||
if (isset($period->start))
|
||||
{
|
||||
$start->setAttribute('value', $period->start);
|
||||
}
|
||||
// A separator; CSS will fill its content.
|
||||
$separator = new ElementSpan();
|
||||
$separator->setAttribute('class', 'period_separator');
|
||||
// The period end time
|
||||
$end = new ElementInputTime();
|
||||
$end->setAttributes(['name' => 'period_ends[]', 'required' => true]);
|
||||
if (isset($period->end))
|
||||
{
|
||||
$end->setAttribute('value', $period->end);
|
||||
}
|
||||
|
||||
$period_times->addElement($start);
|
||||
$period_times->addElement($separator);
|
||||
$period_times->addElement($end);
|
||||
|
||||
// The delete button; CSS will fill its content.
|
||||
$span = new ElementSpan();
|
||||
$span->setAttribute('class', 'delete_period');
|
||||
$field->setAttribute('class', 'period_name')
|
||||
->setControlAttributes(array('name' => 'area_periods[]',
|
||||
'value' => $period->name,
|
||||
'required' => true),
|
||||
false)
|
||||
->addElement($period_times)
|
||||
->addElement($span);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
$field = new FieldButton();
|
||||
$field->setControlAttributes(array('type' => 'button',
|
||||
'id' => 'add_period'))
|
||||
->setControlText(get_vocab('add_period'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_create_ahead() : ElementFieldset
|
||||
{
|
||||
global $min_create_ahead_secs, $max_create_ahead_secs,
|
||||
$min_create_ahead_enabled, $max_create_ahead_enabled;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('booking_creation'));
|
||||
|
||||
// Minimum create ahead
|
||||
$param_names = array(
|
||||
'enabler' => 'area_min_create_ahead_enabled',
|
||||
'quantity' => 'area_min_create_ahead_value',
|
||||
'units' => 'area_min_create_ahead_units',
|
||||
);
|
||||
$field = new FieldTimeWithUnits($param_names, $min_create_ahead_enabled, $min_create_ahead_secs);
|
||||
$field->setLabel(get_vocab('min_book_ahead'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Maximum create ahead
|
||||
// TODO: add some kind of note about exempt types
|
||||
$param_names = array(
|
||||
'enabler' => 'area_max_create_ahead_enabled',
|
||||
'quantity' => 'area_max_create_ahead_value',
|
||||
'units' => 'area_max_create_ahead_units',
|
||||
);
|
||||
$field = new FieldTimeWithUnits($param_names, $max_create_ahead_enabled, $max_create_ahead_secs);
|
||||
$field->setLabel(get_vocab('max_book_ahead'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_delete_ahead() : ElementFieldset
|
||||
{
|
||||
global $min_delete_ahead_secs, $max_delete_ahead_secs,
|
||||
$min_delete_ahead_enabled, $max_delete_ahead_enabled;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('booking_deletion'));
|
||||
|
||||
// Minimum delete ahead
|
||||
$param_names = array(
|
||||
'enabler' => 'area_min_delete_ahead_enabled',
|
||||
'quantity' => 'area_min_delete_ahead_value',
|
||||
'units' => 'area_min_delete_ahead_units',
|
||||
);
|
||||
$field = new FieldTimeWithUnits($param_names, $min_delete_ahead_enabled, $min_delete_ahead_secs);
|
||||
$field->setLabel(get_vocab('min_book_ahead'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Maximum delete ahead
|
||||
$param_names = array(
|
||||
'enabler' => 'area_max_delete_ahead_enabled',
|
||||
'quantity' => 'area_max_delete_ahead_value',
|
||||
'units' => 'area_max_delete_ahead_units',
|
||||
);
|
||||
$field = new FieldTimeWithUnits($param_names, $max_delete_ahead_enabled, $max_delete_ahead_secs);
|
||||
$field->setLabel(get_vocab('max_book_ahead'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_max_number() : ElementFieldset
|
||||
{
|
||||
global $interval_types,
|
||||
$max_per_interval_area_enabled, $max_per_interval_global_enabled,
|
||||
$max_per_interval_area, $max_per_interval_global;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->setAttribute('class', 'max_limits')
|
||||
->addLegend(get_vocab('booking_limits'));
|
||||
|
||||
// Add the column headings
|
||||
$field = new FieldDiv;
|
||||
|
||||
$span_area = new ElementSpan();
|
||||
$span_area->setText(get_vocab('this_area'));
|
||||
|
||||
$span_global = new ElementSpan();
|
||||
$span_global->setAttribute('title', get_vocab('whole_system_note'))
|
||||
->setText(get_vocab('whole_system'));
|
||||
|
||||
$field->addControlElement($span_area)
|
||||
->addControlElement($span_global);
|
||||
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Then do the individual settings
|
||||
foreach ($interval_types as $interval_type)
|
||||
{
|
||||
$field = new FieldDiv;
|
||||
|
||||
$checkbox_area = new ElementInputCheckbox();
|
||||
$checkbox_area->setAttributes(array('name' => "area_max_per_{$interval_type}_enabled",
|
||||
'id' => "area_max_per_{$interval_type}_enabled",
|
||||
'class' => 'enabler'))
|
||||
->setChecked($max_per_interval_area_enabled[$interval_type]);
|
||||
|
||||
$number_area = new ElementInputNumber();
|
||||
$number_area->setAttributes(array('min' => '0',
|
||||
'name' => "area_max_per_$interval_type",
|
||||
'value' => $max_per_interval_area[$interval_type]));
|
||||
|
||||
// Wrap the area and global controls in <div>s. It'll make the CSS easier.
|
||||
$div_area = new ElementDiv();
|
||||
$div_area->addElement($checkbox_area)
|
||||
->addElement($number_area);
|
||||
|
||||
// The global settings can't be changed here: they are just shown for information. The global
|
||||
// settings have to be changed in the config file.
|
||||
$checkbox_global = new ElementInputCheckbox();
|
||||
$checkbox_global->setAttributes(array('disabled' => true))
|
||||
->setChecked($max_per_interval_global_enabled[$interval_type]);
|
||||
|
||||
$number_global = new ElementInputNumber();
|
||||
$number_global->setAttributes(array('value' => $max_per_interval_global[$interval_type],
|
||||
'disabled' => true));
|
||||
|
||||
$div_global = new ElementDiv();
|
||||
$div_global->addElement($checkbox_global)
|
||||
->addElement($number_global);
|
||||
|
||||
$field->setLabel(get_vocab("max_per_$interval_type"))
|
||||
->addControlElement($div_area)
|
||||
->addControlElement($div_global);
|
||||
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_max_secs() : ElementFieldset
|
||||
{
|
||||
global $interval_types,
|
||||
$max_secs_per_interval_area_enabled, $max_secs_per_interval_global_enabled,
|
||||
$max_secs_per_interval_area, $max_secs_per_interval_global;
|
||||
|
||||
// Limit the units to 'hours' because 'days' can confuse the user. That's because
|
||||
// the policy check only checks for time used during the booking 'day', ie between
|
||||
// the start of the first slot and the end of the last slot, which is normally less
|
||||
// than 24 hours. So a 'day' would be 24 hours, not a booking 'day'.
|
||||
$max_unit = 'hours';
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->setAttribute('class', 'max_limits')
|
||||
->addLegend(get_vocab('booking_limits_secs'));
|
||||
|
||||
// Add the column headings
|
||||
$field = new FieldDiv;
|
||||
|
||||
$span_area = new ElementSpan();
|
||||
$span_area->setText(get_vocab('this_area'));
|
||||
|
||||
$span_global = new ElementSpan();
|
||||
$span_global->setAttribute('title', get_vocab('whole_system_note'))
|
||||
->setText(get_vocab('whole_system'));
|
||||
|
||||
$field->addControlElement($span_area)
|
||||
->addControlElement($span_global);
|
||||
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Then do the individual settings
|
||||
foreach ($interval_types as $interval_type)
|
||||
{
|
||||
$field = new FieldDiv;
|
||||
|
||||
$checkbox_area = new ElementInputCheckbox();
|
||||
$checkbox_area->setAttributes(array('name' => "area_max_secs_per_{$interval_type}_enabled",
|
||||
'id' => "area_max_secs_per_{$interval_type}_enabled",
|
||||
'class' => 'enabler'))
|
||||
->setChecked($max_secs_per_interval_area_enabled[$interval_type]);
|
||||
|
||||
$max = $max_secs_per_interval_area[$interval_type];
|
||||
toTimeString($max, $units, true, $max_unit);
|
||||
$options = Form::getTimeUnitOptions($max_unit);
|
||||
|
||||
$select = new ElementSelect();
|
||||
$select->setAttribute('name', "area_max_secs_per_{$interval_type}_units")
|
||||
->addSelectOptions($options, array_search($units, $options), true);
|
||||
|
||||
$time_area = new ElementInputNumber();
|
||||
$time_area->setAttributes(array('min' => '0',
|
||||
'name' => "area_max_secs_per_$interval_type",
|
||||
'value' => $max));
|
||||
|
||||
// Wrap the area and global controls in <div>s. It'll make the CSS easier.
|
||||
$div_area = new ElementDiv();
|
||||
$div_area->addElement($checkbox_area)
|
||||
->addElement($time_area)
|
||||
->addElement($select);
|
||||
|
||||
// The global settings can't be changed here: they are just shown for information. The global
|
||||
// settings have to be changed in the config file.
|
||||
$checkbox_global = new ElementInputCheckbox();
|
||||
$checkbox_global->setAttributes(array('disabled' => true))
|
||||
->setChecked($max_secs_per_interval_global_enabled[$interval_type]);
|
||||
|
||||
$max = $max_secs_per_interval_global[$interval_type];
|
||||
toTimeString($max, $units, true, $max_unit);
|
||||
|
||||
$time_global = new ElementInputNumber();
|
||||
$time_global->setAttributes(array('value' => $max,
|
||||
'disabled' => true));
|
||||
|
||||
$select = new ElementSelect();
|
||||
$select->setAttribute('disabled', true)
|
||||
->addSelectOptions($options, array_search($units, $options), true);
|
||||
|
||||
$div_global = new ElementDiv();
|
||||
$div_global->addElement($checkbox_global)
|
||||
->addElement($time_global)
|
||||
->addElement($select);
|
||||
|
||||
$field->setLabel(get_vocab("max_secs_per_$interval_type"))
|
||||
->addControlElement($div_area)
|
||||
->addControlElement($div_global);
|
||||
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_max_duration() : ElementFieldset
|
||||
{
|
||||
global $max_duration_enabled, $max_duration_secs, $max_duration_periods;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('booking_durations'));
|
||||
|
||||
// Enable checkbox
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('max_duration'))
|
||||
->setControlAttributes(array('name' => 'area_max_duration_enabled',
|
||||
'class' => 'enabler'))
|
||||
->setChecked($max_duration_enabled);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Periods
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('mode_periods'))
|
||||
->setControlAttributes(array('name' => 'area_max_duration_periods',
|
||||
'value' => $max_duration_periods,
|
||||
'min' => '0'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Times
|
||||
$max_duration_value = $max_duration_secs;
|
||||
toTimeString($max_duration_value, $max_duration_units);
|
||||
$options = Form::getTimeUnitOptions();
|
||||
|
||||
$select = new ElementSelect();
|
||||
$select->setAttribute('name', 'area_max_duration_units')
|
||||
->addSelectOptions($options, array_search($max_duration_units, $options), true);
|
||||
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('mode_times'))
|
||||
->setControlAttributes(array('name' => 'area_max_duration_value',
|
||||
'value' => $max_duration_value,
|
||||
'min' => '0'))
|
||||
->addElement($select);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_booking_policies() : ElementFieldset
|
||||
{
|
||||
global $enable_periods;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->setAttribute('id', 'booking_policies')
|
||||
->addLegend(get_vocab('booking_policies'));
|
||||
|
||||
// Note when using periods
|
||||
$field = new FieldSpan();
|
||||
if (!$enable_periods)
|
||||
{
|
||||
$field->setAttribute('class', 'js_none');
|
||||
}
|
||||
$field->setAttribute('id', 'book_ahead_periods_note')
|
||||
->setControlText(get_vocab('book_ahead_note_periods'));
|
||||
|
||||
$fieldset->addElement($field)
|
||||
->addElement(get_fieldset_create_ahead())
|
||||
->addElement(get_fieldset_delete_ahead())
|
||||
->addElement(get_fieldset_max_number())
|
||||
->addElement(get_fieldset_max_secs())
|
||||
->addElement(get_fieldset_max_duration());
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_confirmation_settings() : ElementFieldset
|
||||
{
|
||||
global $confirmation_enabled, $confirmed_default;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('confirmation_settings'));
|
||||
|
||||
// Confirmation enabled
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('allow_confirmation'))
|
||||
->setControlAttribute('name', 'area_confirmation_enabled')
|
||||
->setChecked($confirmation_enabled);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Default settings
|
||||
$options = array('1' => get_vocab('default_confirmed'),
|
||||
'0' => get_vocab('default_tentative'));
|
||||
$value = ($confirmed_default) ? '1' : '0';
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setLabel(get_vocab('default_settings_conf'))
|
||||
->addRadioOptions($options, 'area_confirmed_default', $value, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_approval_settings() : ElementFieldset
|
||||
{
|
||||
global $approval_enabled, $reminders_enabled;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('approval_settings'));
|
||||
|
||||
// Approval enabled
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('enable_approval'))
|
||||
->setControlAttribute('name', 'area_approval_enabled')
|
||||
->setChecked($approval_enabled);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Reminders enabled
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('enable_reminders'))
|
||||
->setControlAttribute('name', 'area_reminders_enabled')
|
||||
->setChecked($reminders_enabled);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_privacy_settings() : ElementFieldset
|
||||
{
|
||||
global $private_enabled, $private_mandatory, $private_default;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('private_settings'));
|
||||
|
||||
// Private enabled
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('allow_private'))
|
||||
->setControlAttribute('name', 'area_private_enabled')
|
||||
->setChecked($private_enabled);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Private mandatory
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel(get_vocab('force_private'))
|
||||
->setControlAttribute('name', 'area_private_mandatory')
|
||||
->setChecked($private_mandatory);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Default settings
|
||||
$options = array('1' => get_vocab('default_private'),
|
||||
'0' => get_vocab('default_public'));
|
||||
$value = ($private_default) ? '1' : '0';
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setLabel(get_vocab('default_settings'))
|
||||
->addRadioOptions($options, 'area_private_default', $value, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_privacy_display() : ElementFieldset
|
||||
{
|
||||
global $private_override;
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('private_display'));
|
||||
|
||||
$options = array('none' => get_vocab('treat_respect'),
|
||||
'private' => get_vocab('treat_private'),
|
||||
'public' => get_vocab('treat_public'));
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setLabel(get_vocab('private_display_label'))
|
||||
->addLabelClass('no_suffix')
|
||||
->setLabelAttribute('title', get_vocab('private_display_caution'))
|
||||
->setAttribute('class', 'multiline')
|
||||
->addControlClass('long')
|
||||
->addRadioOptions($options, 'area_private_override', $private_override, true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_submit_buttons() : ElementFieldset
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
|
||||
// The back and submit buttons
|
||||
$field = new FieldInputSubmit();
|
||||
|
||||
$back = new ElementInputSubmit();
|
||||
$back->setAttributes(array('value' => get_vocab('back'),
|
||||
'formaction' => multisite('admin.php')));
|
||||
$field->addLabelClass('no_suffix')
|
||||
->addLabelElement($back)
|
||||
->setControlAttribute('value', get_vocab('save'));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area ?? null,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Get the details for this area
|
||||
if (!isset($area) || is_null($data = get_area_details($area)))
|
||||
{
|
||||
Errors::fatalError(get_vocab('invalid_area'));
|
||||
}
|
||||
|
||||
$errors = get_form_var('errors', 'array');
|
||||
|
||||
// Generate the form
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('id' => 'edit_area',
|
||||
'class' => 'standard',
|
||||
'action' => multisite('edit_area_handler.php'));
|
||||
|
||||
$form->setAttributes($attributes)
|
||||
->addHiddenInput('area', $area);
|
||||
|
||||
$outer_fieldset = new ElementFieldset();
|
||||
|
||||
$outer_fieldset->addLegend(get_vocab('editarea'))
|
||||
->addElement(get_fieldset_errors($errors))
|
||||
->addElement(get_fieldset_general($data))
|
||||
->addElement(get_fieldset_times())
|
||||
->addElement(get_fieldset_periods())
|
||||
->addElement(get_fieldset_booking_policies())
|
||||
->addElement(get_fieldset_confirmation_settings())
|
||||
->addElement(get_fieldset_approval_settings())
|
||||
->addElement(get_fieldset_privacy_settings())
|
||||
->addElement(get_fieldset_privacy_display())
|
||||
->addElement(get_fieldset_submit_buttons());
|
||||
|
||||
$form->addElement($outer_fieldset);
|
||||
|
||||
$form->render();
|
||||
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,416 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
// Check the CSRF token.
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
|
||||
// Get non-standard form variables
|
||||
$form_vars = array(
|
||||
'sort_key' => 'string',
|
||||
'area_name' => 'string',
|
||||
'area_disabled' => 'string',
|
||||
'area_timezone' => 'string',
|
||||
'area_admin_email' => 'string',
|
||||
'area_start_first_slot' => 'string',
|
||||
'area_start_last_slot' => 'string',
|
||||
'area_res_mins' => 'int',
|
||||
'area_def_duration_mins' => 'int',
|
||||
'area_def_duration_all_day' => 'string',
|
||||
'area_min_create_ahead_enabled' => 'string',
|
||||
'area_min_create_ahead_value' => 'int',
|
||||
'area_min_create_ahead_units' => 'string',
|
||||
'area_max_create_ahead_enabled' => 'string',
|
||||
'area_max_create_ahead_value' => 'int',
|
||||
'area_max_create_ahead_units' => 'string',
|
||||
'area_min_delete_ahead_enabled' => 'string',
|
||||
'area_min_delete_ahead_value' => 'int',
|
||||
'area_min_delete_ahead_units' => 'string',
|
||||
'area_max_delete_ahead_enabled' => 'string',
|
||||
'area_max_delete_ahead_value' => 'int',
|
||||
'area_max_delete_ahead_units' => 'string',
|
||||
'area_max_duration_enabled' => 'string',
|
||||
'area_max_duration_periods' => 'int',
|
||||
'area_max_duration_value' => 'int',
|
||||
'area_max_duration_units' => 'string',
|
||||
'area_private_enabled' => 'string',
|
||||
'area_private_default' => 'int',
|
||||
'area_private_mandatory' => 'string',
|
||||
'area_private_override' => 'string',
|
||||
'area_approval_enabled' => 'string',
|
||||
'area_reminders_enabled' => 'string',
|
||||
'area_enable_periods' => 'string',
|
||||
'area_periods' => 'array',
|
||||
'area_confirmation_enabled' => 'string',
|
||||
'area_confirmed_default' => 'string',
|
||||
'area_default_type' => 'string',
|
||||
'area_times_along_top' => 'string',
|
||||
'custom_html' => 'string',
|
||||
'period_starts' => 'array',
|
||||
'period_ends' => 'array'
|
||||
);
|
||||
|
||||
foreach($form_vars as $var => $var_type)
|
||||
{
|
||||
$$var = get_form_var($var, $var_type);
|
||||
|
||||
// Trim the strings and truncate them to the maximum field length
|
||||
if (is_string($$var))
|
||||
{
|
||||
$$var = trim($$var);
|
||||
$$var = truncate($$var, "area.$var");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!isset($area_default_type))
|
||||
{
|
||||
$area_default_type = $area_defaults['default_type'];
|
||||
}
|
||||
|
||||
// Get the max_per_interval form variables
|
||||
foreach ($interval_types as $interval_type)
|
||||
{
|
||||
$var = "area_max_per_$interval_type";
|
||||
$$var = get_form_var($var, 'int');
|
||||
$var = "area_max_per_{$interval_type}_enabled";
|
||||
$$var = get_form_var($var, 'string');
|
||||
$var = "area_max_secs_per_$interval_type";
|
||||
$$var = get_form_var($var, 'int');
|
||||
$var = "area_max_secs_per_{$interval_type}_units";
|
||||
$$var = get_form_var($var, 'string');
|
||||
$var = "area_max_secs_per_{$interval_type}_enabled";
|
||||
$$var = get_form_var($var, 'string');
|
||||
}
|
||||
|
||||
// UPDATE THE DATABASE
|
||||
// -------------------
|
||||
|
||||
if (empty($area))
|
||||
{
|
||||
throw new \Exception('$area is empty');
|
||||
}
|
||||
|
||||
// Initialise the error array
|
||||
$errors = array();
|
||||
|
||||
// Check the name hasn't been used in another area
|
||||
$id = get_area_id($area_name);
|
||||
if (isset($id) && ($id != $area))
|
||||
{
|
||||
$errors[] = 'invalid_area_name';
|
||||
}
|
||||
|
||||
// Clean up the address list replacing newlines by commas and removing duplicates
|
||||
$area_admin_email = clean_address_list($area_admin_email);
|
||||
// Validate email addresses
|
||||
if (!validate_email_list($area_admin_email))
|
||||
{
|
||||
$errors[] = 'invalid_email';
|
||||
}
|
||||
|
||||
// Check that the time formats are correct (hh:mm). They should be, because
|
||||
// the HTML5 element or polyfill will force them to be, but just in case ...
|
||||
// (for example if we are relying on a polyfill and JavaScript is disabled)
|
||||
|
||||
if (!preg_match(REGEX_HHMM, $area_start_first_slot) ||
|
||||
!preg_match(REGEX_HHMM, $area_start_last_slot))
|
||||
{
|
||||
$errors[] = 'invalid_time_format';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get morningstarts and eveningends
|
||||
list($area_morningstarts, $area_morningstarts_minutes) = explode(':', $area_start_first_slot);
|
||||
list($area_eveningends, $area_eveningends_minutes) = explode(':', $area_start_last_slot);
|
||||
|
||||
// Convert the book ahead times into seconds
|
||||
fromTimeString($area_min_create_ahead_value, $area_min_create_ahead_units);
|
||||
fromTimeString($area_max_create_ahead_value, $area_max_create_ahead_units);
|
||||
fromTimeString($area_min_delete_ahead_value, $area_min_delete_ahead_units);
|
||||
fromTimeString($area_max_delete_ahead_value, $area_max_delete_ahead_units);
|
||||
|
||||
fromTimeString($area_max_duration_value, $area_max_duration_units);
|
||||
|
||||
// If we are using periods, round these down to the nearest whole day
|
||||
// (anything less than a day is meaningless when using periods)
|
||||
if ($area_enable_periods)
|
||||
{
|
||||
$vars = array('area_min_create_ahead_value',
|
||||
'area_max_create_ahead_value',
|
||||
'area_min_delete_ahead_value',
|
||||
'area_max_delete_ahead_value');
|
||||
|
||||
foreach ($vars as $var)
|
||||
{
|
||||
if (isset($$var))
|
||||
{
|
||||
$$var -= $$var % SECONDS_PER_DAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert booleans into 0/1 (necessary for PostgreSQL)
|
||||
$vars = array(
|
||||
'area_disabled',
|
||||
'area_def_duration_all_day',
|
||||
'area_min_create_ahead_enabled',
|
||||
'area_max_create_ahead_enabled',
|
||||
'area_min_delete_ahead_enabled',
|
||||
'area_max_delete_ahead_enabled',
|
||||
'area_max_duration_enabled',
|
||||
'area_private_enabled',
|
||||
'area_private_default',
|
||||
'area_private_mandatory',
|
||||
'area_approval_enabled',
|
||||
'area_reminders_enabled',
|
||||
'area_enable_periods',
|
||||
'area_confirmation_enabled',
|
||||
'area_confirmed_default',
|
||||
'area_times_along_top'
|
||||
);
|
||||
|
||||
foreach ($interval_types as $interval_type)
|
||||
{
|
||||
$vars[] = "area_max_per_{$interval_type}_enabled";
|
||||
$vars[] = "area_max_secs_per_{$interval_type}_enabled";
|
||||
}
|
||||
|
||||
foreach ($vars as $var)
|
||||
{
|
||||
$$var = (!empty($$var)) ? 1 : 0;
|
||||
}
|
||||
|
||||
// TODO: This is a kludge until we store use_period_times in the database.
|
||||
// We need to make sure that the period start times correspond to the correct periods.
|
||||
if (count($area_periods) == count($period_starts) + 1)
|
||||
{
|
||||
array_unshift($period_starts, null);
|
||||
}
|
||||
// Assemble the periods as an object.
|
||||
$periods_tmp = new Periods($area);
|
||||
for ($i = 0; $i < count($area_periods); $i++)
|
||||
{
|
||||
$periods_tmp->add(new Period(
|
||||
$area_periods[$i],
|
||||
$period_starts[$i] ?? null,
|
||||
$period_ends[$i] ?? null
|
||||
));
|
||||
}
|
||||
|
||||
// Validate the periods, but only if we are using periods with period times.
|
||||
if ($area_enable_periods && isset($period_starts[0]) && (true !== ($result = $periods_tmp->validate())))
|
||||
{
|
||||
$errors[] = $result;
|
||||
}
|
||||
|
||||
// Convert the periods to a value suitable for the database.
|
||||
$area_periods = $periods_tmp->toDbValue();
|
||||
|
||||
// Validate times mode settings
|
||||
if (!$area_enable_periods)
|
||||
{
|
||||
// Avoid divide by zero errors
|
||||
if ($area_res_mins == 0)
|
||||
{
|
||||
$errors[] = 'invalid_resolution';
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check morningstarts, eveningends, and resolution for consistency
|
||||
$start_first_slot = ($area_morningstarts*60) + $area_morningstarts_minutes; // minutes
|
||||
$start_last_slot = ($area_eveningends*60) + $area_eveningends_minutes; // minutes
|
||||
|
||||
// If eveningends is before morningstarts then it's really on the next day
|
||||
if (hm_before(array('hours' => $area_eveningends, 'minutes' => $area_eveningends_minutes),
|
||||
array('hours' => $area_morningstarts, 'minutes' => $area_morningstarts_minutes)))
|
||||
{
|
||||
$start_last_slot += MINUTES_PER_DAY;
|
||||
}
|
||||
|
||||
$start_difference = ($start_last_slot - $start_first_slot); // minutes
|
||||
|
||||
if ($start_difference%$area_res_mins != 0)
|
||||
{
|
||||
$errors[] = 'invalid_resolution';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Errors in the form data - go back to the form
|
||||
if (!empty($errors))
|
||||
{
|
||||
$query_string = "area=$area";
|
||||
foreach ($errors as $error)
|
||||
{
|
||||
$query_string .= "&errors[]=$error";
|
||||
}
|
||||
location_header("edit_area.php?$query_string");
|
||||
}
|
||||
|
||||
// Everything is OK, update the database
|
||||
|
||||
$sql = "UPDATE " . _tbl('area') . " SET ";
|
||||
$sql_params = array();
|
||||
$assign_array = array();
|
||||
$assign_array[] = "area_name=?";
|
||||
$sql_params[] = $area_name;
|
||||
$assign_array[] = "sort_key=?";
|
||||
$sql_params[] = $sort_key;
|
||||
$assign_array[] = "disabled=?";
|
||||
$sql_params[] = $area_disabled;
|
||||
$assign_array[] = "timezone=?";
|
||||
$sql_params[] = $area_timezone;
|
||||
$assign_array[] = "area_admin_email=?";
|
||||
$sql_params[] = $area_admin_email;
|
||||
|
||||
if (isset($custom_html))
|
||||
{
|
||||
// The custom HTML field won't be present if it has been
|
||||
// disabled in the config file
|
||||
$assign_array[] = "custom_html=?";
|
||||
$sql_params[] = $custom_html;
|
||||
}
|
||||
|
||||
if (!$area_enable_periods)
|
||||
{
|
||||
$assign_array[] = "resolution=?";
|
||||
$sql_params[] = $area_res_mins * 60;
|
||||
if (!$area_def_duration_all_day)
|
||||
{
|
||||
// If the default duration is all day, then this value will have
|
||||
// been disabled on the form, so don't change it.
|
||||
$assign_array[] = "default_duration=?";
|
||||
$sql_params[] = $area_def_duration_mins * 60;
|
||||
}
|
||||
$assign_array[] = "default_duration_all_day=?";
|
||||
$sql_params[] = $area_def_duration_all_day;
|
||||
$assign_array[] = "morningstarts=?";
|
||||
$sql_params[] = $area_morningstarts;
|
||||
$assign_array[] = "morningstarts_minutes=?";
|
||||
$sql_params[] = $area_morningstarts_minutes;
|
||||
$assign_array[] = "eveningends=?";
|
||||
$sql_params[] = $area_eveningends;
|
||||
$assign_array[] = "eveningends_minutes=?";
|
||||
$sql_params[] = $area_eveningends_minutes;
|
||||
}
|
||||
|
||||
// only update the min and max *_ahead_secs fields if the form values
|
||||
// are set; they might be NULL because they've been disabled by JavaScript
|
||||
$assign_array[] = "min_create_ahead_enabled=?";
|
||||
$sql_params[] = $area_min_create_ahead_enabled;
|
||||
$assign_array[] = "max_create_ahead_enabled=?";
|
||||
$sql_params[] = $area_max_create_ahead_enabled;
|
||||
$assign_array[] = "min_delete_ahead_enabled=?";
|
||||
$sql_params[] = $area_min_delete_ahead_enabled;
|
||||
$assign_array[] = "max_delete_ahead_enabled=?";
|
||||
$sql_params[] = $area_max_delete_ahead_enabled;
|
||||
$assign_array[] = "max_duration_enabled=?";
|
||||
$sql_params[] = $area_max_duration_enabled;
|
||||
|
||||
if (isset($area_min_create_ahead_value))
|
||||
{
|
||||
$assign_array[] = "min_create_ahead_secs=?";
|
||||
$sql_params[] = $area_min_create_ahead_value;
|
||||
}
|
||||
if (isset($area_max_create_ahead_value))
|
||||
{
|
||||
$assign_array[] = "max_create_ahead_secs=?";
|
||||
$sql_params[] = $area_max_create_ahead_value;
|
||||
}
|
||||
if (isset($area_min_delete_ahead_value))
|
||||
{
|
||||
$assign_array[] = "min_delete_ahead_secs=?";
|
||||
$sql_params[] = $area_min_delete_ahead_value;
|
||||
}
|
||||
if (isset($area_max_delete_ahead_value))
|
||||
{
|
||||
$assign_array[] = "max_delete_ahead_secs=?";
|
||||
$sql_params[] = $area_max_delete_ahead_value;
|
||||
}
|
||||
if (isset($area_max_duration_value))
|
||||
{
|
||||
$assign_array[] = "max_duration_secs=?";
|
||||
$sql_params[] = $area_max_duration_value;
|
||||
$assign_array[] = "max_duration_periods=?";
|
||||
$sql_params[] = $area_max_duration_periods;
|
||||
}
|
||||
|
||||
foreach($interval_types as $interval_type)
|
||||
{
|
||||
$var = "max_per_{$interval_type}_enabled";
|
||||
$area_var = "area_" . $var;
|
||||
$assign_array[] = "$var=" . $$area_var;
|
||||
|
||||
$var = "max_per_$interval_type";
|
||||
$area_var = "area_" . $var;
|
||||
if (isset($$area_var))
|
||||
{
|
||||
// only update these fields if they are set; they might be NULL because
|
||||
// they have been disabled by JavaScript
|
||||
$assign_array[] = "$var=?";
|
||||
$sql_params[] = $$area_var;
|
||||
}
|
||||
|
||||
// Now do the max_secs variables (limits on the total length of bookings)
|
||||
$var = "max_secs_per_{$interval_type}_enabled";
|
||||
$area_var = "area_" . $var;
|
||||
$assign_array[] = "$var=" . $$area_var;
|
||||
|
||||
$var = "max_secs_per_$interval_type";
|
||||
$area_var = "area_" . $var;
|
||||
|
||||
if (isset($$area_var))
|
||||
{
|
||||
// only update these fields if they are set; they might be NULL because
|
||||
// they have been disabled by JavaScript
|
||||
// Need to convert back into seconds
|
||||
$units_var = "area_max_secs_per_{$interval_type}_units";
|
||||
fromTimeString($$area_var, $$units_var);
|
||||
$assign_array[] = "$var=?";
|
||||
$sql_params[] = $$area_var;
|
||||
}
|
||||
}
|
||||
|
||||
$assign_array[] = "private_enabled=?";
|
||||
$sql_params[] = $area_private_enabled;
|
||||
$assign_array[] = "private_default=?";
|
||||
$sql_params[] = $area_private_default;
|
||||
$assign_array[] = "private_mandatory=?";
|
||||
$sql_params[] = $area_private_mandatory;
|
||||
$assign_array[] = "private_override=?";
|
||||
$sql_params[] = $area_private_override;
|
||||
$assign_array[] = "approval_enabled=?";
|
||||
$sql_params[] = $area_approval_enabled;
|
||||
$assign_array[] = "reminders_enabled=?";
|
||||
$sql_params[] = $area_reminders_enabled;
|
||||
$assign_array[] = "enable_periods=?";
|
||||
$sql_params[] = $area_enable_periods;
|
||||
$assign_array[] = "periods=?";
|
||||
$sql_params[] = $area_periods;
|
||||
$assign_array[] = "confirmation_enabled=?";
|
||||
$sql_params[] = $area_confirmation_enabled;
|
||||
$assign_array[] = "confirmed_default=?";
|
||||
$sql_params[] = $area_confirmed_default;
|
||||
$assign_array[] = "default_type=?";
|
||||
$sql_params[] = $area_default_type;
|
||||
$assign_array[] = "times_along_top=?";
|
||||
$sql_params[] = $area_times_along_top;
|
||||
|
||||
$sql .= implode(",", $assign_array) . " WHERE id=?";
|
||||
$sql_params[] = $area;
|
||||
|
||||
db()->command($sql, $sql_params);
|
||||
|
||||
|
||||
// Go back to the admin page
|
||||
location_header("admin.php?day=$day&month=$month&year=$year&area=$area");
|
||||
+1821
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,985 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require 'defaultincludes.inc';
|
||||
require_once 'mrbs_sql.inc';
|
||||
require_once 'functions_mail.inc';
|
||||
|
||||
use MRBS\Calendar\CalendarFactory;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
|
||||
function invalid_date(string $message, bool $is_ajax) : void
|
||||
{
|
||||
if ($is_ajax)
|
||||
{
|
||||
http_response_code(500);
|
||||
// Trigger the error after we have sent the 500 code so that if $debug is set the JavaScript
|
||||
// does not interpret the output as success.
|
||||
trigger_error($message, E_USER_WARNING);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception($message);
|
||||
}
|
||||
|
||||
|
||||
// Check that a room id is set and not the empty string and convert it to an int.
|
||||
function sanitize_room_id($id) : int
|
||||
{
|
||||
if (!isset($id))
|
||||
{
|
||||
throw new Exception("Room id not set");
|
||||
}
|
||||
|
||||
if ($id === '')
|
||||
{
|
||||
throw new Exception("Room id is ''");
|
||||
}
|
||||
|
||||
return intval($id);
|
||||
}
|
||||
|
||||
|
||||
function invalid_booking(string $message) : void
|
||||
{
|
||||
global $view, $view_all, $year, $month, $day, $area, $room;
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
echo "<h1>" . get_vocab('invalid_booking') . "</h1>\n";
|
||||
echo "<p>$message</p>\n";
|
||||
// Print footer and exit
|
||||
print_footer(true);
|
||||
}
|
||||
|
||||
|
||||
$is_ajax = is_ajax();
|
||||
|
||||
if ($is_ajax && !checkAuthorised(this_page(), true))
|
||||
{
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
|
||||
// (1) Check the user is authorised for this page
|
||||
// ---------------------------------------------
|
||||
checkAuthorised(this_page());
|
||||
|
||||
$mrbs_user = session()->getCurrentUser();
|
||||
$mrbs_username = (isset($mrbs_user)) ? $mrbs_user->username : null;
|
||||
|
||||
|
||||
// (2) Get the form variables
|
||||
// --------------------------
|
||||
|
||||
// NOTE: the code on this page assumes that array form variables are passed
|
||||
// as an array of values, rather than an array indexed by value. This is
|
||||
// particularly important for checkbox arrays which should be formed like this:
|
||||
//
|
||||
// <input type="checkbox" name="foo[]" value="n">
|
||||
// <input type="checkbox" name="foo[]" value="m">
|
||||
//
|
||||
// and not like this:
|
||||
//
|
||||
// <input type="checkbox" name="foo[n]" value="1">
|
||||
// <input type="checkbox" name="foo[m]" value="1">
|
||||
|
||||
|
||||
// This page can be called with an Ajax call. In this case it just checks
|
||||
// the validity of a proposed booking and does not make the booking.
|
||||
|
||||
// Get non-standard form variables
|
||||
$form_vars = array(
|
||||
'create_by' => 'string',
|
||||
'name' => 'string',
|
||||
'description' => 'string',
|
||||
'start_seconds' => 'int',
|
||||
'start_date' => 'string',
|
||||
'end_seconds' => 'int',
|
||||
'end_date' => 'string',
|
||||
'all_day' => 'string', // bool, actually
|
||||
'type' => 'string',
|
||||
'rooms' => 'array',
|
||||
'original_room_id' => 'int',
|
||||
'ical_uid' => 'string',
|
||||
'ical_sequence' => 'int',
|
||||
'ical_recur_id' => 'string',
|
||||
'allow_registration' => 'string', // bool, actually
|
||||
'registrant_limit' => 'int',
|
||||
'registrant_limit_enabled' => 'string', // bool, actually
|
||||
'registration_opens_value' => 'int',
|
||||
'registration_opens_units' => 'string',
|
||||
'registration_opens_enabled' => 'string', // bool, actually
|
||||
'registration_closes_value' => 'int',
|
||||
'registration_closes_units' => 'string',
|
||||
'registration_closes_enabled' => 'string', // bool, actually
|
||||
'returl' => 'url_local',
|
||||
'id' => 'int',
|
||||
'rep_id' => 'int',
|
||||
'edit_series' => 'bool',
|
||||
'rep_type' => 'int',
|
||||
'rep_end_date' => 'string',
|
||||
'rep_day' => 'array', // array of bools
|
||||
'rep_interval' => 'int',
|
||||
'month_type' => 'int',
|
||||
'month_absolute' => 'int',
|
||||
'month_relative_ord' => 'string',
|
||||
'month_relative_day' => 'string',
|
||||
'skip' => 'bool',
|
||||
'no_mail' => 'bool',
|
||||
'private' => 'string', // bool, actually
|
||||
'confirmed' => 'string',
|
||||
'back_button' => 'string',
|
||||
'timetohighlight' => 'int',
|
||||
'commit' => 'string'
|
||||
);
|
||||
|
||||
foreach($form_vars as $var => $var_type)
|
||||
{
|
||||
$$var = get_form_var($var, $var_type);
|
||||
|
||||
// Trim the strings and truncate them to the maximum field length
|
||||
if (is_string($$var))
|
||||
{
|
||||
$$var = trim($$var);
|
||||
$$var = truncate($$var, "entry.$var");
|
||||
}
|
||||
}
|
||||
|
||||
// Provide a default for $rep_interval (it could be null in an Ajax post request
|
||||
// if the user has an empty string in the input).
|
||||
if (!isset($rep_interval))
|
||||
{
|
||||
$rep_interval = 1;
|
||||
}
|
||||
|
||||
// Sanitize the room ids
|
||||
$rooms = array_map(__NAMESPACE__ . '\sanitize_room_id', $rooms);
|
||||
|
||||
// Convert the registration opens and closes times into seconds
|
||||
if (isset($registration_opens_value) && isset($registration_opens_units))
|
||||
{
|
||||
$registration_opens = $registration_opens_value;
|
||||
fromTimeString($registration_opens, $registration_opens_units);
|
||||
$registration_opens = constrain_int($registration_opens, 4);
|
||||
}
|
||||
|
||||
if (isset($registration_closes_value) && isset($registration_closes_units))
|
||||
{
|
||||
$registration_closes = $registration_closes_value;
|
||||
fromTimeString($registration_closes, $registration_closes_units);
|
||||
$registration_closes = constrain_int($registration_closes, 4);
|
||||
}
|
||||
|
||||
if (!$is_ajax)
|
||||
{
|
||||
// Convert the database booleans (the custom field booleans are done later)
|
||||
foreach (['allow_registration', 'registrant_limit_enabled', 'registration_opens_enabled', 'registration_closes_enabled'] as $var)
|
||||
{
|
||||
$$var = ($$var) ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If they're not an admin and multi-day bookings are not allowed, then
|
||||
// set the end date to the start date
|
||||
if (!is_book_admin($rooms) && $auth['only_admin_can_book_multiday'])
|
||||
{
|
||||
$end_date = $start_date;
|
||||
}
|
||||
|
||||
if (false === ($start_date_split = split_iso_date($start_date)))
|
||||
{
|
||||
invalid_date("Invalid start_date '$start_date'", $is_ajax);
|
||||
}
|
||||
list($start_year, $start_month, $start_day) = $start_date_split;
|
||||
|
||||
if (false === ($end_date_split = split_iso_date($end_date)))
|
||||
{
|
||||
invalid_date("Invalid end_date '$end_date'", $is_ajax);
|
||||
}
|
||||
list($end_year, $end_month, $end_day) = $end_date_split;
|
||||
|
||||
|
||||
// BACK: we didn't really want to be here - send them to the returl
|
||||
if (!empty($back_button))
|
||||
{
|
||||
if (empty($returl))
|
||||
{
|
||||
$returl = "index.php";
|
||||
}
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
// Get custom form variables
|
||||
$custom_fields = array();
|
||||
|
||||
// Get the information about the fields in the entry table
|
||||
$fields = db()->field_info(_tbl('entry'));
|
||||
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $standard_fields['entry']))
|
||||
{
|
||||
$f_type = get_form_var_type($field);
|
||||
$var = VAR_PREFIX . $field['name'];
|
||||
$custom_fields[$field['name']] = get_form_var($var, $f_type);
|
||||
|
||||
// Trim any strings and truncate them to the maximum field length
|
||||
if (is_string($custom_fields[$field['name']]) && ($field['nature'] != 'decimal'))
|
||||
{
|
||||
$custom_fields[$field['name']] = trim($custom_fields[$field['name']]);
|
||||
$custom_fields[$field['name']] = truncate($custom_fields[$field['name']], 'entry.' . $field['name']);
|
||||
}
|
||||
|
||||
// For certain nullable data types convert empty strings into null values, to prevent an SQL error
|
||||
if (in_array($field['nature'], ['decimal', 'timestamp']))
|
||||
{
|
||||
if ($field['is_nullable'] && (!isset($custom_fields[$field['name']]) || ($custom_fields[$field['name']] === '')))
|
||||
{
|
||||
$custom_fields[$field['name']] = null;
|
||||
}
|
||||
// TODO: Validate that the fields are valid for the SQL input before they trigger an SQL error?
|
||||
// TODO: Rewrite all of this so that there is common form validation and processing for all forms.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (3) Clean up the form variables
|
||||
// -------------------------------
|
||||
|
||||
// Form validation checks. Normally checked for client side.
|
||||
|
||||
// Validate the create_by variable, checking that it's the current user, unless the
|
||||
// user is an admin and the booking is being edited or it's a new booking and we allow
|
||||
// admins to make bookings on behalf of others.
|
||||
//
|
||||
// Only carry out this check if it's not an Ajax request. If it is an Ajax request then
|
||||
// $create_by isn't set yet, but a getWritable check will be done later,
|
||||
if (!$is_ajax)
|
||||
{
|
||||
if (!isset($create_by))
|
||||
{
|
||||
// Shouldn't happen, unless something's gone wrong with the form or the POST request.
|
||||
throw new Exception('$create_by not set');
|
||||
}
|
||||
if (!is_book_admin($rooms) || (!isset($id) && $auth['admin_can_only_book_for_self']))
|
||||
{
|
||||
if ($create_by !== $mrbs_username)
|
||||
{
|
||||
$message = "Attempt made by user '$mrbs_username' to make a booking in the name of '$create_by'";
|
||||
trigger_error($message, E_USER_NOTICE);
|
||||
$create_by = $mrbs_username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($rooms))
|
||||
{
|
||||
if (!$is_ajax)
|
||||
{
|
||||
invalid_booking(get_vocab('no_rooms_selected'));
|
||||
}
|
||||
if ($commit)
|
||||
{
|
||||
throw new \Exception('No rooms specified');
|
||||
}
|
||||
// If this is an Ajax request and we're not committing the booking, ie we are just
|
||||
// checking for conflicts, then it's perfectly possible to get here without any rooms
|
||||
// being selected on the form (just deselect the room on the form with Ctrl Click). So
|
||||
// in this case just return a null response.
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
echo json_encode(null);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Make sure the area corresponds to the room that is being booked
|
||||
$area = get_area($rooms[0]);
|
||||
get_area_settings($area); // Update the area settings
|
||||
|
||||
// and that $room is in $area
|
||||
if (get_area($room) != $area)
|
||||
{
|
||||
$room = get_default_room($area);
|
||||
}
|
||||
|
||||
// Don't bother with these checks if this is an Ajax request.
|
||||
if (!$is_ajax)
|
||||
{
|
||||
if (!isset($name) || ($name === ''))
|
||||
{
|
||||
invalid_booking(get_vocab('must_set_description'));
|
||||
}
|
||||
|
||||
if (($rep_type != RepeatRule::NONE) && ($rep_interval < 1))
|
||||
{
|
||||
invalid_booking(get_vocab('invalid_rep_interval'));
|
||||
}
|
||||
|
||||
// Check that we've got the mandatory fields
|
||||
if (!empty($is_mandatory_field))
|
||||
{
|
||||
foreach ($is_mandatory_field as $full_field => $value)
|
||||
{
|
||||
if (is_mandatory_field($full_field, $area))
|
||||
{
|
||||
$field = preg_replace('/^entry\./', '', $full_field);
|
||||
if ((in_array($field, $standard_fields['entry']) && ($$field === '')) ||
|
||||
(array_key_exists($field, $custom_fields) && ($custom_fields[$field] === '')))
|
||||
{
|
||||
invalid_booking(get_vocab('missing_mandatory_field') . ' "' .
|
||||
get_loc_field_name(_tbl('entry'), $field) . '"');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($type))
|
||||
{
|
||||
$type = $default_type;
|
||||
}
|
||||
|
||||
// Check that the type is allowed
|
||||
if (!is_book_admin($rooms) && isset($auth['admin_only_types']) && in_array($type, $auth['admin_only_types']))
|
||||
{
|
||||
invalid_booking(get_vocab('type_reserved_for_admins', get_type_vocab($type)));
|
||||
}
|
||||
|
||||
if (isset($month_relative_ord) && isset($month_relative_day))
|
||||
{
|
||||
$month_relative = $month_relative_ord . $month_relative_day;
|
||||
}
|
||||
|
||||
// Handle private bookings.
|
||||
// If the area settings allow users to make private bookings then use the value from
|
||||
// the form, unless the privacy status is forced and the user is not an admin (admins
|
||||
// are allowed to make public bookings if they want, even if the status is forced).
|
||||
// Otherwise the booking is not private, unless the status is forced, in which case
|
||||
// the default applies, whether or not the user is an admin.
|
||||
if ($private_enabled)
|
||||
{
|
||||
$is_private = (!is_book_admin() && $private_mandatory) ? $private_default : (bool) $private;
|
||||
}
|
||||
else
|
||||
{
|
||||
$is_private = ($private_mandatory) ? $private_default : false;
|
||||
}
|
||||
|
||||
// Check that they really are allowed to set $no_mail;
|
||||
if ($no_mail)
|
||||
{
|
||||
if (!$mail_settings['allow_no_mail'] &&
|
||||
(!is_book_admin($rooms) || !$mail_settings['allow_admins_no_mail']))
|
||||
{
|
||||
$no_mail = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an Ajax request and we're being asked to commit the booking, then
|
||||
// we'll only have been supplied with parameters that need to be changed. Fill in
|
||||
// the rest from the existing booking information.
|
||||
// Note: we assume that
|
||||
// (1) this is not a series (we can't cope with them yet)
|
||||
// (2) we always get passed start_seconds and end_seconds in the Ajax data
|
||||
if ($is_ajax && $commit)
|
||||
{
|
||||
$old_booking = get_booking_info($id, false);
|
||||
|
||||
foreach ($form_vars as $var => $var_type)
|
||||
{
|
||||
if (!isset($$var) || (($var_type == 'array') && empty($$var)))
|
||||
{
|
||||
switch ($var)
|
||||
{
|
||||
case 'rep_type':
|
||||
// If it's a series we're just going to change this entry
|
||||
$$var = RepeatRule::NONE;
|
||||
break;
|
||||
case 'rooms':
|
||||
$rooms = array($old_booking['room_id']);
|
||||
break;
|
||||
case 'original_room_id':
|
||||
$$var = $old_booking['room_id'];
|
||||
break;
|
||||
case 'private':
|
||||
$$var = $old_booking['private'];
|
||||
break;
|
||||
case 'confirmed':
|
||||
$$var = !$old_booking['tentative'];
|
||||
break;
|
||||
// In the calculation of $start_seconds and $end_seconds below we need to take
|
||||
// care of the case when 0000 on the day in question is across a DST boundary
|
||||
// from the current time, ie the days on which DST starts and ends.
|
||||
case 'start_seconds':
|
||||
$date = getdate($old_booking['start_time']);
|
||||
$start_year = (int) $date['year'];
|
||||
$start_month = (int) $date['mon'];
|
||||
$start_day = (int) $date['mday'];
|
||||
$start_daystart = mktime(0, 0, 0, $start_month, $start_day, $start_year);
|
||||
$old_start = $old_booking['start_time'];
|
||||
$start_seconds = $old_start - $start_daystart;
|
||||
$start_seconds -= cross_dst($start_daystart, $old_start);
|
||||
break;
|
||||
case 'end_seconds':
|
||||
$date = getdate($old_booking['end_time']);
|
||||
$end_year = (int) $date['year'];
|
||||
$end_month = (int) $date['mon'];
|
||||
$end_day = (int) $date['mday'];
|
||||
$end_daystart = mktime(0, 0, 0, $end_month, $end_day, $end_year);
|
||||
$old_end = $old_booking['end_time'];
|
||||
$end_seconds = $old_end - $end_daystart;
|
||||
$end_seconds -= cross_dst($end_daystart, $old_end);
|
||||
// When using periods end_seconds is actually the start of the last period
|
||||
if ($enable_periods)
|
||||
{
|
||||
$end_seconds -= 60;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (array_key_exists($var, $old_booking))
|
||||
{
|
||||
$$var = $old_booking[$var];
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now the custom fields
|
||||
$custom_fields = array();
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
if (!in_array($field['name'], $standard_fields['entry']))
|
||||
{
|
||||
$custom_fields[$field['name']] = $old_booking[$field['name']];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// When All Day is checked, $start_seconds and $end_seconds are disabled and so won't
|
||||
// get passed through by the form. We therefore need to set them.
|
||||
if (!empty($all_day))
|
||||
{
|
||||
if ($enable_periods)
|
||||
{
|
||||
$start_seconds = 12 * SECONDS_PER_HOUR;
|
||||
// This is actually the start of the last period, which is what the form would
|
||||
// have returned. It will get corrected in a moment.
|
||||
$end_seconds = $start_seconds + ((count($periods) - 1) * 60);
|
||||
}
|
||||
else
|
||||
{
|
||||
$start_seconds = (($morningstarts * 60) + $morningstarts_minutes) * 60;
|
||||
$end_seconds = (($eveningends * 60) + $eveningends_minutes) *60;
|
||||
$end_seconds += $resolution; // We want the end of the last slot, not the beginning
|
||||
if ($end_seconds <= $start_seconds)
|
||||
{
|
||||
$end_seconds += SECONDS_PER_DAY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we're operating on a booking day that stretches past midnight, it's more convenient
|
||||
// for the sections past midnight to be shown as being on the day before. That way the
|
||||
// $returl will end up taking us back to the day we started on
|
||||
if (day_past_midnight())
|
||||
{
|
||||
$end_last = (((($eveningends * 60) + $eveningends_minutes) *60) + $resolution) % SECONDS_PER_DAY;
|
||||
if ($start_seconds < $end_last)
|
||||
{
|
||||
$start_seconds += SECONDS_PER_DAY;
|
||||
$day_before = getdate(mktime(0, 0, 0, $start_month, $start_day-1, $start_year));
|
||||
$start_day = (int) $day_before['mday'];
|
||||
$start_month = (int) $day_before['mon'];
|
||||
$start_year = (int) $day_before['year'];
|
||||
}
|
||||
}
|
||||
|
||||
$target_rooms = $rooms;
|
||||
|
||||
// Check that the user has permission to create/edit an entry for this room.
|
||||
// Get the id of the room that we are creating/editing
|
||||
if (isset($id))
|
||||
{
|
||||
// Editing an existing booking: get the room_id from the database (you can't
|
||||
// get it from $rooms because they are the new rooms)
|
||||
$sql = "SELECT room_id
|
||||
FROM " . _tbl('entry') . "
|
||||
WHERE id=?
|
||||
LIMIT 1";
|
||||
$existing_room = db()->query1($sql, array($id));
|
||||
if ($existing_room < 0)
|
||||
{
|
||||
// Ideally we should give more feedback to the user when this happens, or
|
||||
// even lock the entry once a user starts to edit it.
|
||||
$message = "Tried to edit an entry that no longer exists - probably because " .
|
||||
"somebody else has deleted it in the meantime.";
|
||||
trigger_error($message, E_USER_NOTICE);
|
||||
location_header($returl);
|
||||
}
|
||||
$target_rooms[] = $existing_room;
|
||||
$target_rooms = array_unique($target_rooms);
|
||||
}
|
||||
|
||||
// Must have write access to at least one of the rooms
|
||||
if (!getWritable($create_by, $target_rooms, false))
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
if ($enable_periods)
|
||||
{
|
||||
$resolution = 60;
|
||||
}
|
||||
|
||||
// Now work out the start and times
|
||||
$start_time = mktime(0, 0, $start_seconds, $start_month, $start_day, $start_year);
|
||||
$end_time = mktime(0, 0, $end_seconds, $end_month, $end_day, $end_year);
|
||||
|
||||
// If we're using periods then the endtime we've been returned by the form is actually
|
||||
// the beginning of the last period in the booking (it's more intuitive for users this way)
|
||||
// so we need to add on 60 seconds (1 period)
|
||||
if ($enable_periods)
|
||||
{
|
||||
$end_time = $end_time + 60;
|
||||
}
|
||||
|
||||
// Round down the starttime and round up the endtime to the nearest slot boundaries
|
||||
// (This step is probably unnecessary now that MRBS always returns times aligned
|
||||
// on slot boundaries, but is left in for good measure).
|
||||
$start_first_slot = get_start_first_slot($start_month, $start_day, $start_year);
|
||||
$start_time = round_t_down($start_time, $resolution, $start_first_slot);
|
||||
$start_first_slot = get_start_first_slot($end_month, $end_day, $end_year);
|
||||
$end_time = round_t_up($end_time, $resolution, $start_first_slot);
|
||||
|
||||
// If they asked for 0 minutes, and even after the rounding the slot length is still
|
||||
// 0 minutes, push that up to 1 resolution unit.
|
||||
if ($end_time == $start_time)
|
||||
{
|
||||
$end_time += $resolution;
|
||||
}
|
||||
|
||||
if (!isset($rep_type))
|
||||
{
|
||||
$rep_type = RepeatRule::NONE;
|
||||
}
|
||||
|
||||
if (!isset($rep_day))
|
||||
{
|
||||
$rep_day = array();
|
||||
}
|
||||
|
||||
// Get the repeat details
|
||||
$repeat_rule = new RepeatRule();
|
||||
$repeat_rule->setType($rep_type ?? RepeatRule::NONE);
|
||||
|
||||
if ($repeat_rule->getType() != RepeatRule::NONE)
|
||||
{
|
||||
$repeat_rule->setInterval($rep_interval);
|
||||
if ($repeat_rule->getType() == RepeatRule::MONTHLY)
|
||||
{
|
||||
$repeat_rule->setMonthlyType($month_type);
|
||||
if ($repeat_rule->getMonthlyType() == RepeatRule::MONTHLY_ABSOLUTE)
|
||||
{
|
||||
$repeat_rule->setMonthlyAbsolute($month_absolute);
|
||||
}
|
||||
else
|
||||
{
|
||||
$repeat_rule->setMonthlyRelative($month_relative);
|
||||
}
|
||||
}
|
||||
if (isset($rep_end_date))
|
||||
{
|
||||
$repeat_end_date = DateTime::createFromFormat(DateTime::ISO8601_DATE, $rep_end_date);
|
||||
if ($repeat_end_date === false)
|
||||
{
|
||||
throw new Exception("Could not create repeat end date");
|
||||
}
|
||||
$repeat_end_date->setTime(intval($start_seconds/SECONDS_PER_HOUR), intval(($start_seconds%SECONDS_PER_HOUR)/60));
|
||||
$repeat_rule->setEndDate($repeat_end_date);
|
||||
}
|
||||
|
||||
if ($repeat_rule->getType() == RepeatRule::WEEKLY)
|
||||
{
|
||||
// If no repeat day has been set, then set a default repeat day
|
||||
// as the day of the week of the start of the period
|
||||
$repeat_rule->setDays ((count($rep_day) > 0) ? $rep_day : array(date('w', $start_time)));
|
||||
}
|
||||
|
||||
// Make sure that the starttime coincides with a repeat day. In
|
||||
// other words make sure that the first starttime defines an actual
|
||||
// entry. We need to do this because if we are going to construct an iCalendar
|
||||
// object, RFC 5545 demands that the start time is the first event of
|
||||
// a series. ['The "DTSTART" property for a "VEVENT" specifies the inclusive
|
||||
// start of the event. For recurring events, it also specifies the very first
|
||||
// instance in the recurrence set.']
|
||||
|
||||
// Get the first entry in the series and make that the start time
|
||||
$reps = $repeat_rule->getRepeatStartTimes($start_time, 1);
|
||||
|
||||
if (count($reps) > 0)
|
||||
{
|
||||
$duration = $end_time - $start_time;
|
||||
$duration -= cross_dst($start_time, $end_time);
|
||||
$start_time = $reps[0];
|
||||
$end_time = $start_time + $duration;
|
||||
$start_day = (int) date('j', $start_time);
|
||||
$start_month = (int) date('n', $start_time);
|
||||
$start_year = (int) date('Y', $start_time);
|
||||
}
|
||||
}
|
||||
|
||||
// If we're committing this booking, get the start day/month/year and
|
||||
// make them the current day/month/year
|
||||
if (!$is_ajax || $commit)
|
||||
{
|
||||
$day = $start_day;
|
||||
$month = $start_month;
|
||||
$year = $start_year;
|
||||
}
|
||||
|
||||
// Set up the return URL. As the user has tried to book a particular room and a particular
|
||||
// day, we must consider these to be the new "sticky room" and "sticky day", so modify the
|
||||
// return URL accordingly.
|
||||
|
||||
// First get the return URL basename, having stripped off the old query string
|
||||
// (1) It's possible that $returl could be empty, for example if edit_entry.php had been called
|
||||
// direct, perhaps if the user has it set as a bookmark
|
||||
// (2) Avoid an endless loop. It shouldn't happen, but just in case ...
|
||||
// (3) If you've come from search, you probably don't want to go back there (and if you did we'd
|
||||
// have to preserve the search parameter in the query string)
|
||||
if (isset($returl) && ($returl !== ''))
|
||||
{
|
||||
$returl = parse_url($returl);
|
||||
if ($returl !== false)
|
||||
{
|
||||
if (isset($returl['query']))
|
||||
{
|
||||
parse_str($returl['query'], $query_vars);
|
||||
}
|
||||
$view = $query_vars['view'] ?? $default_view;
|
||||
$view_all = $query_vars['view_all'] ?? (($default_view_all) ? 1 : 0);
|
||||
$returl = explode('/', $returl['path']);
|
||||
$returl = end($returl);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($returl) ||
|
||||
in_array($returl, array('edit_entry.php',
|
||||
'edit_entry_handler.php',
|
||||
'search.php')))
|
||||
{
|
||||
$returl = 'index.php';
|
||||
}
|
||||
|
||||
// If we haven't been given a sensible date then get out of here and don't try and make a booking
|
||||
if (!isset($start_day) || !isset($start_month) || !isset($start_year) || !checkdate($start_month, $start_day, $start_year))
|
||||
{
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
// If the old sticky room is one of the rooms requested for booking, then don't change the sticky room.
|
||||
// Otherwise change the sticky room to be one of the new rooms.
|
||||
if (!in_array($room, $rooms))
|
||||
{
|
||||
$room = $rooms[0];
|
||||
}
|
||||
// Find the corresponding area
|
||||
$area = get_area($room);
|
||||
if (empty($area))
|
||||
{
|
||||
// This shouldn't happen.
|
||||
// TODO: Get rid of the area parameter from the query string, and just use the room parameter, as given the room
|
||||
// TODO: the area is redundant.
|
||||
trigger_error("Room with id $room doesn't exist.", E_USER_WARNING);
|
||||
}
|
||||
|
||||
// Now construct the new query string
|
||||
$vars = [
|
||||
'view' => $view ?? $default_view,
|
||||
'view_all' => $view_all ?? $default_view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room
|
||||
];
|
||||
|
||||
// If we're going back to the index page then add any scroll positions to the
|
||||
// query string so that the JavaScript can scroll back to the same position.
|
||||
if ('index.php' == basename(parse_url($returl, PHP_URL_PATH)))
|
||||
{
|
||||
foreach (['top', 'left'] as $var)
|
||||
{
|
||||
$$var = get_form_var($var, 'string');
|
||||
if (isset($$var))
|
||||
{
|
||||
$vars[$var] = $$var;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$returl .= '?' . http_build_query($vars, '', '&');
|
||||
|
||||
|
||||
// Check to see whether this is a repeat booking and if so, whether the user
|
||||
// is allowed to make/edit repeat bookings. (The edit_entry form should
|
||||
// prevent you ever getting here, but this check is here as a safeguard in
|
||||
// case someone has spoofed the HTML)
|
||||
if (isset($rep_type) && ($rep_type != RepeatRule::NONE) &&
|
||||
!is_book_admin($rooms) &&
|
||||
!empty($auth['only_admin_can_book_repeat']))
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
// (4) Assemble the booking data
|
||||
// -----------------------------
|
||||
|
||||
// Assemble an array of bookings, one for each room
|
||||
$bookings = array();
|
||||
foreach ($rooms as $room_id)
|
||||
{
|
||||
// Ignore rooms for which the user doesn't have write access
|
||||
if (!getWritable($create_by, $room_id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$booking = array();
|
||||
$booking['create_by'] = $create_by;
|
||||
$booking['modified_by'] = (isset($id)) ? $mrbs_username : '';
|
||||
$booking['name'] = $name;
|
||||
$booking['type'] = $type;
|
||||
$booking['description'] = $description;
|
||||
$booking['room_id'] = $room_id;
|
||||
$booking['start_time'] = $start_time;
|
||||
$booking['end_time'] = $end_time;
|
||||
$booking['ical_uid'] = $ical_uid;
|
||||
$booking['ical_sequence'] = $ical_sequence;
|
||||
$booking['ical_recur_id'] = $ical_recur_id;
|
||||
$booking['allow_registration'] = $allow_registration;
|
||||
$booking['registrant_limit'] = $registrant_limit;
|
||||
$booking['registrant_limit_enabled'] = $registrant_limit_enabled;
|
||||
$booking['registration_opens'] = (isset($registration_opens)) ? $registration_opens : null;
|
||||
$booking['registration_opens_enabled'] = $registration_opens_enabled;
|
||||
$booking['registration_closes'] = (isset($registration_closes)) ? $registration_closes : null;
|
||||
$booking['registration_closes_enabled'] = $registration_closes_enabled;
|
||||
$booking['repeat_rule'] = $repeat_rule;
|
||||
|
||||
// Do the custom fields
|
||||
foreach ($custom_fields as $key => $value)
|
||||
{
|
||||
$booking[$key] = $value;
|
||||
}
|
||||
|
||||
// Set the various statuses as appropriate
|
||||
// (Note: the statuses fields are the only ones that can differ by room)
|
||||
|
||||
// Privacy status
|
||||
$booking['private'] = (bool) $is_private;
|
||||
|
||||
// If we are using booking approvals then we need to work out whether the
|
||||
// status of this booking is approved. If the user is allowed to approve
|
||||
// bookings for this room, then the status will be approved, since they are
|
||||
// in effect immediately approving their own booking. Otherwise the booking
|
||||
// will need to approved.
|
||||
$booking['awaiting_approval'] = ($approval_enabled && !is_book_admin($room_id));
|
||||
|
||||
// Confirmation status
|
||||
$booking['tentative'] = ($confirmation_enabled && !$confirmed);
|
||||
|
||||
$bookings[] = $booking;
|
||||
}
|
||||
|
||||
$just_check = $is_ajax && !$commit;
|
||||
$this_id = (isset($id)) ? $id : null;
|
||||
$send_mail = !$no_mail && need_to_send_mail();
|
||||
|
||||
try
|
||||
{
|
||||
// Wrap the editing process in a transaction, because we'll want to roll back the edit if the
|
||||
// deletion of the old booking fails. This could happen, for example, if
|
||||
// (a) somebody else has already edited the booking and the original booking no longer exists; or
|
||||
// (b) if there's some other problem, eg the database user hasn't been granted DELETE rights, in which
|
||||
// case we would be left with two overlapping bookings.
|
||||
db()->begin();
|
||||
$transaction_ok = true;
|
||||
$result = mrbsMakeBookings($bookings, $this_id, $just_check, $skip, $original_room_id, $send_mail, $edit_series);
|
||||
|
||||
// If we weren't just checking and this was a successful booking and
|
||||
// we were editing an existing booking, then delete the old booking
|
||||
if (!$just_check && $result['valid_booking'] && isset($id))
|
||||
{
|
||||
$transaction_ok = mrbsDelEntry($id, $edit_series, true);
|
||||
}
|
||||
|
||||
if ($transaction_ok)
|
||||
{
|
||||
db()->commit();
|
||||
}
|
||||
else
|
||||
{
|
||||
db()->rollback();
|
||||
trigger_error('Edit failed.', E_USER_WARNING);
|
||||
}
|
||||
|
||||
// If this is an Ajax request, output the result and finish
|
||||
if ($is_ajax)
|
||||
{
|
||||
if ($commit)
|
||||
{
|
||||
// Generate the new HTML
|
||||
$calendar = CalendarFactory::create($view, $view_all, $year, $month, $day, $area, $room, $timetohighlight);
|
||||
$result['table_innerhtml'] = $calendar->innerHTML();
|
||||
}
|
||||
http_headers(array("Content-Type: application/json"));
|
||||
echo json_encode($result);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
if ($is_ajax)
|
||||
{
|
||||
trigger_error('Caught exception: ' . $e->getMessage(), E_USER_WARNING);
|
||||
http_response_code(500);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
// Everything was OK. Go back to where we came from
|
||||
if ($result['valid_booking'])
|
||||
{
|
||||
location_header($returl);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => $room ?? null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
echo "<h2>" . get_vocab("sched_conflict") . "</h2>\n";
|
||||
if (!empty($result['violations']['errors']))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo get_vocab("rules_broken") . "\n";
|
||||
echo "</p>\n";
|
||||
echo "<ul>\n";
|
||||
foreach ($result['violations']['errors'] as $rule)
|
||||
{
|
||||
echo "<li>$rule</li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
}
|
||||
if (!empty($result['conflicts']))
|
||||
{
|
||||
echo "<p>\n";
|
||||
echo get_vocab("conflict") . "\n";
|
||||
echo "</p>\n";
|
||||
echo "<ul>\n";
|
||||
foreach ($result['conflicts'] as $conflict)
|
||||
{
|
||||
echo "<li>$conflict</li>\n";
|
||||
}
|
||||
echo "</ul>\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "<div id=\"submit_buttons\">\n";
|
||||
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array('action' => multisite(this_page())));
|
||||
|
||||
// Back button
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array(
|
||||
'formaction' => multisite('edit_entry.php'),
|
||||
'name' => 'back_button',
|
||||
'value' => get_vocab('back')
|
||||
));
|
||||
$form->addElement($submit);
|
||||
|
||||
// Skip and Book button (to book the entries that don't conflict)
|
||||
// Only show this button if there were no policies broken and it's a series
|
||||
if (empty($result['violations']['errors']) &&
|
||||
isset($rep_type) && ($rep_type != RepeatRule::NONE))
|
||||
{
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array(
|
||||
'value' => get_vocab('skip_and_book'),
|
||||
'title' => get_vocab('skip_and_book_note')
|
||||
));
|
||||
$form->addElement($submit);
|
||||
// Force a skip next time round
|
||||
$skip = true;
|
||||
}
|
||||
|
||||
// Put the booking data in as hidden inputs
|
||||
// First the ordinary fields
|
||||
foreach ($form_vars as $var => $var_type)
|
||||
{
|
||||
if ($var_type == 'array')
|
||||
{
|
||||
// See the comment at the top of the page about array formats
|
||||
foreach ($$var as $value)
|
||||
{
|
||||
if (isset($value))
|
||||
{
|
||||
$form->addHiddenInput("{$var}[]", $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif (isset($$var))
|
||||
{
|
||||
$form->addHiddenInput($var, $$var);
|
||||
}
|
||||
}
|
||||
// Then the custom fields
|
||||
foreach($fields as $field)
|
||||
{
|
||||
if (array_key_exists($field['name'], $custom_fields) && isset($custom_fields[$field['name']]))
|
||||
{
|
||||
$form->addHiddenInput(VAR_PREFIX . $field['name'], $custom_fields[$field['name']]);
|
||||
}
|
||||
}
|
||||
|
||||
$form->render();
|
||||
|
||||
echo "</div>\n";
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\FieldDiv;
|
||||
use MRBS\Form\FieldInputDate;
|
||||
use MRBS\Form\FieldTextarea;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require 'defaultincludes.inc';
|
||||
|
||||
|
||||
function get_field_display_from(Message $message): FieldInputDate
|
||||
{
|
||||
$field = new FieldInputDate();
|
||||
$field->setLabel(get_vocab('display_from'))
|
||||
->setControlAttributes(['name' => 'message_from', 'value' => $message->getFromDate()]);
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
||||
function get_field_display_until(Message $message): FieldInputDate
|
||||
{
|
||||
$field = new FieldInputDate();
|
||||
$field->setLabel(get_vocab('display_until'))
|
||||
->setControlAttributes(['name' => 'message_until', 'value' => $message->getUntilDate()]);
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
||||
function get_field_message_text(Message $message): FieldTextarea
|
||||
{
|
||||
$field = new FieldTextarea();
|
||||
$field->setLabel(get_vocab('message'))
|
||||
->setControlAttribute('name', 'message_text')
|
||||
->setControlText($message->getText());
|
||||
return $field;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_submit_buttons() : ElementFieldset
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
|
||||
// The back and submit buttons
|
||||
$field = new FieldDiv();
|
||||
|
||||
$back = new ElementInputSubmit();
|
||||
$back->setAttributes(array(
|
||||
'name' => 'back_button',
|
||||
'value' => get_vocab('back'),
|
||||
'formnovalidate' => true)
|
||||
);
|
||||
|
||||
$submit = new ElementInputSubmit();
|
||||
$submit->setAttributes(array(
|
||||
'class' => 'default_action',
|
||||
'name' => 'save_button',
|
||||
'value' => get_vocab('save'))
|
||||
);
|
||||
|
||||
$field->setAttribute('class', 'submit_buttons')
|
||||
->addLabelClass('no_suffix')
|
||||
->addLabelElement($back)
|
||||
->addControlElement($submit);
|
||||
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Must also be a booking admin
|
||||
if (!is_book_admin())
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => $area,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
$returl = 'admin.php?' . http_build_query($context, '', '&');
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Get the current message, if any
|
||||
$message = Message::getInstance();
|
||||
$message->load();
|
||||
|
||||
// Construct the form
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$form->setAttributes(array(
|
||||
'class' => 'standard',
|
||||
'id' => 'message',
|
||||
'action' => multisite('edit_message_handler.php'))
|
||||
);
|
||||
|
||||
$form->addHiddenInput('returl', $returl);
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend(get_vocab('edit_message'));
|
||||
|
||||
$fieldset->addElement(get_field_message_text($message))
|
||||
->addElement(get_field_display_from($message))
|
||||
->addElement(get_field_display_until($message));
|
||||
|
||||
$form->addElement($fieldset)
|
||||
->addElement(get_fieldset_submit_buttons())
|
||||
->render();
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require 'defaultincludes.inc';
|
||||
|
||||
// Check the CSRF token
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Must also be a booking admin
|
||||
if (!is_book_admin())
|
||||
{
|
||||
showAccessDenied($view, $view_all, $year, $month, $day, $area, $room ?? null);
|
||||
exit;
|
||||
}
|
||||
|
||||
$message_text = get_form_var('message_text', 'string', '');
|
||||
$message_from = get_form_var('message_from', 'string', '');
|
||||
$message_until = get_form_var('message_until', 'string', '');
|
||||
$returl = get_form_var('returl', 'url_local', 'admin.php');
|
||||
$save_button = get_form_var('save_button', 'string');
|
||||
|
||||
if (!empty($save_button))
|
||||
{
|
||||
$message = Message::getInstance($message_text, $message_from, $message_until);
|
||||
$message->save();
|
||||
}
|
||||
|
||||
location_header($returl);
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
use MRBS\Errors\Errors;
|
||||
use MRBS\Form\ElementFieldset;
|
||||
use MRBS\Form\ElementInputSubmit;
|
||||
use MRBS\Form\ElementP;
|
||||
use MRBS\Form\FieldInputCheckbox;
|
||||
use MRBS\Form\FieldInputEmail;
|
||||
use MRBS\Form\FieldInputNumber;
|
||||
use MRBS\Form\FieldInputRadioGroup;
|
||||
use MRBS\Form\FieldInputSubmit;
|
||||
use MRBS\Form\FieldInputText;
|
||||
use MRBS\Form\FieldSelect;
|
||||
use MRBS\Form\FieldTextarea;
|
||||
use MRBS\Form\Form;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
require_once "mrbs_sql.inc";
|
||||
|
||||
|
||||
// If you want to add some extra columns to the room table to describe the room
|
||||
// then you can do so and this page should automatically recognise them and handle
|
||||
// them. At the moment support is limited to the following column types:
|
||||
//
|
||||
// MySQL PostgreSQL Form input type
|
||||
// ----- ---------- ---------------
|
||||
// bigint bigint text
|
||||
// int integer text
|
||||
// mediumint text
|
||||
// smallint smallint checkbox
|
||||
// tinyint checkbox
|
||||
// text text textarea
|
||||
// tinytext textarea
|
||||
// character varying textarea
|
||||
// varchar(n) character varying(n) text/textarea, depending on the value of n
|
||||
// character text
|
||||
// char(n) character(n) text/textarea, depending on the value of n
|
||||
//
|
||||
// NOTE 1: For char(n) and varchar(n) fields, a text input will be presented if
|
||||
// n is less than or equal to $text_input_max, otherwise a textarea box will be
|
||||
// presented.
|
||||
//
|
||||
// NOTE 2: PostgreSQL booleans are not supported, due to difficulties in
|
||||
// handling the fields in a database independent way (a PostgreSQL boolean
|
||||
// will return a PHP boolean type when read by a PHP query, whereas a MySQL
|
||||
// tinyint returns an int). In order to have a boolean field in the room
|
||||
// table you should use a smallint in PostgreSQL or a smallint or a tinyint
|
||||
// in MySQL.
|
||||
//
|
||||
// You can put a description of the column that will be used as the label in
|
||||
// the form in the $vocab_override variable in the config file using the tag
|
||||
// 'room.[columnname]'.
|
||||
//
|
||||
// For example if you want to add a column specifying whether or not a room
|
||||
// has a coffee machine you could add a column to the room table called
|
||||
// 'coffee_machine' of type tinyint, in MySQL, or smallint in PostgreSQL.
|
||||
// Then in the config file you would add the line
|
||||
//
|
||||
// $vocab_override['en']['room.coffee_machine'] = "Coffee machine"; // or appropriate translation
|
||||
//
|
||||
// If MRBS can't find an entry for the field in the lang file or vocab overrides, then
|
||||
// it will use the fieldname, eg 'coffee_machine'.
|
||||
|
||||
|
||||
function get_custom_fields($data)
|
||||
{
|
||||
global $standard_fields, $text_input_max;
|
||||
|
||||
// TODO: have a common way of generating custom fields for all tables
|
||||
|
||||
$result = array();
|
||||
$disabled = !is_admin();
|
||||
|
||||
// Get the information about the columns in the room table
|
||||
$columns = db()->field_info(_tbl('room'));
|
||||
|
||||
foreach ($columns as $column)
|
||||
{
|
||||
if (!in_array($column['name'], $standard_fields['room']))
|
||||
{
|
||||
$label = get_loc_field_name(_tbl('room'), $column['name']);
|
||||
$name = VAR_PREFIX . $column['name'];
|
||||
$value = $data[$column['name']];
|
||||
|
||||
// Output a checkbox if it's a boolean or integer <= 2 bytes (which we will
|
||||
// assume are intended to be booleans)
|
||||
if (($column['nature'] == 'boolean') ||
|
||||
(($column['nature'] == 'integer') && isset($column['length']) && ($column['length'] <= 2)) )
|
||||
{
|
||||
$field = new FieldInputCheckbox();
|
||||
$field->setLabel($label)
|
||||
->setControlAttributes(array('name' => $name,
|
||||
'disabled' => $disabled))
|
||||
->setChecked($value);
|
||||
}
|
||||
// Output a textarea if it's a character string longer than the limit for a
|
||||
// text input
|
||||
elseif (($column['nature'] == 'character') && isset($column['length']) && ($column['length'] > $text_input_max))
|
||||
{
|
||||
$field = new FieldTextarea();
|
||||
$field->setLabel($label)
|
||||
->setControlAttributes(array('name' => $name,
|
||||
'disabled' => $disabled))
|
||||
->setControlText($value ?? '');
|
||||
}
|
||||
// Otherwise output a text input
|
||||
else
|
||||
{
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel($label)
|
||||
->setControlAttributes(array('name' => $name,
|
||||
'value' => $value,
|
||||
'maxlength' => maxlength('room.' . $column['name']),
|
||||
'disabled' => $disabled));
|
||||
}
|
||||
$result[] = $field;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_errors(array $errors) : ElementFieldset
|
||||
{
|
||||
$fieldset = new ElementFieldset();
|
||||
$fieldset->addLegend('')
|
||||
->setAttribute('class', 'error');
|
||||
|
||||
foreach ($errors as $error)
|
||||
{
|
||||
$element = new ElementP();
|
||||
$element->setText(get_vocab($error));
|
||||
$fieldset-> addElement($element);
|
||||
}
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
function get_fieldset_general(array $data) : ElementFieldset
|
||||
{
|
||||
global $auth;
|
||||
|
||||
$disabled = !is_admin();
|
||||
|
||||
$fieldset = new ElementFieldset();
|
||||
|
||||
// The area select
|
||||
$areas = get_area_names(true);
|
||||
$field = new FieldSelect();
|
||||
$field->setLabel(get_vocab('area'))
|
||||
->setControlAttributes(array('name' => 'new_area',
|
||||
'disabled' => $disabled))
|
||||
->addSelectOptions($areas, $data['area_id'], true);
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Room name
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('name'))
|
||||
->setControlAttributes(array('name' => 'room_name',
|
||||
'value' => $data['room_name'],
|
||||
'maxlength' => maxlength('room.room_name'),
|
||||
'required' => true,
|
||||
'disabled' => $disabled));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Sort key
|
||||
if (is_admin())
|
||||
{
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('sort_key'))
|
||||
->setLabelAttribute('title', get_vocab('sort_key_note'))
|
||||
->setControlAttributes(array('name' => 'sort_key',
|
||||
'value' => $data['sort_key'],
|
||||
'maxlength' => maxlength('room.sort_key'),
|
||||
'disabled' => $disabled));
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// Status - Enabled or Disabled
|
||||
if (is_admin())
|
||||
{
|
||||
$options = array('0' => get_vocab('enabled'),
|
||||
'1' => get_vocab('disabled'));
|
||||
$value = ($data['disabled']) ? '1' : '0';
|
||||
$field = new FieldInputRadioGroup();
|
||||
$field->setLabel(get_vocab('status'))
|
||||
->setLabelAttributes(array('title' => get_vocab('disabled_room_note')))
|
||||
->addRadioOptions($options, 'room_disabled', $value, true, $disabled);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// Description
|
||||
$field = new FieldInputText();
|
||||
$field->setLabel(get_vocab('description'))
|
||||
->setControlAttributes(array('name' => 'description',
|
||||
'value' => $data['description'],
|
||||
'maxlength' => maxlength('room.description'),
|
||||
'disabled' => $disabled));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Capacity
|
||||
$field = new FieldInputNumber();
|
||||
$field->setLabel(get_vocab('capacity'))
|
||||
->setControlAttributes(array('name' => 'capacity',
|
||||
'min' => '0',
|
||||
'value' => $data['capacity'],
|
||||
'disabled' => $disabled));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Room admin email
|
||||
$field = new FieldInputEmail();
|
||||
$field->setLabel(get_vocab('room_admin_email'))
|
||||
->setLabelAttribute('title', get_vocab('email_list_note'))
|
||||
->setControlAttributes(array('name' => 'room_admin_email',
|
||||
'value' => $data['room_admin_email'],
|
||||
'multiple' => true,
|
||||
'disabled' => $disabled));
|
||||
$fieldset->addElement($field);
|
||||
|
||||
// Invalid types
|
||||
$type_options = get_type_options(true);
|
||||
if (!empty($type_options))
|
||||
{
|
||||
$field = new FieldSelect();
|
||||
$field->setAttribute('class', 'multiline')
|
||||
->setLabel(get_vocab('invalid_types'))
|
||||
->setLabelAttribute('title', get_vocab('invalid_types_note'))
|
||||
->setControlAttributes(array(
|
||||
'name' => 'invalid_types[]',
|
||||
'title' => get_vocab('select_note'),
|
||||
'multiple' => true)
|
||||
)
|
||||
->addSelectOptions($type_options, $data['invalid_types'], true);
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// The custom HTML
|
||||
if (is_admin() && $auth['allow_custom_html'])
|
||||
{
|
||||
// Only show the raw HTML to admins. Non-admins will see the rendered HTML
|
||||
$field = new FieldTextarea();
|
||||
$field->setLabel(get_vocab('custom_html'))
|
||||
->setLabelAttribute('title', get_vocab('custom_html_note'))
|
||||
->setControlAttribute('name', 'custom_html')
|
||||
->setControlText($data['custom_html'] ?? '');
|
||||
$fieldset->addElement($field);
|
||||
}
|
||||
|
||||
// Then the custom fields
|
||||
$fields = get_custom_fields($data);
|
||||
$fieldset->addElements($fields);
|
||||
|
||||
// The Submit and Back buttons
|
||||
$field = new FieldInputSubmit();
|
||||
|
||||
$back = new ElementInputSubmit();
|
||||
$back->setAttributes(array(
|
||||
'value' => get_vocab('back'),
|
||||
'formnovalidate' => true,
|
||||
'formaction' => multisite('admin.php'))
|
||||
);
|
||||
$field->setAttribute('class', 'buttons')
|
||||
->addLabelClass('no_suffix')
|
||||
->addLabelElement($back)
|
||||
->setControlAttribute('value', get_vocab('save'));
|
||||
if (!is_admin())
|
||||
{
|
||||
$field->removeControl();
|
||||
}
|
||||
$fieldset->addElement($field);
|
||||
|
||||
return $fieldset;
|
||||
}
|
||||
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
$context = array(
|
||||
'view' => $view,
|
||||
'view_all' => $view_all,
|
||||
'year' => $year,
|
||||
'month' => $month,
|
||||
'day' => $day,
|
||||
'area' => isset($area) ? $area : null,
|
||||
'room' => isset($room) ? $room : null
|
||||
);
|
||||
|
||||
print_header($context);
|
||||
|
||||
// Get the details for this room
|
||||
if (empty($room) || is_null($data = get_room_details($room)))
|
||||
{
|
||||
Errors::fatalError(get_vocab('invalid_room'));
|
||||
}
|
||||
|
||||
$errors = get_form_var('errors', 'array');
|
||||
|
||||
// Generate the form
|
||||
$form = new Form(Form::METHOD_POST);
|
||||
|
||||
$attributes = array('id' => 'edit_room',
|
||||
'class' => 'standard',
|
||||
'action' => multisite('edit_room_handler.php'));
|
||||
|
||||
// Non-admins will only be allowed to view room details, not change them
|
||||
$legend = (is_admin()) ? get_vocab('editroom') : get_vocab('viewroom');
|
||||
|
||||
$form->setAttributes($attributes)
|
||||
->addHiddenInputs(array(
|
||||
'room' => $data['id'],
|
||||
'area' => $data['area_id'],
|
||||
'old_area' => $data['area_id'],
|
||||
'old_room_name' => $data['room_name']
|
||||
));
|
||||
|
||||
$outer_fieldset = new ElementFieldset();
|
||||
|
||||
$outer_fieldset->addLegend($legend)
|
||||
->addElement(get_fieldset_errors($errors))
|
||||
->addElement(get_fieldset_general($data));
|
||||
|
||||
$form->addElement($outer_fieldset);
|
||||
|
||||
$form->render();
|
||||
|
||||
if ($auth['allow_custom_html'])
|
||||
{
|
||||
// Now the custom HTML
|
||||
echo "<div id=\"div_custom_html\">\n";
|
||||
// no escape_html() because we want the HTML!
|
||||
echo (isset($data['custom_html'])) ? $data['custom_html'] . "\n" : "";
|
||||
echo "</div>\n";
|
||||
}
|
||||
|
||||
|
||||
print_footer();
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace MRBS;
|
||||
|
||||
require "defaultincludes.inc";
|
||||
|
||||
use MRBS\Form\Form;
|
||||
|
||||
// Check the CSRF token.
|
||||
Form::checkToken();
|
||||
|
||||
// Check the user is authorised for this page
|
||||
checkAuthorised(this_page());
|
||||
|
||||
// Get non-standard form variables
|
||||
$form_vars = array(
|
||||
'new_area' => 'int',
|
||||
'old_area' => 'int',
|
||||
'room_name' => 'string',
|
||||
'sort_key' => 'string',
|
||||
'room_disabled' => 'string',
|
||||
'old_room_name' => 'string',
|
||||
'description' => 'string',
|
||||
'capacity' => 'int',
|
||||
'room_admin_email' => 'string',
|
||||
'invalid_types' => 'array',
|
||||
'custom_html' => 'string'
|
||||
);
|
||||
|
||||
foreach($form_vars as $var => $var_type)
|
||||
{
|
||||
$$var = get_form_var($var, $var_type);
|
||||
|
||||
// Trim the strings and truncate them to the maximum field length
|
||||
if (is_string($$var))
|
||||
{
|
||||
$$var = trim($$var);
|
||||
$$var = truncate($$var, "room.$var");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Get the information about the fields in the room table
|
||||
$fields = db()->field_info(_tbl('room'));
|
||||
|
||||
// Get any custom fields
|
||||
foreach($fields as $field)
|
||||
{
|
||||
$var = VAR_PREFIX . $field['name'];
|
||||
$$var = get_form_var($var, get_form_var_type($field));
|
||||
|
||||
// Cast booleans to ints (0 or 1) for insertion into the database
|
||||
if (is_bool($$var))
|
||||
{
|
||||
$$var = intval($$var);
|
||||
}
|
||||
|
||||
// Trim any strings and truncate them to the maximum field length
|
||||
if (is_string($$var) && ($field['nature'] != 'decimal'))
|
||||
{
|
||||
$$var = trim($$var);
|
||||
$$var = truncate($$var, 'room.' . $field['name']);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($capacity))
|
||||
{
|
||||
$capacity = 0;
|
||||
}
|
||||
|
||||
|
||||
// UPDATE THE DATABASE
|
||||
// -------------------
|
||||
|
||||
// Initialise the error array
|
||||
$errors = array();
|
||||
|
||||
// Clean up the address list replacing newlines by commas and removing duplicates
|
||||
$room_admin_email = clean_address_list($room_admin_email);
|
||||
// Validate email addresses
|
||||
if (!validate_email_list($room_admin_email))
|
||||
{
|
||||
$errors[] = 'invalid_email';
|
||||
}
|
||||
|
||||
// Make sure the invalid types exist
|
||||
if (isset($booking_types))
|
||||
{
|
||||
$invalid_types = array_intersect($invalid_types, $booking_types);
|
||||
}
|
||||
else
|
||||
{
|
||||
$invalid_types = array();
|
||||
}
|
||||
|
||||
|
||||
if (empty($errors))
|
||||
{
|
||||
// Start a transaction
|
||||
db()->begin();
|
||||
|
||||
// Check the new area still exists
|
||||
$sql = "SELECT id
|
||||
FROM " . _tbl('area') . "
|
||||
WHERE id=?
|
||||
LIMIT 1
|
||||
FOR UPDATE"; // lock this row
|
||||
|
||||
if (db()->query1($sql, array($new_area)) < 1)
|
||||
{
|
||||
$errors[] = 'invalid_area';
|
||||
db()->rollback();
|
||||
}
|
||||
// If so, check that the room name is not already used in the area
|
||||
// (only do this if you're changing the room name or the area - if you're
|
||||
// just editing the other details for an existing room we don't want to reject
|
||||
// the edit because the room already exists!)
|
||||
elseif ( (($new_area != $old_area) || ($room_name != $old_room_name))
|
||||
&& db()->query1("SELECT id
|
||||
FROM " . _tbl('room') . "
|
||||
WHERE room_name=:room_name
|
||||
AND area_id=:area_id
|
||||
LIMIT 1
|
||||
FOR UPDATE", array(":room_name" => $room_name, ":area_id" => $new_area)) > 0)
|
||||
{
|
||||
$errors[] = 'invalid_room_name';
|
||||
db()->rollback();
|
||||
}
|
||||
// If everything is still OK, update the database
|
||||
else
|
||||
{
|
||||
// Convert booleans into 0/1 (necessary for PostgreSQL)
|
||||
$room_disabled = (!empty($room_disabled)) ? 1 : 0;
|
||||
$sql = "UPDATE " . _tbl('room') . " SET ";
|
||||
$sql_params = array();
|
||||
$assign_array = array();
|
||||
foreach ($fields as $field)
|
||||
{
|
||||
if ($field['name'] != 'id') // don't do anything with the id field
|
||||
{
|
||||
switch ($field['name'])
|
||||
{
|
||||
// first of all deal with the standard MRBS fields
|
||||
case 'area_id':
|
||||
$assign_array[] = "area_id=?";
|
||||
$sql_params[] = $new_area;
|
||||
break;
|
||||
case 'disabled':
|
||||
$assign_array[] = "disabled=?";
|
||||
$sql_params[] = $room_disabled;
|
||||
break;
|
||||
case 'room_name':
|
||||
$assign_array[] = "room_name=?";
|
||||
$sql_params[] = $room_name;
|
||||
break;
|
||||
case 'sort_key':
|
||||
$assign_array[] = "sort_key=?";
|
||||
$sql_params[] = $sort_key;
|
||||
break;
|
||||
case 'description':
|
||||
$assign_array[] = "description=?";
|
||||
$sql_params[] = $description;
|
||||
break;
|
||||
case 'capacity':
|
||||
$assign_array[] = "capacity=?";
|
||||
$sql_params[] = $capacity;
|
||||
break;
|
||||
case 'room_admin_email':
|
||||
$assign_array[] = "room_admin_email=?";
|
||||
$sql_params[] = $room_admin_email;
|
||||
break;
|
||||
case 'invalid_types':
|
||||
$assign_array[] = "invalid_types=?";
|
||||
$sql_params[] = json_encode($invalid_types);
|
||||
break;
|
||||
case 'custom_html':
|
||||
$assign_array[] = "custom_html=?";
|
||||
$sql_params[] = $custom_html;
|
||||
break;
|
||||
// then look at any user defined fields
|
||||
default:
|
||||
$var = VAR_PREFIX . $field['name'];
|
||||
switch ($field['nature'])
|
||||
{
|
||||
case 'integer':
|
||||
if (!isset($$var) || ($$var === ''))
|
||||
{
|
||||
// Try and set it to NULL when we can because there will be cases when we
|
||||
// want to distinguish between NULL and 0 - especially when the field
|
||||
// is a genuine integer.
|
||||
$$var = ($field['is_nullable']) ? null : 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Do nothing
|
||||
break;
|
||||
}
|
||||
$assign_array[] = db()->quote($field['name']) . "=?";
|
||||
$sql_params[] = $$var;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$sql .= implode(",", $assign_array) . " WHERE id=?";
|
||||
$sql_params[] = $room;
|
||||
db()->command($sql, $sql_params);
|
||||
|
||||
// Commit the transaction
|
||||
db()->commit();
|
||||
|
||||
// Go back to the admin page (for the new area)
|
||||
location_header("admin.php?day=$day&month=$month&year=$year&area=$new_area&room=$room");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Go back to the room form with errors
|
||||
$query_string = "area=$old_area&room=$room";
|
||||
foreach ($errors as $error)
|
||||
{
|
||||
$query_string .= "&errors[]=$error";
|
||||
}
|
||||
location_header("edit_room.php?$query_string");
|
||||
+1446
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user