MRBS 1.12.2 等保2.0二级整改完整提交
Docker image / push (push) Canceled after 0s

包含:登录失败锁定、90天密码有效期、30分钟会话超时、
强制改密、登录审计日志、屏幕水印、企业背景图、
备案信息固定底部、favicon、登录页JS修复等全部改动
This commit is contained in:
人事系统开发
2026-09-08 21:19:47 +08:00
commit 48092cab42
2221 changed files with 659586 additions and 0 deletions
+19
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
* text eol=lf
*.ics text eol=crlf
*.gif binary
*.png binary
*.jpg binary
+39
View File
@@ -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.
+20
View File
@@ -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.
+26
View File
@@ -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.
+53
View File
@@ -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
View File
@@ -0,0 +1,10 @@
.bak-*
*.bak
*.legacy-*
*.smokebak*
.DS_Store
Thumbs.db
vendor/
node_modules/
*.log
*.tmp
+18
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+339
View File
@@ -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.
+110892
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -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
+564
View File
@@ -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.
+36
View File
@@ -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 &nbsp;. If you need to use any other characters you should
just encode those characters in utf-8.
+158
View File
@@ -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.
+4274
View File
File diff suppressed because it is too large Load Diff
+56
View File
@@ -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
View File
@@ -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.
+150
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+3
View File
@@ -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
+63
View File
@@ -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;
+23
View File
@@ -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;
+78
View File
@@ -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. */
+59
View File
@@ -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
~~~
+38
View File
@@ -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:
+6
View File
@@ -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
+11
View File
@@ -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;
+103
View File
@@ -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";
}
}
+25
View File
@@ -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;
+296
View File
@@ -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&amp;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&amp;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&amp;typematch[]=Y&amp;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&amp;output=1&amp;output_format=2&amp;from_date=2019-01-01&amp;to_date=2020-01-01&amp;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\&amp;output=1\&amp;output_format=2\&amp;sumby=c
</code>
</p>
</body>
</html>
+60
View File
@@ -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
+29
View File
@@ -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));
+26
View File
@@ -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;
+848
View File
@@ -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']));
}
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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');
+52
View File
@@ -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;
}
+246
View File
@@ -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();
+504
View File
@@ -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();
+79
View File
@@ -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();
+3
View File
@@ -0,0 +1,3 @@
<Files ~ "\.inc$">
Require all denied
</Files>
+4
View File
@@ -0,0 +1,4 @@
<Files ~ "\.inc$">
Order allow,deny
Deny from all
</Files>
+411
View File
@@ -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);
}
}
+381
View File
@@ -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);
}
}
+348
View File
@@ -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);
}
}
+370
View File
@@ -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;
}
}
+587
View File
@@ -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;
}
}
?>
+304
View File
@@ -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);
}
}
?>
+2
View File
@@ -0,0 +1,2 @@
<?php
class File_Passwd_Exception extends Exception {}
+415
View File
@@ -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);
}
}
?>
+652
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1138
View File
File diff suppressed because it is too large Load Diff
+118
View File
@@ -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";
+47
View File
@@ -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 = "&#169; 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>
&nbsp;|&nbsp;
公安网安备案号:<img src="{$gongan_icon}" alt="公安备案图标"> <a href="{$gongan_url}" target="_blank">{$gongan_number}</a>
&nbsp;|&nbsp;
{$copyright}
</div>
HTML;
echo "</div>\n"; // closing the contents div, opened in print_theme_header()
echo "</body>\n";
echo "</html>\n";
}
+741
View File
@@ -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()
+673
View File
@@ -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()
+129
View File
@@ -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
View File
@@ -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
View File
@@ -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 &nbsp;)
// (b) $text could contain the span above.
echo "<th>$text</th>\n";
}
}
if (is_admin())
{
echo "<th>&nbsp;</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\">" : "&nbsp;") . "</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\">" : "&nbsp;";
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) ? '&hellip;' : '';
$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();
+48
View File
@@ -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);
+80
View File
@@ -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;
+8
View File
@@ -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';
+20
View File
@@ -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);
}
+40
View File
@@ -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);
+118
View File
@@ -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);
+333
View File
@@ -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";
+8
View File
@@ -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>
+19
View File
@@ -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';
+10
View File
@@ -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;
+276
View File
@@ -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();
+131
View File
@@ -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'; // 安全审计日志文件
+92
View File
@@ -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. */
+97
View File
@@ -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; // 使用时间段模式(而非分钟)
+10
View File
@@ -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
}
+26
View File
@@ -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 */
}
+64
View File
@@ -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;
}
+94
View File
@@ -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;
}
+169
View File
@@ -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;
}
+2911
View File
File diff suppressed because it is too large Load Diff
+136
View File
@@ -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);
}
+35
View File
@@ -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
View File
@@ -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");
+112
View File
@@ -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);
+851
View File
@@ -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();
+416
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+985
View File
@@ -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();
+125
View File
@@ -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();
+34
View File
@@ -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);
+341
View File
@@ -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();
+225
View File
@@ -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
View File
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