First commit
This commit is contained in:
commit
c6e2478c40
14
.editorconfig
Normal file
14
.editorconfig
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Drupal editor configuration normalization
|
||||||
|
# @see http://editorconfig.org/
|
||||||
|
|
||||||
|
# This is the top-most .editorconfig file; do not search in parent directories.
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# All files.
|
||||||
|
[*]
|
||||||
|
end_of_line = LF
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
# Ignore configuration files that may contain sensitive information.
|
||||||
|
sites/*/settings*.php
|
||||||
|
sites/*/civicrm.settings.php
|
||||||
|
|
||||||
|
# Ignore paths that contain user-generated content.
|
||||||
|
sites/*/files
|
||||||
|
sites/*/private
|
154
.htaccess
Normal file
154
.htaccess
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
#
|
||||||
|
# Apache/PHP/Drupal settings:
|
||||||
|
#
|
||||||
|
|
||||||
|
# Protect files and directories from prying eyes.
|
||||||
|
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock))$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
|
||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order allow,deny
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Don't show directory listings for URLs which map to a directory.
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Follow symbolic links in this directory.
|
||||||
|
Options +FollowSymLinks
|
||||||
|
|
||||||
|
# Make Drupal handle any 404 errors.
|
||||||
|
ErrorDocument 404 /index.php
|
||||||
|
|
||||||
|
# Set the default handler.
|
||||||
|
DirectoryIndex index.php index.html index.htm
|
||||||
|
|
||||||
|
# Override PHP settings that cannot be changed at runtime. See
|
||||||
|
# sites/default/default.settings.php and drupal_environment_initialize() in
|
||||||
|
# includes/bootstrap.inc for settings that can be changed at runtime.
|
||||||
|
|
||||||
|
# PHP 5, Apache 1 and 2.
|
||||||
|
<IfModule mod_php5.c>
|
||||||
|
php_flag magic_quotes_gpc off
|
||||||
|
php_flag magic_quotes_sybase off
|
||||||
|
php_flag register_globals off
|
||||||
|
php_flag session.auto_start off
|
||||||
|
php_value mbstring.http_input pass
|
||||||
|
php_value mbstring.http_output pass
|
||||||
|
php_flag mbstring.encoding_translation off
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Requires mod_expires to be enabled.
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
# Enable expirations.
|
||||||
|
ExpiresActive On
|
||||||
|
|
||||||
|
# Cache all files for 2 weeks after access (A).
|
||||||
|
ExpiresDefault A1209600
|
||||||
|
|
||||||
|
<FilesMatch \.php$>
|
||||||
|
# Do not allow PHP scripts to be cached unless they explicitly send cache
|
||||||
|
# headers themselves. Otherwise all scripts would have to overwrite the
|
||||||
|
# headers set by mod_expires if they want another caching behavior. This may
|
||||||
|
# fail if an error occurs early in the bootstrap process, and it may cause
|
||||||
|
# problems if a non-Drupal PHP file is installed in a subdirectory.
|
||||||
|
ExpiresActive Off
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Various rewrite rules.
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine on
|
||||||
|
|
||||||
|
# Set "protossl" to "s" if we were accessed via https://. This is used later
|
||||||
|
# if you enable "www." stripping or enforcement, in order to ensure that
|
||||||
|
# you don't bounce between http and https.
|
||||||
|
RewriteRule ^ - [E=protossl]
|
||||||
|
RewriteCond %{HTTPS} on
|
||||||
|
RewriteRule ^ - [E=protossl:s]
|
||||||
|
|
||||||
|
# Make sure Authorization HTTP header is available to PHP
|
||||||
|
# even when running as CGI or FastCGI.
|
||||||
|
RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||||
|
|
||||||
|
# Block access to "hidden" directories whose names begin with a period. This
|
||||||
|
# includes directories used by version control systems such as Subversion or
|
||||||
|
# Git to store control files. Files whose names begin with a period, as well
|
||||||
|
# as the control files used by CVS, are protected by the FilesMatch directive
|
||||||
|
# above.
|
||||||
|
#
|
||||||
|
# NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is
|
||||||
|
# not possible to block access to entire directories from .htaccess, because
|
||||||
|
# <DirectoryMatch> is not allowed here.
|
||||||
|
#
|
||||||
|
# If you do not have mod_rewrite installed, you should remove these
|
||||||
|
# directories from your webroot or otherwise protect them from being
|
||||||
|
# downloaded.
|
||||||
|
RewriteRule "/\.|^\.(?!well-known/)" - [F]
|
||||||
|
|
||||||
|
# If your site can be accessed both with and without the 'www.' prefix, you
|
||||||
|
# can use one of the following settings to redirect users to your preferred
|
||||||
|
# URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
|
||||||
|
#
|
||||||
|
# To redirect all users to access the site WITH the 'www.' prefix,
|
||||||
|
# (http://example.com/... will be redirected to http://www.example.com/...)
|
||||||
|
# uncomment the following:
|
||||||
|
# RewriteCond %{HTTP_HOST} .
|
||||||
|
# RewriteCond %{HTTP_HOST} !^www\. [NC]
|
||||||
|
# RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
|
||||||
|
#
|
||||||
|
# To redirect all users to access the site WITHOUT the 'www.' prefix,
|
||||||
|
# (http://www.example.com/... will be redirected to http://example.com/...)
|
||||||
|
# uncomment the following:
|
||||||
|
# RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
|
||||||
|
# RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301]
|
||||||
|
|
||||||
|
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
|
||||||
|
# VirtualDocumentRoot and the rewrite rules are not working properly.
|
||||||
|
# For example if your site is at http://example.com/drupal uncomment and
|
||||||
|
# modify the following line:
|
||||||
|
# RewriteBase /drupal
|
||||||
|
#
|
||||||
|
# If your site is running in a VirtualDocumentRoot at http://example.com/,
|
||||||
|
# uncomment the following line:
|
||||||
|
# RewriteBase /
|
||||||
|
|
||||||
|
# Pass all requests not referring directly to files in the filesystem to
|
||||||
|
# index.php. Clean URLs are handled in drupal_environment_initialize().
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_URI} !=/favicon.ico
|
||||||
|
RewriteRule ^ index.php [L]
|
||||||
|
|
||||||
|
# Rules to correctly serve gzip compressed CSS and JS files.
|
||||||
|
# Requires both mod_rewrite and mod_headers to be enabled.
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Serve gzip compressed CSS files if they exist and the client accepts gzip.
|
||||||
|
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||||
|
RewriteCond %{REQUEST_FILENAME}\.gz -s
|
||||||
|
RewriteRule ^(.*)\.css $1\.css\.gz [QSA]
|
||||||
|
|
||||||
|
# Serve gzip compressed JS files if they exist and the client accepts gzip.
|
||||||
|
RewriteCond %{HTTP:Accept-encoding} gzip
|
||||||
|
RewriteCond %{REQUEST_FILENAME}\.gz -s
|
||||||
|
RewriteRule ^(.*)\.js $1\.js\.gz [QSA]
|
||||||
|
|
||||||
|
# Serve correct content types, and prevent mod_deflate double gzip.
|
||||||
|
RewriteRule \.css\.gz$ - [T=text/css,E=no-gzip:1]
|
||||||
|
RewriteRule \.js\.gz$ - [T=text/javascript,E=no-gzip:1]
|
||||||
|
|
||||||
|
<FilesMatch "(\.js\.gz|\.css\.gz)$">
|
||||||
|
# Serve correct encoding type.
|
||||||
|
Header set Content-Encoding gzip
|
||||||
|
# Force proxies to cache gzipped & non-gzipped css/js files separately.
|
||||||
|
Header append Vary Accept-Encoding
|
||||||
|
</FilesMatch>
|
||||||
|
</IfModule>
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Add headers to all responses.
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
# Disable content sniffing, since it's an attack vector.
|
||||||
|
Header always set X-Content-Type-Options nosniff
|
||||||
|
</IfModule>
|
2304
CHANGELOG.txt
Normal file
2304
CHANGELOG.txt
Normal file
File diff suppressed because it is too large
Load diff
44
COPYRIGHT.txt
Normal file
44
COPYRIGHT.txt
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
All Drupal code is Copyright 2001 - 2013 by the original authors.
|
||||||
|
|
||||||
|
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 as the file LICENSE.txt; if not, please see
|
||||||
|
http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
|
||||||
|
|
||||||
|
Drupal is a registered trademark of Dries Buytaert.
|
||||||
|
|
||||||
|
Drupal includes works under other copyright notices and distributed
|
||||||
|
according to the terms of the GNU General Public License or a compatible
|
||||||
|
license, including:
|
||||||
|
|
||||||
|
Javascript
|
||||||
|
|
||||||
|
Farbtastic - Copyright (c) 2010 Matt Farina
|
||||||
|
|
||||||
|
jQuery - Copyright (c) 2010 John Resig
|
||||||
|
|
||||||
|
jQuery BBQ - Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
|
||||||
|
jQuery Cookie - Copyright (c) 2006 Klaus Hartl
|
||||||
|
|
||||||
|
jQuery Form - Copyright (c) 2010 Mike Alsup
|
||||||
|
|
||||||
|
jQuery Once - Copyright (c) 2009 Konstantin K<>fer
|
||||||
|
|
||||||
|
jQuery UI - Copyright (c) 2010 by the original authors
|
||||||
|
(http://jqueryui.com/about)
|
||||||
|
|
||||||
|
Sizzle.js - Copyright (c) 2010 The Dojo Foundation (http://sizzlejs.com/)
|
||||||
|
|
||||||
|
PHP
|
||||||
|
|
||||||
|
ArchiveTar - Copyright (c) 1997 - 2008 Vincent Blavet
|
45
INSTALL.mysql.txt
Normal file
45
INSTALL.mysql.txt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
CREATE THE MySQL DATABASE
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
This step is only necessary if you don't already have a database set up (e.g.,
|
||||||
|
by your host). In the following examples, 'username' is an example MySQL user
|
||||||
|
which has the CREATE and GRANT privileges. Use the appropriate user name for
|
||||||
|
your system.
|
||||||
|
|
||||||
|
First, you must create a new database for your Drupal site (here, 'databasename'
|
||||||
|
is the name of the new database):
|
||||||
|
|
||||||
|
mysqladmin -u username -p create databasename
|
||||||
|
|
||||||
|
MySQL will prompt for the 'username' database password and then create the
|
||||||
|
initial database files. Next you must log in and set the access database rights:
|
||||||
|
|
||||||
|
mysql -u username -p
|
||||||
|
|
||||||
|
Again, you will be asked for the 'username' database password. At the MySQL
|
||||||
|
prompt, enter the following command:
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
|
||||||
|
CREATE TEMPORARY TABLES ON databasename.*
|
||||||
|
TO 'username'@'localhost' IDENTIFIED BY 'password';
|
||||||
|
|
||||||
|
where:
|
||||||
|
|
||||||
|
'databasename' is the name of your database
|
||||||
|
'username' is the username of your MySQL account
|
||||||
|
'localhost' is the web server host where Drupal is installed
|
||||||
|
'password' is the password required for that username
|
||||||
|
|
||||||
|
Note: Unless the database user/host combination for your Drupal installation
|
||||||
|
has all of the privileges listed above (except possibly CREATE TEMPORARY TABLES,
|
||||||
|
which is currently only used by Drupal core automated tests and some
|
||||||
|
contributed modules), you will not be able to install or run Drupal.
|
||||||
|
|
||||||
|
If successful, MySQL will reply with:
|
||||||
|
|
||||||
|
Query OK, 0 rows affected
|
||||||
|
|
||||||
|
If the InnoDB storage engine is available, it will be used for all database
|
||||||
|
tables. InnoDB provides features over MyISAM such as transaction support,
|
||||||
|
row-level locks, and consistent non-locking reads.
|
44
INSTALL.pgsql.txt
Normal file
44
INSTALL.pgsql.txt
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
|
||||||
|
CREATE THE PostgreSQL DATABASE
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Note that the database must be created with UTF-8 (Unicode) encoding.
|
||||||
|
|
||||||
|
1. CREATE DATABASE USER
|
||||||
|
|
||||||
|
This step is only necessary if you don't already have a user set up (e.g., by
|
||||||
|
your host), or want to create a new user for use with Drupal only. The
|
||||||
|
following command creates a new user named 'username' and asks for a password
|
||||||
|
for that user:
|
||||||
|
|
||||||
|
createuser --pwprompt --encrypted --no-createrole --no-createdb username
|
||||||
|
|
||||||
|
If there are no errors, then the command was successful.
|
||||||
|
|
||||||
|
2. CREATE DRUPAL DATABASE
|
||||||
|
|
||||||
|
This step is only necessary if you don't already have a database set up
|
||||||
|
(e.g., by your host) or want to create a new database for use with Drupal
|
||||||
|
only. The following command creates a new database named 'databasename',
|
||||||
|
which is owned by the previously created 'username':
|
||||||
|
|
||||||
|
createdb --encoding=UTF8 --owner=username databasename
|
||||||
|
|
||||||
|
If there are no errors, then the command was successful.
|
||||||
|
|
||||||
|
3. CREATE SCHEMA OR SCHEMAS (Optional advanced step)
|
||||||
|
|
||||||
|
Drupal will run across different schemas within your database if you so wish.
|
||||||
|
By default, Drupal runs inside the 'public' schema but you can use $db_prefix
|
||||||
|
inside settings.php to define a schema for Drupal to run inside of, or
|
||||||
|
specify tables that are shared inside of a separate schema. Drupal will not
|
||||||
|
create schemas for you. In fact, the user that Drupal runs as should not be
|
||||||
|
allowed to do this. You'll need to execute the SQL below as a superuser,
|
||||||
|
replace 'username' with the username that Drupal uses to connect to
|
||||||
|
PostgreSQL, and replace 'schema_name' with a schema name you wish to use,
|
||||||
|
such as 'shared':
|
||||||
|
|
||||||
|
CREATE SCHEMA schema_name AUTHORIZATION username;
|
||||||
|
|
||||||
|
Do this for as many schemas as you need. See default.settings.php for
|
||||||
|
instructions on how to set which tables use which schemas.
|
31
INSTALL.sqlite.txt
Normal file
31
INSTALL.sqlite.txt
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
|
||||||
|
SQLITE REQUIREMENTS
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
To use SQLite with your Drupal installation, the following requirements must be
|
||||||
|
met: Server has PHP 5.2 or later with PDO, and the PDO SQLite driver must be
|
||||||
|
enabled.
|
||||||
|
|
||||||
|
SQLITE DATABASE CREATION
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
The Drupal installer will create the SQLite database for you. The only
|
||||||
|
requirement is that the installer must have write permissions to the directory
|
||||||
|
where the database file resides. This directory (not just the database file) also
|
||||||
|
has to remain writeable by the web server going forward for SQLite to continue to
|
||||||
|
be able to operate.
|
||||||
|
|
||||||
|
On the "Database configuration" form in the "Database file" field, you must
|
||||||
|
supply the exact path to where you wish your database file to reside. It is
|
||||||
|
strongly suggested that you choose a path that is outside of the webroot, yet
|
||||||
|
ensure that the directory is writeable by the web server.
|
||||||
|
|
||||||
|
If you must place your database file in your webroot, you could try using the
|
||||||
|
following in your "Database file" field:
|
||||||
|
|
||||||
|
sites/default/files/.ht.sqlite
|
||||||
|
|
||||||
|
Note: The .ht in the name will tell Apache to prevent the database from being
|
||||||
|
downloaded. Please check that the file is, indeed, protected by your webserver.
|
||||||
|
If not, please consult the documentation of your webserver on how to protect a
|
||||||
|
file from downloading.
|
400
INSTALL.txt
Normal file
400
INSTALL.txt
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
|
||||||
|
CONTENTS OF THIS FILE
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* Requirements and notes
|
||||||
|
* Optional server requirements
|
||||||
|
* Installation
|
||||||
|
* Building and customizing your site
|
||||||
|
* Multisite configuration
|
||||||
|
* More information
|
||||||
|
|
||||||
|
REQUIREMENTS AND NOTES
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Drupal requires:
|
||||||
|
|
||||||
|
- A web server. Apache (version 2.0 or greater) is recommended.
|
||||||
|
- PHP 5.2.4 (or greater) (http://www.php.net/).
|
||||||
|
- One of the following databases:
|
||||||
|
- MySQL 5.0.15 (or greater) (http://www.mysql.com/).
|
||||||
|
- MariaDB 5.1.44 (or greater) (http://mariadb.org/). MariaDB is a fully
|
||||||
|
compatible drop-in replacement for MySQL.
|
||||||
|
- Percona Server 5.1.70 (or greater) (http://www.percona.com/). Percona
|
||||||
|
Server is a backwards-compatible replacement for MySQL.
|
||||||
|
- PostgreSQL 8.3 (or greater) (http://www.postgresql.org/).
|
||||||
|
- SQLite 3.3.7 (or greater) (http://www.sqlite.org/).
|
||||||
|
|
||||||
|
For more detailed information about Drupal requirements, including a list of
|
||||||
|
PHP extensions and configurations that are required, see "System requirements"
|
||||||
|
(http://drupal.org/requirements) in the Drupal.org online documentation.
|
||||||
|
|
||||||
|
For detailed information on how to configure a test server environment using a
|
||||||
|
variety of operating systems and web servers, see "Local server setup"
|
||||||
|
(http://drupal.org/node/157602) in the Drupal.org online documentation.
|
||||||
|
|
||||||
|
Note that all directories mentioned in this document are always relative to the
|
||||||
|
directory of your Drupal installation, and commands are meant to be run from
|
||||||
|
this directory (except for the initial commands that create that directory).
|
||||||
|
|
||||||
|
OPTIONAL SERVER REQUIREMENTS
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
- If you want to use Drupal's "Clean URLs" feature on an Apache web server, you
|
||||||
|
will need the mod_rewrite module and the ability to use local .htaccess
|
||||||
|
files. For Clean URLs support on IIS, see "Clean URLs with IIS"
|
||||||
|
(http://drupal.org/node/3854) in the Drupal.org online documentation.
|
||||||
|
|
||||||
|
- If you plan to use XML-based services such as RSS aggregation, you will need
|
||||||
|
PHP's XML extension. This extension is enabled by default on most PHP
|
||||||
|
installations.
|
||||||
|
|
||||||
|
- To serve gzip compressed CSS and JS files on an Apache web server, you will
|
||||||
|
need the mod_headers module and the ability to use local .htaccess files.
|
||||||
|
|
||||||
|
- Some Drupal functionality (e.g., checking whether Drupal and contributed
|
||||||
|
modules need updates, RSS aggregation, etc.) require that the web server be
|
||||||
|
able to go out to the web and download information. If you want to use this
|
||||||
|
functionality, you need to verify that your hosting provider or server
|
||||||
|
configuration allows the web server to initiate outbound connections. Most web
|
||||||
|
hosting setups allow this.
|
||||||
|
|
||||||
|
INSTALLATION
|
||||||
|
------------
|
||||||
|
|
||||||
|
1. Download and extract Drupal.
|
||||||
|
|
||||||
|
You can obtain the latest Drupal release from http://drupal.org -- the files
|
||||||
|
are available in .tar.gz and .zip formats and can be extracted using most
|
||||||
|
compression tools.
|
||||||
|
|
||||||
|
To download and extract the files, on a typical Unix/Linux command line, use
|
||||||
|
the following commands (assuming you want version x.y of Drupal in .tar.gz
|
||||||
|
format):
|
||||||
|
|
||||||
|
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
|
||||||
|
tar -zxvf drupal-x.y.tar.gz
|
||||||
|
|
||||||
|
This will create a new directory drupal-x.y/ containing all Drupal files and
|
||||||
|
directories. Then, to move the contents of that directory into a directory
|
||||||
|
within your web server's document root or your public HTML directory,
|
||||||
|
continue with this command:
|
||||||
|
|
||||||
|
mv drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
|
||||||
|
|
||||||
|
2. Optionally, download a translation.
|
||||||
|
|
||||||
|
By default, Drupal is installed in English, and further languages may be
|
||||||
|
installed later. If you prefer to install Drupal in another language
|
||||||
|
initially:
|
||||||
|
|
||||||
|
- Download a translation file for the correct Drupal version and language
|
||||||
|
from the translation server: http://localize.drupal.org/translate/downloads
|
||||||
|
|
||||||
|
- Place the file into your installation profile's translations directory.
|
||||||
|
For instance, if you are using the Standard installation profile,
|
||||||
|
move the .po file into the directory:
|
||||||
|
|
||||||
|
profiles/standard/translations/
|
||||||
|
|
||||||
|
For detailed instructions, visit http://drupal.org/localize
|
||||||
|
|
||||||
|
3. Create the Drupal database.
|
||||||
|
|
||||||
|
Because Drupal stores all site information in a database, you must create
|
||||||
|
this database in order to install Drupal, and grant Drupal certain database
|
||||||
|
privileges (such as the ability to create tables). For details, consult
|
||||||
|
INSTALL.mysql.txt, INSTALL.pgsql.txt, or INSTALL.sqlite.txt. You may also
|
||||||
|
need to consult your web hosting provider for instructions specific to your
|
||||||
|
web host.
|
||||||
|
|
||||||
|
Take note of the username, password, database name, and hostname as you
|
||||||
|
create the database. You will enter this information during the install.
|
||||||
|
|
||||||
|
4. Run the install script.
|
||||||
|
|
||||||
|
To run the install script, point your browser to the base URL of your
|
||||||
|
website (e.g., http://www.example.com).
|
||||||
|
|
||||||
|
You will be guided through several screens to set up the database, add the
|
||||||
|
site maintenance account (the first user, also known as user/1), and provide
|
||||||
|
basic web site settings.
|
||||||
|
|
||||||
|
During installation, several files and directories need to be created, which
|
||||||
|
the install script will try to do automatically. However, on some hosting
|
||||||
|
environments, manual steps are required, and the install script will tell
|
||||||
|
you that it cannot proceed until you fix certain issues. This is normal and
|
||||||
|
does not indicate a problem with your server.
|
||||||
|
|
||||||
|
The most common steps you may need to perform are:
|
||||||
|
|
||||||
|
a. Missing files directory.
|
||||||
|
|
||||||
|
The install script will attempt to create a file storage directory in
|
||||||
|
the default location at sites/default/files (the location of the files
|
||||||
|
directory may be changed after Drupal is installed).
|
||||||
|
|
||||||
|
If auto-creation fails, you can make it work by changing permissions on
|
||||||
|
the sites/default directory so that the web server can create the files
|
||||||
|
directory within it for you. (If you are creating a multisite
|
||||||
|
installation, substitute the correct sites directory for sites/default;
|
||||||
|
see the Multisite Configuration section of this file, below.)
|
||||||
|
|
||||||
|
For example, on a Unix/Linux command line, you can grant everyone
|
||||||
|
(including the web server) permission to write to the sites/default
|
||||||
|
directory with this command:
|
||||||
|
|
||||||
|
chmod a+w sites/default
|
||||||
|
|
||||||
|
Be sure to set the permissions back after the installation is finished!
|
||||||
|
Sample command:
|
||||||
|
|
||||||
|
chmod go-w sites/default
|
||||||
|
|
||||||
|
Alternatively, instead of allowing the web server to create the files
|
||||||
|
directory for you as described above, you can create it yourself. Sample
|
||||||
|
commands from a Unix/Linux command line:
|
||||||
|
|
||||||
|
mkdir sites/default/files
|
||||||
|
chmod a+w sites/default/files
|
||||||
|
|
||||||
|
b. Missing settings file.
|
||||||
|
|
||||||
|
Drupal will try to automatically create a settings.php configuration file,
|
||||||
|
which is normally in the directory sites/default (to avoid problems when
|
||||||
|
upgrading, Drupal is not packaged with this file). If auto-creation fails,
|
||||||
|
you will need to create this file yourself, using the file
|
||||||
|
sites/default/default.settings.php as a template.
|
||||||
|
|
||||||
|
For example, on a Unix/Linux command line, you can make a copy of the
|
||||||
|
default.settings.php file with the command:
|
||||||
|
|
||||||
|
cp sites/default/default.settings.php sites/default/settings.php
|
||||||
|
|
||||||
|
Next, grant write privileges to the file to everyone (including the web
|
||||||
|
server) with the command:
|
||||||
|
|
||||||
|
chmod a+w sites/default/settings.php
|
||||||
|
|
||||||
|
Be sure to set the permissions back after the installation is finished!
|
||||||
|
Sample command:
|
||||||
|
|
||||||
|
chmod go-w sites/default/settings.php
|
||||||
|
|
||||||
|
c. Write permissions after install.
|
||||||
|
|
||||||
|
The install script will attempt to write-protect the settings.php file and
|
||||||
|
the sites/default directory after saving your configuration. If this
|
||||||
|
fails, you will be notified, and you can do it manually. Sample commands
|
||||||
|
from a Unix/Linux command line:
|
||||||
|
|
||||||
|
chmod go-w sites/default/settings.php
|
||||||
|
chmod go-w sites/default
|
||||||
|
|
||||||
|
5. Verify that the site is working.
|
||||||
|
|
||||||
|
When the install script finishes, you will be logged in with the site
|
||||||
|
maintenance account on a "Welcome" page. If the default Drupal theme is not
|
||||||
|
displaying properly and links on the page result in "Page Not Found" errors,
|
||||||
|
you may be experiencing problems with clean URLs. Visit
|
||||||
|
http://drupal.org/getting-started/clean-urls to troubleshoot.
|
||||||
|
|
||||||
|
6. Change file system storage settings (optional).
|
||||||
|
|
||||||
|
The files directory created in step 4 is the default file system path used to
|
||||||
|
store all uploaded files, as well as some temporary files created by
|
||||||
|
Drupal. After installation, you can modify the file system path to store
|
||||||
|
uploaded files in a different location.
|
||||||
|
|
||||||
|
It is not necessary to modify this path, but you may wish to change it if:
|
||||||
|
|
||||||
|
- Your site runs multiple Drupal installations from a single codebase (modify
|
||||||
|
the file system path of each installation to a different directory so that
|
||||||
|
uploads do not overlap between installations).
|
||||||
|
|
||||||
|
- Your site runs on a number of web servers behind a load balancer or reverse
|
||||||
|
proxy (modify the file system path on each server to point to a shared file
|
||||||
|
repository).
|
||||||
|
|
||||||
|
- You want to restrict access to uploaded files.
|
||||||
|
|
||||||
|
To modify the file system path:
|
||||||
|
|
||||||
|
a. Ensure that the new location for the path exists and is writable by the
|
||||||
|
web server. For example, to create a new directory named uploads and grant
|
||||||
|
write permissions, use the following commands on a Unix/Linux command
|
||||||
|
line:
|
||||||
|
|
||||||
|
mkdir uploads
|
||||||
|
chmod a+w uploads
|
||||||
|
|
||||||
|
b. Navigate to Administration > Configuration > Media > File system, and
|
||||||
|
enter the desired path. Note that if you want to use private file storage,
|
||||||
|
you need to first enter the path for private files and save the
|
||||||
|
configuration, and then change the "Default download method" setting and
|
||||||
|
save again.
|
||||||
|
|
||||||
|
Changing the file system path after files have been uploaded may cause
|
||||||
|
unexpected problems on an existing site. If you modify the file system path
|
||||||
|
on an existing site, remember to copy all files from the original location
|
||||||
|
to the new location.
|
||||||
|
|
||||||
|
7. Revoke documentation file permissions (optional).
|
||||||
|
|
||||||
|
Some administrators suggest making the documentation files, especially
|
||||||
|
CHANGELOG.txt, non-readable so that the exact version of Drupal you are
|
||||||
|
running is slightly more difficult to determine. If you wish to implement
|
||||||
|
this optional security measure, from a Unix/Linux command line you can use
|
||||||
|
the following command:
|
||||||
|
|
||||||
|
chmod a-r CHANGELOG.txt
|
||||||
|
|
||||||
|
Note that the example only affects CHANGELOG.txt. To completely hide all
|
||||||
|
documentation files from public view, repeat this command for each of the
|
||||||
|
Drupal documentation files in the installation directory, substituting the
|
||||||
|
name of each file for CHANGELOG.txt in the example.
|
||||||
|
|
||||||
|
For more information on setting file permissions, see "Modifying Linux,
|
||||||
|
Unix, and Mac file permissions" (http://drupal.org/node/202483) or
|
||||||
|
"Modifying Windows file permissions" (http://drupal.org/node/202491) in the
|
||||||
|
Drupal.org online documentation.
|
||||||
|
|
||||||
|
8. Set up independent "cron" maintenance jobs.
|
||||||
|
|
||||||
|
Many Drupal modules have tasks that must be run periodically, including the
|
||||||
|
Search module (building and updating the index used for keyword searching),
|
||||||
|
the Aggregator module (retrieving feeds from other sites), and the System
|
||||||
|
module (performing routine maintenance and pruning of database tables). These
|
||||||
|
tasks are known as "cron maintenance tasks", named after the Unix/Linux
|
||||||
|
"cron" utility.
|
||||||
|
|
||||||
|
When you install Drupal, its built-in cron feature is enabled, which
|
||||||
|
automatically runs the cron tasks periodically, triggered by people visiting
|
||||||
|
pages of your site. You can configure the built-in cron feature by navigating
|
||||||
|
to Administration > Configuration > System > Cron.
|
||||||
|
|
||||||
|
It is also possible to run the cron tasks independent of site visits; this is
|
||||||
|
recommended for most sites. To do this, you will need to set up an automated
|
||||||
|
process to visit the page cron.php on your site, which executes the cron
|
||||||
|
tasks.
|
||||||
|
|
||||||
|
The URL of the cron.php page requires a "cron key" to protect against
|
||||||
|
unauthorized access. Your site's cron key is automatically generated during
|
||||||
|
installation and is specific to your site. The full URL of the page, with the
|
||||||
|
cron key, is available in the "Cron maintenance tasks" section of the Status
|
||||||
|
report page at Administration > Reports > Status report.
|
||||||
|
|
||||||
|
As an example for how to set up this automated process, you can use the
|
||||||
|
crontab utility on Unix/Linux systems. The following crontab line uses the
|
||||||
|
wget command to visit the cron.php page, and runs each hour, on the hour:
|
||||||
|
|
||||||
|
0 * * * * wget -O - -q -t 1 http://example.com/cron.php?cron_key=YOURKEY
|
||||||
|
|
||||||
|
Replace the text "http://example.com/cron.php?cron_key=YOURKEY" in the
|
||||||
|
example with the full URL displayed under "Cron maintenance tasks" on the
|
||||||
|
"Status report" page.
|
||||||
|
|
||||||
|
More information about cron maintenance tasks is available at
|
||||||
|
http://drupal.org/cron, and sample cron shell scripts can be found in the
|
||||||
|
scripts/ directory. (Note that these scripts must be customized like the
|
||||||
|
above example, to add your site-specific cron key and domain name.)
|
||||||
|
|
||||||
|
BUILDING AND CUSTOMIZING YOUR SITE
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
A new installation of Drupal defaults to a very basic configuration. To extend
|
||||||
|
your site, you use "modules" and "themes". A module is a plugin that adds
|
||||||
|
functionality to Drupal, while a theme changes the look of your site. The core
|
||||||
|
of Drupal provides several optional modules and themes, and you can download
|
||||||
|
more at http://drupal.org/project/modules and http://drupal.org/project/themes
|
||||||
|
|
||||||
|
Do not mix downloaded or custom modules and themes with Drupal's core modules
|
||||||
|
and themes. Drupal's modules and themes are located in the top-level modules and
|
||||||
|
themes directories, while the modules and themes you add to Drupal are normally
|
||||||
|
placed in the sites/all/modules and sites/all/themes directories. If you run a
|
||||||
|
multisite installation, you can also place modules and themes in the
|
||||||
|
site-specific directories -- see the Multisite Configuration section, below.
|
||||||
|
|
||||||
|
Never edit Drupal's core modules and themes; instead, use the hooks available in
|
||||||
|
the Drupal API. To modify the behavior of Drupal, develop a module as described
|
||||||
|
at http://drupal.org/developing/modules. To modify the look of Drupal, create a
|
||||||
|
subtheme as described at http://drupal.org/node/225125, or a completely new
|
||||||
|
theme as described at http://drupal.org/documentation/theme
|
||||||
|
|
||||||
|
MULTISITE CONFIGURATION
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
A single Drupal installation can host several Drupal-powered sites, each with
|
||||||
|
its own individual configuration.
|
||||||
|
|
||||||
|
Additional site configurations are created in subdirectories within the 'sites'
|
||||||
|
directory. Each subdirectory must have a 'settings.php' file, which specifies
|
||||||
|
the configuration settings. The easiest way to create additional sites is to
|
||||||
|
copy the 'default' directory and modify the 'settings.php' file as appropriate.
|
||||||
|
The new directory name is constructed from the site's URL. The configuration for
|
||||||
|
www.example.com could be in 'sites/example.com/settings.php' (note that 'www.'
|
||||||
|
should be omitted if users can access your site at http://example.com/).
|
||||||
|
|
||||||
|
Sites do not have to have a different domain. You can also use subdomains and
|
||||||
|
subdirectories for Drupal sites. For example, example.com, sub.example.com, and
|
||||||
|
sub.example.com/site3 can all be defined as independent Drupal sites. The setup
|
||||||
|
for a configuration such as this would look like the following:
|
||||||
|
|
||||||
|
sites/default/settings.php
|
||||||
|
sites/example.com/settings.php
|
||||||
|
sites/sub.example.com/settings.php
|
||||||
|
sites/sub.example.com.site3/settings.php
|
||||||
|
|
||||||
|
When searching for a site configuration (for example www.sub.example.com/site3),
|
||||||
|
Drupal will search for configuration files in the following order, using the
|
||||||
|
first configuration it finds:
|
||||||
|
|
||||||
|
sites/www.sub.example.com.site3/settings.php
|
||||||
|
sites/sub.example.com.site3/settings.php
|
||||||
|
sites/example.com.site3/settings.php
|
||||||
|
sites/www.sub.example.com/settings.php
|
||||||
|
sites/sub.example.com/settings.php
|
||||||
|
sites/example.com/settings.php
|
||||||
|
sites/default/settings.php
|
||||||
|
|
||||||
|
If you are installing on a non-standard port, the port number is treated as the
|
||||||
|
deepest subdomain. For example: http://www.example.com:8080/ could be loaded
|
||||||
|
from sites/8080.www.example.com/. The port number will be removed according to
|
||||||
|
the pattern above if no port-specific configuration is found, just like a real
|
||||||
|
subdomain.
|
||||||
|
|
||||||
|
Each site configuration can have its own site-specific modules and themes in
|
||||||
|
addition to those installed in the standard 'modules' and 'themes' directories.
|
||||||
|
To use site-specific modules or themes, simply create a 'modules' or 'themes'
|
||||||
|
directory within the site configuration directory. For example, if
|
||||||
|
sub.example.com has a custom theme and a custom module that should not be
|
||||||
|
accessible to other sites, the setup would look like this:
|
||||||
|
|
||||||
|
sites/sub.example.com/
|
||||||
|
settings.php
|
||||||
|
themes/custom_theme
|
||||||
|
modules/custom_module
|
||||||
|
|
||||||
|
NOTE: for more information about multiple virtual hosts or the configuration
|
||||||
|
settings, consult http://drupal.org/getting-started/6/install/multi-site
|
||||||
|
|
||||||
|
For more information on configuring Drupal's file system path in a multisite
|
||||||
|
configuration, see step 6 above.
|
||||||
|
|
||||||
|
MORE INFORMATION
|
||||||
|
----------------
|
||||||
|
|
||||||
|
- See the Drupal.org online documentation:
|
||||||
|
http://drupal.org/documentation
|
||||||
|
|
||||||
|
- For a list of security announcements, see the "Security advisories" page at
|
||||||
|
http://drupal.org/security (available as an RSS feed). This page also
|
||||||
|
describes how to subscribe to these announcements via e-mail.
|
||||||
|
|
||||||
|
- For information about the Drupal security process, or to find out how to
|
||||||
|
report a potential security issue to the Drupal security team, see the
|
||||||
|
"Security team" page at http://drupal.org/security-team
|
||||||
|
|
||||||
|
- For information about the wide range of available support options, visit
|
||||||
|
http://drupal.org and click on Community and Support in the top or bottom
|
||||||
|
navigation.
|
339
LICENSE.txt
Normal file
339
LICENSE.txt
Normal file
|
@ -0,0 +1,339 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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 Lesser 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
|
||||||
|
|
||||||
|
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) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 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.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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) year 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 Lesser General
|
||||||
|
Public License instead of this License.
|
307
MAINTAINERS.txt
Normal file
307
MAINTAINERS.txt
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
|
||||||
|
Drupal core is built and maintained by the Drupal project community. Everyone is
|
||||||
|
encouraged to submit issues and changes (patches) to improve Drupal, and to
|
||||||
|
contribute in other ways -- see https://www.drupal.org/contribute to find out
|
||||||
|
how.
|
||||||
|
|
||||||
|
Branch maintainers
|
||||||
|
------------------
|
||||||
|
|
||||||
|
The Drupal Core branch maintainers oversee the development of Drupal as a whole.
|
||||||
|
The branch maintainers for Drupal 7 are:
|
||||||
|
|
||||||
|
- Dries Buytaert 'dries' https://www.drupal.org/u/dries
|
||||||
|
- Angela Byron 'webchick' https://www.drupal.org/u/webchick
|
||||||
|
- Fabian Franz 'Fabianx' https://www.drupal.org/u/fabianx
|
||||||
|
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
|
||||||
|
- Stefan Ruijsenaars 'stefan.r' https://www.drupal.org/u/stefanr-0
|
||||||
|
|
||||||
|
|
||||||
|
Component maintainers
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The Drupal Core component maintainers oversee the development of Drupal
|
||||||
|
subsystems. See https://www.drupal.org/contribute/core-maintainers for more
|
||||||
|
information on their responsibilities, and to find out how to become a component
|
||||||
|
maintainer. Current component maintainers for Drupal 7:
|
||||||
|
|
||||||
|
Ajax system
|
||||||
|
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
|
||||||
|
- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
|
||||||
|
|
||||||
|
Base system
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
|
||||||
|
|
||||||
|
Batch system
|
||||||
|
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
|
||||||
|
|
||||||
|
Cache system
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
|
||||||
|
|
||||||
|
Cron system
|
||||||
|
- Derek Wright 'dww' https://www.drupal.org/u/dww
|
||||||
|
|
||||||
|
Database system
|
||||||
|
- Larry Garfield 'Crell' https://www.drupal.org/u/crell
|
||||||
|
|
||||||
|
- MySQL driver
|
||||||
|
- Larry Garfield 'Crell' https://www.drupal.org/u/crell
|
||||||
|
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
|
||||||
|
|
||||||
|
- PostgreSQL driver
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
- Josh Waihi 'fiasco' https://www.drupal.org/u/josh-waihi
|
||||||
|
|
||||||
|
- Sqlite driver
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
|
||||||
|
Database update system
|
||||||
|
- Ashok Modi 'BTMash' https://www.drupal.org/u/btmash
|
||||||
|
|
||||||
|
Entity system
|
||||||
|
- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
|
||||||
|
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
|
||||||
|
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
|
||||||
|
|
||||||
|
File system
|
||||||
|
- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
|
||||||
|
- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
|
||||||
|
|
||||||
|
Form system
|
||||||
|
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
|
||||||
|
- Wolfgang Ziegler 'fago' https://www.drupal.org/u/fago
|
||||||
|
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
|
||||||
|
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
|
||||||
|
|
||||||
|
Image system
|
||||||
|
- Andrew Morton 'drewish' https://www.drupal.org/u/drewish
|
||||||
|
- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
|
||||||
|
|
||||||
|
Install system
|
||||||
|
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
|
||||||
|
|
||||||
|
JavaScript
|
||||||
|
- Théodore Biadala 'nod_' https://www.drupal.org/u/nod_
|
||||||
|
- Steve De Jonghe 'seutje' https://www.drupal.org/u/seutje
|
||||||
|
|
||||||
|
Language system
|
||||||
|
- Francesco Placella 'plach' https://www.drupal.org/u/plach
|
||||||
|
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
|
||||||
|
|
||||||
|
Lock system
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
|
||||||
|
Mail system
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Markup
|
||||||
|
- Jacine Luisi 'Jacine' https://www.drupal.org/u/jacine
|
||||||
|
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
|
||||||
|
|
||||||
|
Menu system
|
||||||
|
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
|
||||||
|
|
||||||
|
Path system
|
||||||
|
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
|
||||||
|
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
|
||||||
|
|
||||||
|
Render system
|
||||||
|
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
|
||||||
|
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
|
||||||
|
- Franz Heinzmann 'Frando' https://www.drupal.org/u/frando
|
||||||
|
|
||||||
|
Theme system
|
||||||
|
- Earl Miles 'merlinofchaos' https://www.drupal.org/u/merlinofchaos
|
||||||
|
- Alex Bronstein 'effulgentsia' https://www.drupal.org/u/effulgentsia
|
||||||
|
- Joon Park 'dvessel' https://www.drupal.org/u/dvessel
|
||||||
|
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
|
||||||
|
|
||||||
|
Token system
|
||||||
|
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
|
||||||
|
|
||||||
|
XML-RPC system
|
||||||
|
- Frederic G. Marand 'fgm' https://www.drupal.org/u/fgm
|
||||||
|
|
||||||
|
|
||||||
|
Topic coordinators
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Accessibility
|
||||||
|
- Everett Zufelt 'Everett Zufelt' https://www.drupal.org/u/everett-zufelt
|
||||||
|
- Brandon Bowersox-Johnson 'bowersox' https://www.drupal.org/u/bowersox
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
- Jennifer Hodgdon 'jhodgdon' https://www.drupal.org/u/jhodgdon
|
||||||
|
|
||||||
|
Translations
|
||||||
|
- Gerhard Killesreiter 'killes' https://www.drupal.org/u/gerhard-killesreiter
|
||||||
|
|
||||||
|
User experience and usability
|
||||||
|
- Roy Scholten 'yoroy' https://www.drupal.org/u/yoroy
|
||||||
|
- Bojhan Somers 'Bojhan' https://www.drupal.org/u/bojhan
|
||||||
|
|
||||||
|
Node Access
|
||||||
|
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
|
||||||
|
- Ken Rickard 'agentrickard' https://www.drupal.org/u/agentrickard
|
||||||
|
|
||||||
|
|
||||||
|
Security team
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
To report a security issue, see: https://www.drupal.org/security-team/report-issue
|
||||||
|
|
||||||
|
The Drupal security team provides Security Advisories for vulnerabilities,
|
||||||
|
assists developers in resolving security issues, and provides security
|
||||||
|
documentation. See https://www.drupal.org/security-team for more information.
|
||||||
|
The security team lead is:
|
||||||
|
|
||||||
|
- Michael Hess 'mlhess' https://www.drupal.org/u/mlhess
|
||||||
|
|
||||||
|
|
||||||
|
Module maintainers
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Aggregator module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Block module
|
||||||
|
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
|
||||||
|
|
||||||
|
Blog module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Book module
|
||||||
|
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
|
||||||
|
|
||||||
|
Color module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Comment module
|
||||||
|
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
|
||||||
|
|
||||||
|
Contact module
|
||||||
|
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
|
||||||
|
|
||||||
|
Contextual module
|
||||||
|
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
|
||||||
|
|
||||||
|
Dashboard module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Database logging module
|
||||||
|
- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
|
||||||
|
|
||||||
|
Field module
|
||||||
|
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
|
||||||
|
- Barry Jaspan 'bjaspan' https://www.drupal.org/u/bjaspan
|
||||||
|
|
||||||
|
Field UI module
|
||||||
|
- Yves Chedemois 'yched' https://www.drupal.org/u/yched
|
||||||
|
|
||||||
|
File module
|
||||||
|
- Aaron Winborn 'aaron' https://www.drupal.org/u/aaron
|
||||||
|
|
||||||
|
Filter module
|
||||||
|
- Daniel F. Kudwien 'sun' https://www.drupal.org/u/sun
|
||||||
|
|
||||||
|
Forum module
|
||||||
|
- Lee Rowlands 'larowlan' https://www.drupal.org/u/larowlan
|
||||||
|
|
||||||
|
Help module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Image module
|
||||||
|
- Nathan Haug 'quicksketch' https://www.drupal.org/u/quicksketch
|
||||||
|
|
||||||
|
Locale module
|
||||||
|
- Gábor Hojtsy 'Gábor Hojtsy' https://www.drupal.org/u/gábor-hojtsy
|
||||||
|
|
||||||
|
Menu module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Node module
|
||||||
|
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
|
||||||
|
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
|
||||||
|
|
||||||
|
OpenID module
|
||||||
|
- Vojtech Kusy 'wojtha' https://www.drupal.org/u/wojtha
|
||||||
|
- Christian Schmidt 'c960657' https://www.drupal.org/u/c960657
|
||||||
|
- Damien Tournoud 'DamZ' https://www.drupal.org/u/damien-tournoud
|
||||||
|
|
||||||
|
Overlay module
|
||||||
|
- Katherine Senzee 'ksenzee' https://www.drupal.org/u/ksenzee
|
||||||
|
|
||||||
|
Path module
|
||||||
|
- Dave Reid 'davereid' https://www.drupal.org/u/dave-reid
|
||||||
|
|
||||||
|
PHP module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Poll module
|
||||||
|
- Andrei Mateescu 'amateescu' https://www.drupal.org/u/amateescu
|
||||||
|
|
||||||
|
Profile module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
RDF module
|
||||||
|
- Stéphane Corlosquet 'scor' https://www.drupal.org/u/scor
|
||||||
|
|
||||||
|
Search module
|
||||||
|
- Doug Green 'douggreen' https://www.drupal.org/u/douggreen
|
||||||
|
|
||||||
|
Shortcut module
|
||||||
|
- David Rothstein 'David_Rothstein' https://www.drupal.org/u/david_rothstein
|
||||||
|
|
||||||
|
Simpletest module
|
||||||
|
- Jimmy Berry 'boombatower' https://www.drupal.org/u/boombatower
|
||||||
|
|
||||||
|
Statistics module
|
||||||
|
- Tim Millwood 'timmillwood' https://www.drupal.org/u/timmillwood
|
||||||
|
|
||||||
|
Syslog module
|
||||||
|
- Khalid Baheyeldin 'kbahey' https://www.drupal.org/u/kbahey
|
||||||
|
|
||||||
|
System module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Taxonomy module
|
||||||
|
- Nathaniel Catchpole 'catch' https://www.drupal.org/u/catch
|
||||||
|
- Benjamin Doherty 'bangpound' https://www.drupal.org/u/bangpound
|
||||||
|
|
||||||
|
Toolbar module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Tracker module
|
||||||
|
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
|
||||||
|
|
||||||
|
Translation module
|
||||||
|
- Francesco Placella 'plach' https://www.drupal.org/u/plach
|
||||||
|
|
||||||
|
Trigger module
|
||||||
|
- ?
|
||||||
|
|
||||||
|
Update module
|
||||||
|
- Derek Wright 'dww' https://www.drupal.org/u/dww
|
||||||
|
|
||||||
|
User module
|
||||||
|
- Moshe Weitzman 'moshe weitzman' https://www.drupal.org/u/moshe-weitzman
|
||||||
|
- David Strauss 'David Strauss' https://www.drupal.org/u/david-strauss
|
||||||
|
|
||||||
|
|
||||||
|
Theme maintainers
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Bartik theme
|
||||||
|
- Jen Simmons 'jensimmons' https://www.drupal.org/u/jensimmons
|
||||||
|
- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
|
||||||
|
|
||||||
|
Garland theme
|
||||||
|
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
|
||||||
|
|
||||||
|
Seven theme
|
||||||
|
- Jeff Burns 'Jeff Burnz' https://www.drupal.org/u/jeff-burnz
|
||||||
|
|
||||||
|
Stark theme
|
||||||
|
- John Albin Wilkins 'JohnAlbin' https://www.drupal.org/u/johnalbin
|
123
README.txt
Normal file
123
README.txt
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
|
||||||
|
CONTENTS OF THIS FILE
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
* About Drupal
|
||||||
|
* Configuration and features
|
||||||
|
* Installation profiles
|
||||||
|
* Appearance
|
||||||
|
* Developing for Drupal
|
||||||
|
|
||||||
|
ABOUT DRUPAL
|
||||||
|
------------
|
||||||
|
|
||||||
|
Drupal is an open source content management platform supporting a variety of
|
||||||
|
websites ranging from personal weblogs to large community-driven websites. For
|
||||||
|
more information, see the Drupal website at http://drupal.org/, and join the
|
||||||
|
Drupal community at http://drupal.org/community.
|
||||||
|
|
||||||
|
Legal information about Drupal:
|
||||||
|
* Know your rights when using Drupal:
|
||||||
|
See LICENSE.txt in the same directory as this document.
|
||||||
|
* Learn about the Drupal trademark and logo policy:
|
||||||
|
http://drupal.com/trademark
|
||||||
|
|
||||||
|
CONFIGURATION AND FEATURES
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Drupal core (what you get when you download and extract a drupal-x.y.tar.gz or
|
||||||
|
drupal-x.y.zip file from http://drupal.org/project/drupal) has what you need to
|
||||||
|
get started with your website. It includes several modules (extensions that add
|
||||||
|
functionality) for common website features, such as managing content, user
|
||||||
|
accounts, image uploading, and search. Core comes with many options that allow
|
||||||
|
site-specific configuration. In addition to the core modules, there are
|
||||||
|
thousands of contributed modules (for functionality not included with Drupal
|
||||||
|
core) available for download.
|
||||||
|
|
||||||
|
More about configuration:
|
||||||
|
* Install, upgrade, and maintain Drupal:
|
||||||
|
See INSTALL.txt and UPGRADE.txt in the same directory as this document.
|
||||||
|
* Learn about how to use Drupal to create your site:
|
||||||
|
http://drupal.org/documentation
|
||||||
|
* Download contributed modules to sites/all/modules to extend Drupal's
|
||||||
|
functionality:
|
||||||
|
http://drupal.org/project/modules
|
||||||
|
* See also: "Developing for Drupal" for writing your own modules, below.
|
||||||
|
|
||||||
|
INSTALLATION PROFILES
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Installation profiles define additional steps (such as enabling modules,
|
||||||
|
defining content types, etc.) that run after the base installation provided
|
||||||
|
by core when Drupal is first installed. There are two basic installation
|
||||||
|
profiles provided with Drupal core.
|
||||||
|
|
||||||
|
Installation profiles from the Drupal community modify the installation process
|
||||||
|
to provide a website for a specific use case, such as a CMS for media
|
||||||
|
publishers, a web-based project tracking tool, or a full-fledged CRM for
|
||||||
|
non-profit organizations raising money and accepting donations. They can be
|
||||||
|
distributed as bare installation profiles or as "distributions". Distributions
|
||||||
|
include Drupal core, the installation profile, and all other required
|
||||||
|
extensions, such as contributed and custom modules, themes, and third-party
|
||||||
|
libraries. Bare installation profiles require you to download Drupal Core and
|
||||||
|
the required extensions separately; place the downloaded profile in the
|
||||||
|
/profiles directory before you start the installation process. Note that the
|
||||||
|
contents of this directory may be overwritten during updates of Drupal core;
|
||||||
|
it is advised to keep code backups or use a version control system.
|
||||||
|
|
||||||
|
Additionally, modules and themes may be placed inside subdirectories in a
|
||||||
|
specific installation profile such as profiles/your_site_profile/modules and
|
||||||
|
profiles/your_site_profile/themes respectively to restrict their usage to only
|
||||||
|
sites that were installed with that specific profile.
|
||||||
|
|
||||||
|
More about installation profiles and distributions:
|
||||||
|
* Read about the difference between installation profiles and distributions:
|
||||||
|
http://drupal.org/node/1089736
|
||||||
|
* Download contributed installation profiles and distributions:
|
||||||
|
http://drupal.org/project/distributions
|
||||||
|
* Develop your own installation profile or distribution:
|
||||||
|
http://drupal.org/developing/distributions
|
||||||
|
|
||||||
|
APPEARANCE
|
||||||
|
----------
|
||||||
|
|
||||||
|
In Drupal, the appearance of your site is set by the theme (themes are
|
||||||
|
extensions that set fonts, colors, and layout). Drupal core comes with several
|
||||||
|
themes. More themes are available for download, and you can also create your own
|
||||||
|
custom theme.
|
||||||
|
|
||||||
|
More about themes:
|
||||||
|
* Download contributed themes to sites/all/themes to modify Drupal's
|
||||||
|
appearance:
|
||||||
|
http://drupal.org/project/themes
|
||||||
|
* Develop your own theme:
|
||||||
|
http://drupal.org/documentation/theme
|
||||||
|
|
||||||
|
DEVELOPING FOR DRUPAL
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Drupal contains an extensive API that allows you to add to and modify the
|
||||||
|
functionality of your site. The API consists of "hooks", which allow modules to
|
||||||
|
react to system events and customize Drupal's behavior, and functions that
|
||||||
|
standardize common operations such as database queries and form generation. The
|
||||||
|
flexible hook architecture means that you should never need to directly modify
|
||||||
|
the files that come with Drupal core to achieve the functionality you want;
|
||||||
|
instead, functionality modifications take the form of modules.
|
||||||
|
|
||||||
|
When you need new functionality for your Drupal site, search for existing
|
||||||
|
contributed modules. If you find a module that matches except for a bug or an
|
||||||
|
additional needed feature, change the module and contribute your improvements
|
||||||
|
back to the project in the form of a "patch". Create new custom modules only
|
||||||
|
when nothing existing comes close to what you need.
|
||||||
|
|
||||||
|
More about developing:
|
||||||
|
* Search for existing contributed modules:
|
||||||
|
http://drupal.org/project/modules
|
||||||
|
* Contribute a patch:
|
||||||
|
http://drupal.org/patch/submit
|
||||||
|
* Develop your own module:
|
||||||
|
http://drupal.org/developing/modules
|
||||||
|
* Follow best practices:
|
||||||
|
http://drupal.org/best-practices
|
||||||
|
* Refer to the API documentation:
|
||||||
|
http://api.drupal.org/api/drupal/7
|
246
UPGRADE.txt
Normal file
246
UPGRADE.txt
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
INTRODUCTION
|
||||||
|
------------
|
||||||
|
This document describes how to:
|
||||||
|
|
||||||
|
* Update your Drupal site from one minor 7.x version to another minor 7.x
|
||||||
|
version; for example, from 7.8 to 7.9, or from 7.6 to 7.10.
|
||||||
|
|
||||||
|
* Upgrade your Drupal site's major version from 6.x to 7.x.
|
||||||
|
|
||||||
|
First steps and definitions:
|
||||||
|
|
||||||
|
* If you are upgrading to Drupal version x.y, then x is known as the major
|
||||||
|
version number, and y is known as the minor version number. The download
|
||||||
|
file will be named drupal-x.y.tar.gz (or drupal-x.y.zip).
|
||||||
|
|
||||||
|
* All directories mentioned in this document are relative to the directory of
|
||||||
|
your Drupal installation.
|
||||||
|
|
||||||
|
* Make a full backup of all files, directories, and your database(s) before
|
||||||
|
starting, and save it outside your Drupal installation directory.
|
||||||
|
Instructions may be found at http://drupal.org/upgrade/backing-up-the-db
|
||||||
|
|
||||||
|
* It is wise to try an update or upgrade on a test copy of your site before
|
||||||
|
applying it to your live site. Even minor updates can cause your site's
|
||||||
|
behavior to change.
|
||||||
|
|
||||||
|
* Each new release of Drupal has release notes, which explain the changes made
|
||||||
|
since the previous version and any special instructions needed to update or
|
||||||
|
upgrade to the new version. You can find a link to the release notes for the
|
||||||
|
version you are upgrading or updating to on the Drupal project page
|
||||||
|
(http://drupal.org/project/drupal).
|
||||||
|
|
||||||
|
UPGRADE PROBLEMS
|
||||||
|
----------------
|
||||||
|
If you encounter errors during this process,
|
||||||
|
|
||||||
|
* Note any error messages you see.
|
||||||
|
|
||||||
|
* Restore your site to its previous state, using the file and database backups
|
||||||
|
you created before you started the upgrade process. Do not attempt to do
|
||||||
|
further upgrades on a site that had update problems.
|
||||||
|
|
||||||
|
* Consult one of the support options listed on http://drupal.org/support
|
||||||
|
|
||||||
|
More in-depth information on upgrading can be found at http://drupal.org/upgrade
|
||||||
|
|
||||||
|
MINOR VERSION UPDATES
|
||||||
|
---------------------
|
||||||
|
To update from one minor 7.x version of Drupal to any later 7.x version, after
|
||||||
|
following the instructions in the INTRODUCTION section at the top of this file:
|
||||||
|
|
||||||
|
1. Log in as a user with the permission "Administer software updates".
|
||||||
|
|
||||||
|
2. Go to Administration > Configuration > Development > Maintenance mode.
|
||||||
|
Enable the "Put site into maintenance mode" checkbox and save the
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
3. Remove all old core files and directories, except for the 'sites' directory
|
||||||
|
and any custom files you added elsewhere.
|
||||||
|
|
||||||
|
If you made modifications to files like .htaccess or robots.txt, you will
|
||||||
|
need to re-apply them from your backup, after the new files are in place.
|
||||||
|
|
||||||
|
Sometimes an update includes changes to default.settings.php (this will be
|
||||||
|
noted in the release notes). If that's the case, follow these steps:
|
||||||
|
|
||||||
|
- Locate your settings.php file in the /sites/* directory. (Typically
|
||||||
|
sites/default.)
|
||||||
|
|
||||||
|
- Make a backup copy of your settings.php file, with a different file name.
|
||||||
|
|
||||||
|
- Make a copy of the new default.settings.php file, and name the copy
|
||||||
|
settings.php (overwriting your previous settings.php file).
|
||||||
|
|
||||||
|
- Copy the custom and site-specific entries from the backup you made into the
|
||||||
|
new settings.php file. You will definitely need the lines giving the
|
||||||
|
database information, and you will also want to copy in any other
|
||||||
|
customizations you have added.
|
||||||
|
|
||||||
|
You can find the release notes for your version at
|
||||||
|
https://www.drupal.org/project/drupal. At bottom of the project page under
|
||||||
|
"Downloads" use the link for your version of Drupal to view the release
|
||||||
|
notes. If your version is not listed, use the 'View all releases' link. From
|
||||||
|
this page you can scroll down or use the filter to find your version and its
|
||||||
|
release notes.
|
||||||
|
|
||||||
|
4. Download the latest Drupal 7.x release from http://drupal.org to a
|
||||||
|
directory outside of your web root. Extract the archive and copy the files
|
||||||
|
into your Drupal directory.
|
||||||
|
|
||||||
|
On a typical Unix/Linux command line, use the following commands to download
|
||||||
|
and extract:
|
||||||
|
|
||||||
|
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
|
||||||
|
tar -zxvf drupal-x.y.tar.gz
|
||||||
|
|
||||||
|
This creates a new directory drupal-x.y/ containing all Drupal files and
|
||||||
|
directories. Copy the files into your Drupal installation directory:
|
||||||
|
|
||||||
|
cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
|
||||||
|
|
||||||
|
If you do not have command line access to your server, download the archive
|
||||||
|
from http://drupal.org using your web browser, extract it, and then use an
|
||||||
|
FTP client to upload the files to your web root.
|
||||||
|
|
||||||
|
5. Re-apply any modifications to files such as .htaccess or robots.txt.
|
||||||
|
|
||||||
|
6. Run update.php by visiting http://www.example.com/update.php (replace
|
||||||
|
www.example.com with your domain name). This will update the core database
|
||||||
|
tables.
|
||||||
|
|
||||||
|
If you are unable to access update.php do the following:
|
||||||
|
|
||||||
|
- Open settings.php with a text editor.
|
||||||
|
|
||||||
|
- Find the line that says:
|
||||||
|
$update_free_access = FALSE;
|
||||||
|
|
||||||
|
- Change it into:
|
||||||
|
$update_free_access = TRUE;
|
||||||
|
|
||||||
|
- Once the upgrade is done, $update_free_access must be reverted to FALSE.
|
||||||
|
|
||||||
|
7. Go to Administration > Reports > Status report. Verify that everything is
|
||||||
|
working as expected.
|
||||||
|
|
||||||
|
8. Ensure that $update_free_access is FALSE in settings.php.
|
||||||
|
|
||||||
|
9. Go to Administration > Configuration > Development > Maintenance mode.
|
||||||
|
Disable the "Put site into maintenance mode" checkbox and save the
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
MAJOR VERSION UPGRADE
|
||||||
|
---------------------
|
||||||
|
To upgrade from a previous major version of Drupal to Drupal 7.x, after
|
||||||
|
following the instructions in the INTRODUCTION section at the top of this file:
|
||||||
|
|
||||||
|
1. Check on the Drupal 7 status of your contributed and custom modules and
|
||||||
|
themes. See http://drupal.org/node/948216 for information on upgrading
|
||||||
|
contributed modules and themes. See http://drupal.org/node/895314 for a list
|
||||||
|
of modules that have been moved into core for Drupal 7, and instructions on
|
||||||
|
how to update them. See http://drupal.org/update/modules for information on
|
||||||
|
how to update your custom modules, and http://drupal.org/update/theme for
|
||||||
|
custom themes.
|
||||||
|
|
||||||
|
You may decide at this point that you cannot upgrade your site, because
|
||||||
|
needed modules or themes are not ready for Drupal 7.
|
||||||
|
|
||||||
|
2. Update to the latest available version of Drupal 6.x (if your current version
|
||||||
|
is Drupal 5.x, you have to upgrade to 6.x first). If you need to update,
|
||||||
|
download Drupal 6.x and follow the instructions in its UPGRADE.txt. This
|
||||||
|
document only applies for upgrades from 6.x to 7.x.
|
||||||
|
|
||||||
|
3. In addition to updating to the latest available version of Drupal 6.x core,
|
||||||
|
you must also upgrade all of your contributed modules for Drupal to their
|
||||||
|
latest Drupal 6.x versions.
|
||||||
|
|
||||||
|
4. Log in as user ID 1 (the site maintenance user).
|
||||||
|
|
||||||
|
5. Go to Administer > Site configuration > Site maintenance. Select
|
||||||
|
"Off-line" and save the configuration.
|
||||||
|
|
||||||
|
6. Go to Administer > Site building > Themes. Enable "Garland" and select it as
|
||||||
|
the default theme.
|
||||||
|
|
||||||
|
7. Go to Administer > Site building > Modules. Disable all modules that are not
|
||||||
|
listed under "Core - required" or "Core - optional". It is possible that some
|
||||||
|
modules cannot be disabled, because others depend on them. Repeat this step
|
||||||
|
until all non-core modules are disabled.
|
||||||
|
|
||||||
|
If you know that you will not re-enable some modules for Drupal 7.x and you
|
||||||
|
no longer need their data, then you can uninstall them under the Uninstall
|
||||||
|
tab after disabling them.
|
||||||
|
|
||||||
|
8. On the command line or in your FTP client, remove the file
|
||||||
|
|
||||||
|
sites/default/default.settings.php
|
||||||
|
|
||||||
|
9. Remove all old core files and directories, except for the 'sites' directory
|
||||||
|
and any custom files you added elsewhere.
|
||||||
|
|
||||||
|
If you made modifications to files like .htaccess or robots.txt, you will
|
||||||
|
need to re-apply them from your backup, after the new files are in place.
|
||||||
|
|
||||||
|
10. If you uninstalled any modules, remove them from the sites/all/modules and
|
||||||
|
other sites/*/modules directories. Leave other modules in place, even though
|
||||||
|
they are incompatible with Drupal 7.x.
|
||||||
|
|
||||||
|
11. Download the latest Drupal 7.x release from http://drupal.org to a
|
||||||
|
directory outside of your web root. Extract the archive and copy the files
|
||||||
|
into your Drupal directory.
|
||||||
|
|
||||||
|
On a typical Unix/Linux command line, use the following commands to download
|
||||||
|
and extract:
|
||||||
|
|
||||||
|
wget http://drupal.org/files/projects/drupal-x.y.tar.gz
|
||||||
|
tar -zxvf drupal-x.y.tar.gz
|
||||||
|
|
||||||
|
This creates a new directory drupal-x.y/ containing all Drupal files and
|
||||||
|
directories. Copy the files into your Drupal installation directory:
|
||||||
|
|
||||||
|
cp -R drupal-x.y/* drupal-x.y/.htaccess /path/to/your/installation
|
||||||
|
|
||||||
|
If you do not have command line access to your server, download the archive
|
||||||
|
from http://drupal.org using your web browser, extract it, and then use an
|
||||||
|
FTP client to upload the files to your web root.
|
||||||
|
|
||||||
|
12. Re-apply any modifications to files such as .htaccess or robots.txt.
|
||||||
|
|
||||||
|
13. Make your settings.php file writeable, so that the update process can
|
||||||
|
convert it to the format of Drupal 7.x. settings.php is usually located in
|
||||||
|
|
||||||
|
sites/default/settings.php
|
||||||
|
|
||||||
|
14. Run update.php by visiting http://www.example.com/update.php (replace
|
||||||
|
www.example.com with your domain name). This will update the core database
|
||||||
|
tables.
|
||||||
|
|
||||||
|
If you are unable to access update.php do the following:
|
||||||
|
|
||||||
|
- Open settings.php with a text editor.
|
||||||
|
|
||||||
|
- Find the line that says:
|
||||||
|
$update_free_access = FALSE;
|
||||||
|
|
||||||
|
- Change it into:
|
||||||
|
$update_free_access = TRUE;
|
||||||
|
|
||||||
|
- Once the upgrade is done, $update_free_access must be reverted to FALSE.
|
||||||
|
|
||||||
|
15. Backup your database after the core upgrade has run.
|
||||||
|
|
||||||
|
16. Replace and update your non-core modules and themes, following the
|
||||||
|
procedures at http://drupal.org/node/948216
|
||||||
|
|
||||||
|
17. Go to Administration > Reports > Status report. Verify that everything is
|
||||||
|
working as expected.
|
||||||
|
|
||||||
|
18. Ensure that $update_free_access is FALSE in settings.php.
|
||||||
|
|
||||||
|
19. Go to Administration > Configuration > Development > Maintenance mode.
|
||||||
|
Disable the "Put site into maintenance mode" checkbox and save the
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
To get started with Drupal 7 administration, visit
|
||||||
|
http://drupal.org/getting-started/7/admin
|
174
authorize.php
Normal file
174
authorize.php
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Administrative script for running authorized file operations.
|
||||||
|
*
|
||||||
|
* Using this script, the site owner (the user actually owning the files on the
|
||||||
|
* webserver) can authorize certain file-related operations to proceed with
|
||||||
|
* elevated privileges, for example to deploy and upgrade modules or themes.
|
||||||
|
* Users should not visit this page directly, but instead use an administrative
|
||||||
|
* user interface which knows how to redirect the user to this script as part of
|
||||||
|
* a multistep process. This script actually performs the selected operations
|
||||||
|
* without loading all of Drupal, to be able to more gracefully recover from
|
||||||
|
* errors. Access to the script is controlled by a global killswitch in
|
||||||
|
* settings.php ('allow_authorize_operations') and via the 'administer software
|
||||||
|
* updates' permission.
|
||||||
|
*
|
||||||
|
* There are helper functions for setting up an operation to run via this
|
||||||
|
* system in modules/system/system.module. For more information, see:
|
||||||
|
* @link authorize Authorized operation helper functions @endlink
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the root directory of the Drupal installation.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_ROOT', getcwd());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global flag to identify update.php and authorize.php runs.
|
||||||
|
*
|
||||||
|
* Identifies update.php and authorize.php runs, avoiding unwanted operations
|
||||||
|
* such as hook_init() and hook_exit() invokes, css/js preprocessing and
|
||||||
|
* translation, and solves some theming issues. The flag is checked in other
|
||||||
|
* places in Drupal code (not just authorize.php).
|
||||||
|
*/
|
||||||
|
define('MAINTENANCE_MODE', 'update');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a 403 access denied page for authorize.php.
|
||||||
|
*/
|
||||||
|
function authorize_access_denied_page() {
|
||||||
|
drupal_add_http_header('Status', '403 Forbidden');
|
||||||
|
watchdog('access denied', 'authorize.php', NULL, WATCHDOG_WARNING);
|
||||||
|
drupal_set_title('Access denied');
|
||||||
|
return t('You are not allowed to access this page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the current user is allowed to run authorize.php.
|
||||||
|
*
|
||||||
|
* The killswitch in settings.php overrides all else, otherwise, the user must
|
||||||
|
* have access to the 'administer software updates' permission.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the current user can run authorize.php, and FALSE if not.
|
||||||
|
*/
|
||||||
|
function authorize_access_allowed() {
|
||||||
|
return variable_get('allow_authorize_operations', TRUE) && user_access('administer software updates');
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** Real work of the script begins here. ***
|
||||||
|
|
||||||
|
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/common.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/file.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/module.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/ajax.inc';
|
||||||
|
|
||||||
|
// We prepare only a minimal bootstrap. This includes the database and
|
||||||
|
// variables, however, so we have access to the class autoloader registry.
|
||||||
|
drupal_bootstrap(DRUPAL_BOOTSTRAP_SESSION);
|
||||||
|
|
||||||
|
// This must go after drupal_bootstrap(), which unsets globals!
|
||||||
|
global $conf;
|
||||||
|
|
||||||
|
// We have to enable the user and system modules, even to check access and
|
||||||
|
// display errors via the maintenance theme.
|
||||||
|
$module_list['system']['filename'] = 'modules/system/system.module';
|
||||||
|
$module_list['user']['filename'] = 'modules/user/user.module';
|
||||||
|
module_list(TRUE, FALSE, FALSE, $module_list);
|
||||||
|
drupal_load('module', 'system');
|
||||||
|
drupal_load('module', 'user');
|
||||||
|
|
||||||
|
// We also want to have the language system available, but we do *NOT* want to
|
||||||
|
// actually call drupal_bootstrap(DRUPAL_BOOTSTRAP_LANGUAGE), since that would
|
||||||
|
// also force us through the DRUPAL_BOOTSTRAP_PAGE_HEADER phase, which loads
|
||||||
|
// all the modules, and that's exactly what we're trying to avoid.
|
||||||
|
drupal_language_initialize();
|
||||||
|
|
||||||
|
// Initialize the maintenance theme for this administrative script.
|
||||||
|
drupal_maintenance_theme();
|
||||||
|
|
||||||
|
$output = '';
|
||||||
|
$show_messages = TRUE;
|
||||||
|
|
||||||
|
if (authorize_access_allowed()) {
|
||||||
|
// Load both the Form API and Batch API.
|
||||||
|
require_once DRUPAL_ROOT . '/includes/form.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/batch.inc';
|
||||||
|
// Load the code that drives the authorize process.
|
||||||
|
require_once DRUPAL_ROOT . '/includes/authorize.inc';
|
||||||
|
|
||||||
|
// For the sake of Batch API and a few other low-level functions, we need to
|
||||||
|
// initialize the URL path into $_GET['q']. However, we do not want to raise
|
||||||
|
// our bootstrap level, nor do we want to call drupal_initialize_path(),
|
||||||
|
// since that is assuming that modules are loaded and invoking hooks.
|
||||||
|
// However, all we really care is if we're in the middle of a batch, in which
|
||||||
|
// case $_GET['q'] will already be set, we just initialize it to an empty
|
||||||
|
// string if it's not already defined.
|
||||||
|
if (!isset($_GET['q'])) {
|
||||||
|
$_GET['q'] = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_SESSION['authorize_operation']['page_title'])) {
|
||||||
|
drupal_set_title($_SESSION['authorize_operation']['page_title']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
drupal_set_title(t('Authorize file system changes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if we've run the operation and need to display a report.
|
||||||
|
if (isset($_SESSION['authorize_results']) && $results = $_SESSION['authorize_results']) {
|
||||||
|
|
||||||
|
// Clear the session out.
|
||||||
|
unset($_SESSION['authorize_results']);
|
||||||
|
unset($_SESSION['authorize_operation']);
|
||||||
|
unset($_SESSION['authorize_filetransfer_info']);
|
||||||
|
|
||||||
|
if (!empty($results['page_title'])) {
|
||||||
|
drupal_set_title($results['page_title']);
|
||||||
|
}
|
||||||
|
if (!empty($results['page_message'])) {
|
||||||
|
drupal_set_message($results['page_message']['message'], $results['page_message']['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$output = theme('authorize_report', array('messages' => $results['messages']));
|
||||||
|
|
||||||
|
$links = array();
|
||||||
|
if (is_array($results['tasks'])) {
|
||||||
|
$links += $results['tasks'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$links = array_merge($links, array(
|
||||||
|
l(t('Administration pages'), 'admin'),
|
||||||
|
l(t('Front page'), '<front>'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$output .= theme('item_list', array('items' => $links, 'title' => t('Next steps')));
|
||||||
|
}
|
||||||
|
// If a batch is running, let it run.
|
||||||
|
elseif (isset($_GET['batch'])) {
|
||||||
|
$output = _batch_page();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (empty($_SESSION['authorize_operation']) || empty($_SESSION['authorize_filetransfer_info'])) {
|
||||||
|
$output = t('It appears you have reached this page in error.');
|
||||||
|
}
|
||||||
|
elseif (!$batch = batch_get()) {
|
||||||
|
// We have a batch to process, show the filetransfer form.
|
||||||
|
$elements = drupal_get_form('authorize_filetransfer_form');
|
||||||
|
$output = drupal_render($elements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We defer the display of messages until all operations are done.
|
||||||
|
$show_messages = !(($batch = batch_get()) && isset($batch['running']));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$output = authorize_access_denied_page();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($output)) {
|
||||||
|
print theme('update_page', array('content' => $output, 'show_messages' => $show_messages));
|
||||||
|
}
|
5
composer.json
Normal file
5
composer.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"drush/drush": "^8.1"
|
||||||
|
}
|
||||||
|
}
|
1141
composer.lock
generated
Normal file
1141
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
26
cron.php
Normal file
26
cron.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Handles incoming requests to fire off regularly-scheduled tasks (cron jobs).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root directory of Drupal installation.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_ROOT', getcwd());
|
||||||
|
|
||||||
|
include_once DRUPAL_ROOT . '/includes/bootstrap.inc';
|
||||||
|
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
|
||||||
|
|
||||||
|
if (!isset($_GET['cron_key']) || variable_get('cron_key', 'drupal') != $_GET['cron_key']) {
|
||||||
|
watchdog('cron', 'Cron could not run because an invalid key was used.', array(), WATCHDOG_NOTICE);
|
||||||
|
drupal_access_denied();
|
||||||
|
}
|
||||||
|
elseif (variable_get('maintenance_mode', 0)) {
|
||||||
|
watchdog('cron', 'Cron could not run because the site is in maintenance mode.', array(), WATCHDOG_NOTICE);
|
||||||
|
drupal_access_denied();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
drupal_cron_run();
|
||||||
|
}
|
388
includes/actions.inc
Normal file
388
includes/actions.inc
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This is the actions engine for executing stored actions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup actions Actions
|
||||||
|
* @{
|
||||||
|
* Functions that perform an action on a certain system object.
|
||||||
|
*
|
||||||
|
* Action functions are declared by modules by implementing hook_action_info().
|
||||||
|
* Modules can cause action functions to run by calling actions_do(), and
|
||||||
|
* trigger.module provides a user interface that lets administrators define
|
||||||
|
* events that cause action functions to run.
|
||||||
|
*
|
||||||
|
* Each action function takes two to four arguments:
|
||||||
|
* - $entity: The object that the action acts on, such as a node, comment, or
|
||||||
|
* user.
|
||||||
|
* - $context: Array of additional information about what triggered the action.
|
||||||
|
* - $a1, $a2: Optional additional information, which can be passed into
|
||||||
|
* actions_do() and will be passed along to the action function.
|
||||||
|
*
|
||||||
|
* @}
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a given list of actions by executing their callback functions.
|
||||||
|
*
|
||||||
|
* Given the IDs of actions to perform, this function finds out what the
|
||||||
|
* callback functions for the actions are by querying the database. Then
|
||||||
|
* it calls each callback using the function call $function($object, $context,
|
||||||
|
* $a1, $a2), passing the input arguments of this function (see below) to the
|
||||||
|
* action function.
|
||||||
|
*
|
||||||
|
* @param $action_ids
|
||||||
|
* The IDs of the actions to perform. Can be a single action ID or an array
|
||||||
|
* of IDs. IDs of configurable actions must be given as numeric action IDs;
|
||||||
|
* IDs of non-configurable actions may be given as action function names.
|
||||||
|
* @param $object
|
||||||
|
* The object that the action will act on: a node, user, or comment object.
|
||||||
|
* @param $context
|
||||||
|
* Associative array containing extra information about what triggered
|
||||||
|
* the action call, with $context['hook'] giving the name of the hook
|
||||||
|
* that resulted in this call to actions_do().
|
||||||
|
* @param $a1
|
||||||
|
* Passed along to the callback.
|
||||||
|
* @param $a2
|
||||||
|
* Passed along to the callback.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array containing the results of the functions that
|
||||||
|
* perform the actions, keyed on action ID.
|
||||||
|
*
|
||||||
|
* @ingroup actions
|
||||||
|
*/
|
||||||
|
function actions_do($action_ids, $object = NULL, $context = NULL, $a1 = NULL, $a2 = NULL) {
|
||||||
|
// $stack tracks the number of recursive calls.
|
||||||
|
static $stack;
|
||||||
|
$stack++;
|
||||||
|
if ($stack > variable_get('actions_max_stack', 35)) {
|
||||||
|
watchdog('actions', 'Stack overflow: too many calls to actions_do(). Aborting to prevent infinite recursion.', array(), WATCHDOG_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$actions = array();
|
||||||
|
$available_actions = actions_list();
|
||||||
|
$actions_result = array();
|
||||||
|
if (is_array($action_ids)) {
|
||||||
|
$conditions = array();
|
||||||
|
foreach ($action_ids as $action_id) {
|
||||||
|
if (is_numeric($action_id)) {
|
||||||
|
$conditions[] = $action_id;
|
||||||
|
}
|
||||||
|
elseif (isset($available_actions[$action_id])) {
|
||||||
|
$actions[$action_id] = $available_actions[$action_id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we have action instances we must go to the database to retrieve
|
||||||
|
// instance data.
|
||||||
|
if (!empty($conditions)) {
|
||||||
|
$query = db_select('actions');
|
||||||
|
$query->addField('actions', 'aid');
|
||||||
|
$query->addField('actions', 'type');
|
||||||
|
$query->addField('actions', 'callback');
|
||||||
|
$query->addField('actions', 'parameters');
|
||||||
|
$query->condition('aid', $conditions, 'IN');
|
||||||
|
$result = $query->execute();
|
||||||
|
foreach ($result as $action) {
|
||||||
|
$actions[$action->aid] = $action->parameters ? unserialize($action->parameters) : array();
|
||||||
|
$actions[$action->aid]['callback'] = $action->callback;
|
||||||
|
$actions[$action->aid]['type'] = $action->type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire actions, in no particular order.
|
||||||
|
foreach ($actions as $action_id => $params) {
|
||||||
|
// Configurable actions need parameters.
|
||||||
|
if (is_numeric($action_id)) {
|
||||||
|
$function = $params['callback'];
|
||||||
|
if (function_exists($function)) {
|
||||||
|
$context = array_merge($context, $params);
|
||||||
|
$actions_result[$action_id] = $function($object, $context, $a1, $a2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$actions_result[$action_id] = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Singleton action; $action_id is the function name.
|
||||||
|
else {
|
||||||
|
$actions_result[$action_id] = $action_id($object, $context, $a1, $a2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Optimized execution of a single action.
|
||||||
|
else {
|
||||||
|
// If it's a configurable action, retrieve stored parameters.
|
||||||
|
if (is_numeric($action_ids)) {
|
||||||
|
$action = db_query("SELECT callback, parameters FROM {actions} WHERE aid = :aid", array(':aid' => $action_ids))->fetchObject();
|
||||||
|
$function = $action->callback;
|
||||||
|
if (function_exists($function)) {
|
||||||
|
$context = array_merge($context, unserialize($action->parameters));
|
||||||
|
$actions_result[$action_ids] = $function($object, $context, $a1, $a2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$actions_result[$action_ids] = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Singleton action; $action_ids is the function name.
|
||||||
|
else {
|
||||||
|
if (function_exists($action_ids)) {
|
||||||
|
$actions_result[$action_ids] = $action_ids($object, $context, $a1, $a2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set to avoid undefined index error messages later.
|
||||||
|
$actions_result[$action_ids] = FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$stack--;
|
||||||
|
return $actions_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discovers all available actions by invoking hook_action_info().
|
||||||
|
*
|
||||||
|
* This function contrasts with actions_get_all_actions(); see the
|
||||||
|
* documentation of actions_get_all_actions() for an explanation.
|
||||||
|
*
|
||||||
|
* @param $reset
|
||||||
|
* Reset the action info static cache.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array keyed on action function name, with the same format
|
||||||
|
* as the return value of hook_action_info(), containing all
|
||||||
|
* modules' hook_action_info() return values as modified by any
|
||||||
|
* hook_action_info_alter() implementations.
|
||||||
|
*
|
||||||
|
* @see hook_action_info()
|
||||||
|
*/
|
||||||
|
function actions_list($reset = FALSE) {
|
||||||
|
$actions = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($actions) || $reset) {
|
||||||
|
$actions = module_invoke_all('action_info');
|
||||||
|
drupal_alter('action_info', $actions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See module_implements() for an explanation of this cast.
|
||||||
|
return (array) $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all action instances from the database.
|
||||||
|
*
|
||||||
|
* This function differs from the actions_list() function, which gathers
|
||||||
|
* actions by invoking hook_action_info(). The actions returned by this
|
||||||
|
* function and the actions returned by actions_list() are partially
|
||||||
|
* synchronized. Non-configurable actions from hook_action_info()
|
||||||
|
* implementations are put into the database when actions_synchronize() is
|
||||||
|
* called, which happens when admin/config/system/actions is visited.
|
||||||
|
* Configurable actions are not added to the database until they are configured
|
||||||
|
* in the user interface, in which case a database row is created for each
|
||||||
|
* configuration of each action.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Associative array keyed by numeric action ID. Each value is an associative
|
||||||
|
* array with keys 'callback', 'label', 'type' and 'configurable'.
|
||||||
|
*/
|
||||||
|
function actions_get_all_actions() {
|
||||||
|
$actions = db_query("SELECT aid, type, callback, parameters, label FROM {actions}")->fetchAllAssoc('aid', PDO::FETCH_ASSOC);
|
||||||
|
foreach ($actions as &$action) {
|
||||||
|
$action['configurable'] = (bool) $action['parameters'];
|
||||||
|
unset($action['parameters']);
|
||||||
|
unset($action['aid']);
|
||||||
|
}
|
||||||
|
return $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an associative array keyed by hashes of function names or IDs.
|
||||||
|
*
|
||||||
|
* Hashes are used to prevent actual function names from going out into HTML
|
||||||
|
* forms and coming back.
|
||||||
|
*
|
||||||
|
* @param $actions
|
||||||
|
* An associative array with function names or action IDs as keys
|
||||||
|
* and associative arrays with keys 'label', 'type', etc. as values.
|
||||||
|
* This is usually the output of actions_list() or actions_get_all_actions().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array whose keys are hashes of the input array keys, and
|
||||||
|
* whose corresponding values are associative arrays with components
|
||||||
|
* 'callback', 'label', 'type', and 'configurable' from the input array.
|
||||||
|
*/
|
||||||
|
function actions_actions_map($actions) {
|
||||||
|
$actions_map = array();
|
||||||
|
foreach ($actions as $callback => $array) {
|
||||||
|
$key = drupal_hash_base64($callback);
|
||||||
|
$actions_map[$key]['callback'] = isset($array['callback']) ? $array['callback'] : $callback;
|
||||||
|
$actions_map[$key]['label'] = $array['label'];
|
||||||
|
$actions_map[$key]['type'] = $array['type'];
|
||||||
|
$actions_map[$key]['configurable'] = $array['configurable'];
|
||||||
|
}
|
||||||
|
return $actions_map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an action array key (function or ID), given its hash.
|
||||||
|
*
|
||||||
|
* Faster than actions_actions_map() when you only need the function name or ID.
|
||||||
|
*
|
||||||
|
* @param $hash
|
||||||
|
* Hash of a function name or action ID array key. The array key
|
||||||
|
* is a key into the return value of actions_list() (array key is the action
|
||||||
|
* function name) or actions_get_all_actions() (array key is the action ID).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The corresponding array key, or FALSE if no match is found.
|
||||||
|
*/
|
||||||
|
function actions_function_lookup($hash) {
|
||||||
|
// Check for a function name match.
|
||||||
|
$actions_list = actions_list();
|
||||||
|
foreach ($actions_list as $function => $array) {
|
||||||
|
if (drupal_hash_base64($function) == $hash) {
|
||||||
|
return $function;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$aid = FALSE;
|
||||||
|
// Must be a configurable action; check database.
|
||||||
|
$result = db_query("SELECT aid FROM {actions} WHERE parameters <> ''")->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($result as $row) {
|
||||||
|
if (drupal_hash_base64($row['aid']) == $hash) {
|
||||||
|
$aid = $row['aid'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $aid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes actions that are provided by modules in hook_action_info().
|
||||||
|
*
|
||||||
|
* Actions provided by modules in hook_action_info() implementations are
|
||||||
|
* synchronized with actions that are stored in the actions database table.
|
||||||
|
* This is necessary so that actions that do not require configuration can
|
||||||
|
* receive action IDs.
|
||||||
|
*
|
||||||
|
* @param $delete_orphans
|
||||||
|
* If TRUE, any actions that exist in the database but are no longer
|
||||||
|
* found in the code (for example, because the module that provides them has
|
||||||
|
* been disabled) will be deleted.
|
||||||
|
*/
|
||||||
|
function actions_synchronize($delete_orphans = FALSE) {
|
||||||
|
$actions_in_code = actions_list(TRUE);
|
||||||
|
$actions_in_db = db_query("SELECT aid, callback, label FROM {actions} WHERE parameters = ''")->fetchAllAssoc('callback', PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Go through all the actions provided by modules.
|
||||||
|
foreach ($actions_in_code as $callback => $array) {
|
||||||
|
// Ignore configurable actions since their instances get put in when the
|
||||||
|
// user adds the action.
|
||||||
|
if (!$array['configurable']) {
|
||||||
|
// If we already have an action ID for this action, no need to assign aid.
|
||||||
|
if (isset($actions_in_db[$callback])) {
|
||||||
|
unset($actions_in_db[$callback]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This is a new singleton that we don't have an aid for; assign one.
|
||||||
|
db_insert('actions')
|
||||||
|
->fields(array(
|
||||||
|
'aid' => $callback,
|
||||||
|
'type' => $array['type'],
|
||||||
|
'callback' => $callback,
|
||||||
|
'parameters' => '',
|
||||||
|
'label' => $array['label'],
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
watchdog('actions', "Action '%action' added.", array('%action' => $array['label']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any actions that we have left in $actions_in_db are orphaned.
|
||||||
|
if ($actions_in_db) {
|
||||||
|
$orphaned = array_keys($actions_in_db);
|
||||||
|
|
||||||
|
if ($delete_orphans) {
|
||||||
|
$actions = db_query('SELECT aid, label FROM {actions} WHERE callback IN (:orphaned)', array(':orphaned' => $orphaned))->fetchAll();
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
actions_delete($action->aid);
|
||||||
|
watchdog('actions', "Removed orphaned action '%action' from database.", array('%action' => $action->label));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$link = l(t('Remove orphaned actions'), 'admin/config/system/actions/orphan');
|
||||||
|
$count = count($actions_in_db);
|
||||||
|
$orphans = implode(', ', $orphaned);
|
||||||
|
watchdog('actions', '@count orphaned actions (%orphans) exist in the actions table. !link', array('@count' => $count, '%orphans' => $orphans, '!link' => $link), WATCHDOG_INFO);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an action and its user-supplied parameter values to the database.
|
||||||
|
*
|
||||||
|
* @param $function
|
||||||
|
* The name of the function to be called when this action is performed.
|
||||||
|
* @param $type
|
||||||
|
* The type of action, to describe grouping and/or context, e.g., 'node',
|
||||||
|
* 'user', 'comment', or 'system'.
|
||||||
|
* @param $params
|
||||||
|
* An associative array with parameter names as keys and parameter values as
|
||||||
|
* values.
|
||||||
|
* @param $label
|
||||||
|
* A user-supplied label of this particular action, e.g., 'Send e-mail
|
||||||
|
* to Jim'.
|
||||||
|
* @param $aid
|
||||||
|
* The ID of this action. If omitted, a new action is created.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The ID of the action.
|
||||||
|
*/
|
||||||
|
function actions_save($function, $type, $params, $label, $aid = NULL) {
|
||||||
|
// aid is the callback for singleton actions so we need to keep a separate
|
||||||
|
// table for numeric aids.
|
||||||
|
if (!$aid) {
|
||||||
|
$aid = db_next_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
db_merge('actions')
|
||||||
|
->key(array('aid' => $aid))
|
||||||
|
->fields(array(
|
||||||
|
'callback' => $function,
|
||||||
|
'type' => $type,
|
||||||
|
'parameters' => serialize($params),
|
||||||
|
'label' => $label,
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
watchdog('actions', 'Action %action saved.', array('%action' => $label));
|
||||||
|
return $aid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a single action from the database.
|
||||||
|
*
|
||||||
|
* @param $aid
|
||||||
|
* The ID of the action to retrieve.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The appropriate action row from the database as an object.
|
||||||
|
*/
|
||||||
|
function actions_load($aid) {
|
||||||
|
return db_query("SELECT aid, type, callback, parameters, label FROM {actions} WHERE aid = :aid", array(':aid' => $aid))->fetchObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a single action from the database.
|
||||||
|
*
|
||||||
|
* @param $aid
|
||||||
|
* The ID of the action to delete.
|
||||||
|
*/
|
||||||
|
function actions_delete($aid) {
|
||||||
|
db_delete('actions')
|
||||||
|
->condition('aid', $aid)
|
||||||
|
->execute();
|
||||||
|
module_invoke_all('actions_delete', $aid);
|
||||||
|
}
|
1317
includes/ajax.inc
Normal file
1317
includes/ajax.inc
Normal file
File diff suppressed because it is too large
Load diff
68
includes/archiver.inc
Normal file
68
includes/archiver.inc
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Shared classes and interfaces for the archiver system.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the common interface for all Archiver classes.
|
||||||
|
*/
|
||||||
|
interface ArchiverInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new archiver instance.
|
||||||
|
*
|
||||||
|
* @param $file_path
|
||||||
|
* The full system path of the archive to manipulate. Only local files
|
||||||
|
* are supported. If the file does not yet exist, it will be created if
|
||||||
|
* appropriate.
|
||||||
|
*/
|
||||||
|
public function __construct($file_path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the specified file or directory to the archive.
|
||||||
|
*
|
||||||
|
* @param $file_path
|
||||||
|
* The full system path of the file or directory to add. Only local files
|
||||||
|
* and directories are supported.
|
||||||
|
*
|
||||||
|
* @return ArchiverInterface
|
||||||
|
* The called object.
|
||||||
|
*/
|
||||||
|
public function add($file_path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the specified file from the archive.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The file name relative to the root of the archive to remove.
|
||||||
|
*
|
||||||
|
* @return ArchiverInterface
|
||||||
|
* The called object.
|
||||||
|
*/
|
||||||
|
public function remove($path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts multiple files in the archive to the specified path.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* A full system path of the directory to which to extract files.
|
||||||
|
* @param $files
|
||||||
|
* Optionally specify a list of files to be extracted. Files are
|
||||||
|
* relative to the root of the archive. If not specified, all files
|
||||||
|
* in the archive will be extracted.
|
||||||
|
*
|
||||||
|
* @return ArchiverInterface
|
||||||
|
* The called object.
|
||||||
|
*/
|
||||||
|
public function extract($path, array $files = array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all files in the archive.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of file names relative to the root of the archive.
|
||||||
|
*/
|
||||||
|
public function listContents();
|
||||||
|
}
|
334
includes/authorize.inc
Normal file
334
includes/authorize.inc
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Helper functions and form handlers used for the authorize.php script.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form constructor for the file transfer authorization form.
|
||||||
|
*
|
||||||
|
* Allows the user to choose a FileTransfer type and supply credentials.
|
||||||
|
*
|
||||||
|
* @see authorize_filetransfer_form_validate()
|
||||||
|
* @see authorize_filetransfer_form_submit()
|
||||||
|
* @ingroup forms
|
||||||
|
*/
|
||||||
|
function authorize_filetransfer_form($form, &$form_state) {
|
||||||
|
global $base_url, $is_https;
|
||||||
|
$form = array();
|
||||||
|
|
||||||
|
// If possible, we want to post this form securely via HTTPS.
|
||||||
|
$form['#https'] = TRUE;
|
||||||
|
|
||||||
|
// CSS we depend on lives in modules/system/maintenance.css, which is loaded
|
||||||
|
// via the default maintenance theme.
|
||||||
|
$form['#attached']['js'][] = $base_url . '/misc/authorize.js';
|
||||||
|
|
||||||
|
// Get all the available ways to transfer files.
|
||||||
|
if (empty($_SESSION['authorize_filetransfer_info'])) {
|
||||||
|
drupal_set_message(t('Unable to continue, no available methods of file transfer'), 'error');
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
$available_backends = $_SESSION['authorize_filetransfer_info'];
|
||||||
|
|
||||||
|
if (!$is_https) {
|
||||||
|
$form['information']['https_warning'] = array(
|
||||||
|
'#prefix' => '<div class="messages error">',
|
||||||
|
'#markup' => t('WARNING: You are not using an encrypted connection, so your password will be sent in plain text. <a href="@https-link">Learn more</a>.', array('@https-link' => 'http://drupal.org/https-information')),
|
||||||
|
'#suffix' => '</div>',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decide on a default backend.
|
||||||
|
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default'])) {
|
||||||
|
$authorize_filetransfer_default = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
|
||||||
|
}
|
||||||
|
elseif ($authorize_filetransfer_default = variable_get('authorize_filetransfer_default', NULL));
|
||||||
|
else {
|
||||||
|
$authorize_filetransfer_default = key($available_backends);
|
||||||
|
}
|
||||||
|
|
||||||
|
$form['information']['main_header'] = array(
|
||||||
|
'#prefix' => '<h3>',
|
||||||
|
'#markup' => t('To continue, provide your server connection details'),
|
||||||
|
'#suffix' => '</h3>',
|
||||||
|
);
|
||||||
|
|
||||||
|
$form['connection_settings']['#tree'] = TRUE;
|
||||||
|
$form['connection_settings']['authorize_filetransfer_default'] = array(
|
||||||
|
'#type' => 'select',
|
||||||
|
'#title' => t('Connection method'),
|
||||||
|
'#default_value' => $authorize_filetransfer_default,
|
||||||
|
'#weight' => -10,
|
||||||
|
);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Here we create two submit buttons. For a JS enabled client, they will
|
||||||
|
* only ever see submit_process. However, if a client doesn't have JS
|
||||||
|
* enabled, they will see submit_connection on the first form (when picking
|
||||||
|
* what filetransfer type to use, and submit_process on the second one (which
|
||||||
|
* leads to the actual operation).
|
||||||
|
*/
|
||||||
|
$form['submit_connection'] = array(
|
||||||
|
'#prefix' => "<br style='clear:both'/>",
|
||||||
|
'#name' => 'enter_connection_settings',
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#value' => t('Enter connection settings'),
|
||||||
|
'#weight' => 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
$form['submit_process'] = array(
|
||||||
|
'#name' => 'process_updates',
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#value' => t('Continue'),
|
||||||
|
'#weight' => 100,
|
||||||
|
'#attributes' => array('style' => 'display:none'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build a container for each connection type.
|
||||||
|
foreach ($available_backends as $name => $backend) {
|
||||||
|
$form['connection_settings']['authorize_filetransfer_default']['#options'][$name] = $backend['title'];
|
||||||
|
$form['connection_settings'][$name] = array(
|
||||||
|
'#type' => 'container',
|
||||||
|
'#attributes' => array('class' => array("filetransfer-$name", 'filetransfer')),
|
||||||
|
);
|
||||||
|
// We can't use #prefix on the container itself since then the header won't
|
||||||
|
// be hidden and shown when the containers are being manipulated via JS.
|
||||||
|
$form['connection_settings'][$name]['header'] = array(
|
||||||
|
'#markup' => '<h4>' . t('@backend connection settings', array('@backend' => $backend['title'])) . '</h4>',
|
||||||
|
);
|
||||||
|
|
||||||
|
$form['connection_settings'][$name] += _authorize_filetransfer_connection_settings($name);
|
||||||
|
|
||||||
|
// Start non-JS code.
|
||||||
|
if (isset($form_state['values']['connection_settings']['authorize_filetransfer_default']) && $form_state['values']['connection_settings']['authorize_filetransfer_default'] == $name) {
|
||||||
|
|
||||||
|
// If the user switches from JS to non-JS, Drupal (and Batch API) will
|
||||||
|
// barf. This is a known bug: http://drupal.org/node/229825.
|
||||||
|
setcookie('has_js', '', time() - 3600, '/');
|
||||||
|
unset($_COOKIE['has_js']);
|
||||||
|
|
||||||
|
// Change the submit button to the submit_process one.
|
||||||
|
$form['submit_process']['#attributes'] = array();
|
||||||
|
unset($form['submit_connection']);
|
||||||
|
|
||||||
|
// Activate the proper filetransfer settings form.
|
||||||
|
$form['connection_settings'][$name]['#attributes']['style'] = 'display:block';
|
||||||
|
// Disable the select box.
|
||||||
|
$form['connection_settings']['authorize_filetransfer_default']['#disabled'] = TRUE;
|
||||||
|
|
||||||
|
// Create a button for changing the type of connection.
|
||||||
|
$form['connection_settings']['change_connection_type'] = array(
|
||||||
|
'#name' => 'change_connection_type',
|
||||||
|
'#type' => 'submit',
|
||||||
|
'#value' => t('Change connection type'),
|
||||||
|
'#weight' => -5,
|
||||||
|
'#attributes' => array('class' => array('filetransfer-change-connection-type')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// End non-JS code.
|
||||||
|
}
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the Form API array for a given connection backend's settings.
|
||||||
|
*
|
||||||
|
* @param $backend
|
||||||
|
* The name of the backend (e.g. 'ftp', 'ssh', etc).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Form API array of connection settings for the given backend.
|
||||||
|
*
|
||||||
|
* @see hook_filetransfer_backends()
|
||||||
|
*/
|
||||||
|
function _authorize_filetransfer_connection_settings($backend) {
|
||||||
|
$defaults = variable_get('authorize_filetransfer_connection_settings_' . $backend, array());
|
||||||
|
$form = array();
|
||||||
|
|
||||||
|
// Create an instance of the file transfer class to get its settings form.
|
||||||
|
$filetransfer = authorize_get_filetransfer($backend);
|
||||||
|
if ($filetransfer) {
|
||||||
|
$form = $filetransfer->getSettingsForm();
|
||||||
|
}
|
||||||
|
// Fill in the defaults based on the saved settings, if any.
|
||||||
|
_authorize_filetransfer_connection_settings_set_defaults($form, NULL, $defaults);
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the default settings on a file transfer connection form recursively.
|
||||||
|
*
|
||||||
|
* The default settings for the file transfer connection forms are saved in
|
||||||
|
* the database. The settings are stored as a nested array in the case of a
|
||||||
|
* settings form that has fieldsets or otherwise uses a nested structure.
|
||||||
|
* Therefore, to properly add defaults, we need to walk through all the
|
||||||
|
* children form elements and process those defaults recursively.
|
||||||
|
*
|
||||||
|
* @param $element
|
||||||
|
* Reference to the Form API form element we're operating on.
|
||||||
|
* @param $key
|
||||||
|
* The key for our current form element, if any.
|
||||||
|
* @param array $defaults
|
||||||
|
* The default settings for the file transfer backend we're operating on.
|
||||||
|
*/
|
||||||
|
function _authorize_filetransfer_connection_settings_set_defaults(&$element, $key, array $defaults) {
|
||||||
|
// If we're operating on a form element which isn't a fieldset, and we have
|
||||||
|
// a default setting saved, stash it in #default_value.
|
||||||
|
if (!empty($key) && isset($defaults[$key]) && isset($element['#type']) && $element['#type'] != 'fieldset') {
|
||||||
|
$element['#default_value'] = $defaults[$key];
|
||||||
|
}
|
||||||
|
// Now, we walk through all the child elements, and recursively invoke
|
||||||
|
// ourself on each one. Since the $defaults settings array can be nested
|
||||||
|
// (because of #tree, any values inside fieldsets will be nested), if
|
||||||
|
// there's a subarray of settings for the form key we're currently
|
||||||
|
// processing, pass in that subarray to the recursive call. Otherwise, just
|
||||||
|
// pass on the whole $defaults array.
|
||||||
|
foreach (element_children($element) as $child_key) {
|
||||||
|
_authorize_filetransfer_connection_settings_set_defaults($element[$child_key], $child_key, ((isset($defaults[$key]) && is_array($defaults[$key])) ? $defaults[$key] : $defaults));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form validation handler for authorize_filetransfer_form().
|
||||||
|
*
|
||||||
|
* @see authorize_filetransfer_form()
|
||||||
|
* @see authorize_filetransfer_submit()
|
||||||
|
*/
|
||||||
|
function authorize_filetransfer_form_validate($form, &$form_state) {
|
||||||
|
// Only validate the form if we have collected all of the user input and are
|
||||||
|
// ready to proceed with updating or installing.
|
||||||
|
if ($form_state['triggering_element']['#name'] != 'process_updates') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($form_state['values']['connection_settings'])) {
|
||||||
|
$backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
|
||||||
|
$filetransfer = authorize_get_filetransfer($backend, $form_state['values']['connection_settings'][$backend]);
|
||||||
|
try {
|
||||||
|
if (!$filetransfer) {
|
||||||
|
throw new Exception(t('Error, this type of connection protocol (%backend) does not exist.', array('%backend' => $backend)));
|
||||||
|
}
|
||||||
|
$filetransfer->connect();
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// The format of this error message is similar to that used on the
|
||||||
|
// database connection form in the installer.
|
||||||
|
form_set_error('connection_settings', t('Failed to connect to the server. The server reports the following message: !message For more help installing or updating code on your server, see the <a href="@handbook_url">handbook</a>.', array(
|
||||||
|
'!message' => '<p class="error">' . $e->getMessage() . '</p>',
|
||||||
|
'@handbook_url' => 'http://drupal.org/documentation/install/modules-themes',
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Form submission handler for authorize_filetransfer_form().
|
||||||
|
*
|
||||||
|
* @see authorize_filetransfer_form()
|
||||||
|
* @see authorize_filetransfer_validate()
|
||||||
|
*/
|
||||||
|
function authorize_filetransfer_form_submit($form, &$form_state) {
|
||||||
|
global $base_url;
|
||||||
|
switch ($form_state['triggering_element']['#name']) {
|
||||||
|
case 'process_updates':
|
||||||
|
|
||||||
|
// Save the connection settings to the DB.
|
||||||
|
$filetransfer_backend = $form_state['values']['connection_settings']['authorize_filetransfer_default'];
|
||||||
|
|
||||||
|
// If the database is available then try to save our settings. We have
|
||||||
|
// to make sure it is available since this code could potentially (will
|
||||||
|
// likely) be called during the installation process, before the
|
||||||
|
// database is set up.
|
||||||
|
try {
|
||||||
|
$connection_settings = array();
|
||||||
|
foreach ($form_state['values']['connection_settings'][$filetransfer_backend] as $key => $value) {
|
||||||
|
// We do *not* want to store passwords in the database, unless the
|
||||||
|
// backend explicitly says so via the magic #filetransfer_save form
|
||||||
|
// property. Otherwise, we store everything that's not explicitly
|
||||||
|
// marked with #filetransfer_save set to FALSE.
|
||||||
|
if (!isset($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save'])) {
|
||||||
|
if ($form['connection_settings'][$filetransfer_backend][$key]['#type'] != 'password') {
|
||||||
|
$connection_settings[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The attribute is defined, so only save if set to TRUE.
|
||||||
|
elseif ($form['connection_settings'][$filetransfer_backend][$key]['#filetransfer_save']) {
|
||||||
|
$connection_settings[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set this one as the default authorize method.
|
||||||
|
variable_set('authorize_filetransfer_default', $filetransfer_backend);
|
||||||
|
// Save the connection settings minus the password.
|
||||||
|
variable_set('authorize_filetransfer_connection_settings_' . $filetransfer_backend, $connection_settings);
|
||||||
|
|
||||||
|
$filetransfer = authorize_get_filetransfer($filetransfer_backend, $form_state['values']['connection_settings'][$filetransfer_backend]);
|
||||||
|
|
||||||
|
// Now run the operation.
|
||||||
|
authorize_run_operation($filetransfer);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// If there is no database available, we don't care and just skip
|
||||||
|
// this part entirely.
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'enter_connection_settings':
|
||||||
|
$form_state['rebuild'] = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'change_connection_type':
|
||||||
|
$form_state['rebuild'] = TRUE;
|
||||||
|
unset($form_state['values']['connection_settings']['authorize_filetransfer_default']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the operation specified in $_SESSION['authorize_operation'].
|
||||||
|
*
|
||||||
|
* @param $filetransfer
|
||||||
|
* The FileTransfer object to use for running the operation.
|
||||||
|
*/
|
||||||
|
function authorize_run_operation($filetransfer) {
|
||||||
|
$operation = $_SESSION['authorize_operation'];
|
||||||
|
unset($_SESSION['authorize_operation']);
|
||||||
|
|
||||||
|
if (!empty($operation['page_title'])) {
|
||||||
|
drupal_set_title($operation['page_title']);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DRUPAL_ROOT . '/' . $operation['file'];
|
||||||
|
call_user_func_array($operation['callback'], array_merge(array($filetransfer), $operation['arguments']));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a FileTransfer class for a specific transfer method and settings.
|
||||||
|
*
|
||||||
|
* @param $backend
|
||||||
|
* The FileTransfer backend to get the class for.
|
||||||
|
* @param $settings
|
||||||
|
* Array of settings for the FileTransfer.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An instantiated FileTransfer object for the requested method and settings,
|
||||||
|
* or FALSE if there was an error finding or instantiating it.
|
||||||
|
*/
|
||||||
|
function authorize_get_filetransfer($backend, $settings = array()) {
|
||||||
|
$filetransfer = FALSE;
|
||||||
|
if (!empty($_SESSION['authorize_filetransfer_info'][$backend])) {
|
||||||
|
$backend_info = $_SESSION['authorize_filetransfer_info'][$backend];
|
||||||
|
if (!empty($backend_info['file'])) {
|
||||||
|
$file = $backend_info['file path'] . '/' . $backend_info['file'];
|
||||||
|
require_once $file;
|
||||||
|
}
|
||||||
|
if (class_exists($backend_info['class'])) {
|
||||||
|
// PHP 5.2 doesn't support $class::factory() syntax, so we have to
|
||||||
|
// use call_user_func_array() until we can require PHP 5.3.
|
||||||
|
$filetransfer = call_user_func_array(array($backend_info['class'], 'factory'), array(DRUPAL_ROOT, $settings));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $filetransfer;
|
||||||
|
}
|
539
includes/batch.inc
Normal file
539
includes/batch.inc
Normal file
|
@ -0,0 +1,539 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Batch processing API for processes to run in multiple HTTP requests.
|
||||||
|
*
|
||||||
|
* Note that batches are usually invoked by form submissions, which is
|
||||||
|
* why the core interaction functions of the batch processing API live in
|
||||||
|
* form.inc.
|
||||||
|
*
|
||||||
|
* @see form.inc
|
||||||
|
* @see batch_set()
|
||||||
|
* @see batch_process()
|
||||||
|
* @see batch_get()
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a batch from the database.
|
||||||
|
*
|
||||||
|
* @param $id
|
||||||
|
* The ID of the batch to load. When a progressive batch is being processed,
|
||||||
|
* the relevant ID is found in $_REQUEST['id'].
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array representing the batch, or FALSE if no batch was found.
|
||||||
|
*/
|
||||||
|
function batch_load($id) {
|
||||||
|
$batch = db_query("SELECT batch FROM {batch} WHERE bid = :bid AND token = :token", array(
|
||||||
|
':bid' => $id,
|
||||||
|
':token' => drupal_get_token($id),
|
||||||
|
))->fetchField();
|
||||||
|
if ($batch) {
|
||||||
|
return unserialize($batch);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the batch processing page based on the current state of the batch.
|
||||||
|
*
|
||||||
|
* @see _batch_shutdown()
|
||||||
|
*/
|
||||||
|
function _batch_page() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
|
||||||
|
if (!isset($_REQUEST['id'])) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the current state of the batch.
|
||||||
|
if (!$batch) {
|
||||||
|
$batch = batch_load($_REQUEST['id']);
|
||||||
|
if (!$batch) {
|
||||||
|
drupal_set_message(t('No active batch.'), 'error');
|
||||||
|
drupal_goto();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register database update for the end of processing.
|
||||||
|
drupal_register_shutdown_function('_batch_shutdown');
|
||||||
|
|
||||||
|
// Add batch-specific CSS.
|
||||||
|
foreach ($batch['sets'] as $batch_set) {
|
||||||
|
if (isset($batch_set['css'])) {
|
||||||
|
foreach ($batch_set['css'] as $css) {
|
||||||
|
drupal_add_css($css);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$op = isset($_REQUEST['op']) ? $_REQUEST['op'] : '';
|
||||||
|
$output = NULL;
|
||||||
|
switch ($op) {
|
||||||
|
case 'start':
|
||||||
|
$output = _batch_start();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'do':
|
||||||
|
// JavaScript-based progress page callback.
|
||||||
|
_batch_do();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'do_nojs':
|
||||||
|
// Non-JavaScript-based progress page.
|
||||||
|
$output = _batch_progress_page_nojs();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'finished':
|
||||||
|
$output = _batch_finished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the batch processing.
|
||||||
|
*
|
||||||
|
* JavaScript-enabled clients are identified by the 'has_js' cookie set in
|
||||||
|
* drupal.js. If no JavaScript-enabled page has been visited during the current
|
||||||
|
* user's browser session, the non-JavaScript version is returned.
|
||||||
|
*/
|
||||||
|
function _batch_start() {
|
||||||
|
if (isset($_COOKIE['has_js']) && $_COOKIE['has_js']) {
|
||||||
|
return _batch_progress_page_js();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return _batch_progress_page_nojs();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs a batch processing page with JavaScript support.
|
||||||
|
*
|
||||||
|
* This initializes the batch and error messages. Note that in JavaScript-based
|
||||||
|
* processing, the batch processing page is displayed only once and updated via
|
||||||
|
* AHAH requests, so only the first batch set gets to define the page title.
|
||||||
|
* Titles specified by subsequent batch sets are not displayed.
|
||||||
|
*
|
||||||
|
* @see batch_set()
|
||||||
|
* @see _batch_do()
|
||||||
|
*/
|
||||||
|
function _batch_progress_page_js() {
|
||||||
|
$batch = batch_get();
|
||||||
|
|
||||||
|
$current_set = _batch_current_set();
|
||||||
|
drupal_set_title($current_set['title'], PASS_THROUGH);
|
||||||
|
|
||||||
|
// Merge required query parameters for batch processing into those provided by
|
||||||
|
// batch_set() or hook_batch_alter().
|
||||||
|
$batch['url_options']['query']['id'] = $batch['id'];
|
||||||
|
|
||||||
|
$js_setting = array(
|
||||||
|
'batch' => array(
|
||||||
|
'errorMessage' => $current_set['error_message'] . '<br />' . $batch['error_message'],
|
||||||
|
'initMessage' => $current_set['init_message'],
|
||||||
|
'uri' => url($batch['url'], $batch['url_options']),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
drupal_add_js($js_setting, 'setting');
|
||||||
|
drupal_add_library('system', 'drupal.batch');
|
||||||
|
|
||||||
|
return '<div id="progress"></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does one execution pass with JavaScript and returns progress to the browser.
|
||||||
|
*
|
||||||
|
* @see _batch_progress_page_js()
|
||||||
|
* @see _batch_process()
|
||||||
|
*/
|
||||||
|
function _batch_do() {
|
||||||
|
// HTTP POST required.
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||||
|
drupal_set_message(t('HTTP POST is required.'), 'error');
|
||||||
|
drupal_set_title(t('Error'));
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform actual processing.
|
||||||
|
list($percentage, $message) = _batch_process();
|
||||||
|
|
||||||
|
drupal_json_output(array('status' => TRUE, 'percentage' => $percentage, 'message' => $message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs a batch processing page without JavaScript support.
|
||||||
|
*
|
||||||
|
* @see _batch_process()
|
||||||
|
*/
|
||||||
|
function _batch_progress_page_nojs() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
|
||||||
|
$current_set = _batch_current_set();
|
||||||
|
drupal_set_title($current_set['title'], PASS_THROUGH);
|
||||||
|
|
||||||
|
$new_op = 'do_nojs';
|
||||||
|
|
||||||
|
if (!isset($batch['running'])) {
|
||||||
|
// This is the first page so we return some output immediately.
|
||||||
|
$percentage = 0;
|
||||||
|
$message = $current_set['init_message'];
|
||||||
|
$batch['running'] = TRUE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// This is one of the later requests; do some processing first.
|
||||||
|
|
||||||
|
// Error handling: if PHP dies due to a fatal error (e.g. a nonexistent
|
||||||
|
// function), it will output whatever is in the output buffer, followed by
|
||||||
|
// the error message.
|
||||||
|
ob_start();
|
||||||
|
$fallback = $current_set['error_message'] . '<br />' . $batch['error_message'];
|
||||||
|
$fallback = theme('maintenance_page', array('content' => $fallback, 'show_messages' => FALSE));
|
||||||
|
|
||||||
|
// We strip the end of the page using a marker in the template, so any
|
||||||
|
// additional HTML output by PHP shows up inside the page rather than below
|
||||||
|
// it. While this causes invalid HTML, the same would be true if we didn't,
|
||||||
|
// as content is not allowed to appear after </html> anyway.
|
||||||
|
list($fallback) = explode('<!--partial-->', $fallback);
|
||||||
|
print $fallback;
|
||||||
|
|
||||||
|
// Perform actual processing.
|
||||||
|
list($percentage, $message) = _batch_process($batch);
|
||||||
|
if ($percentage == 100) {
|
||||||
|
$new_op = 'finished';
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP did not die; remove the fallback output.
|
||||||
|
ob_end_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge required query parameters for batch processing into those provided by
|
||||||
|
// batch_set() or hook_batch_alter().
|
||||||
|
$batch['url_options']['query']['id'] = $batch['id'];
|
||||||
|
$batch['url_options']['query']['op'] = $new_op;
|
||||||
|
|
||||||
|
$url = url($batch['url'], $batch['url_options']);
|
||||||
|
$element = array(
|
||||||
|
'#tag' => 'meta',
|
||||||
|
'#attributes' => array(
|
||||||
|
'http-equiv' => 'Refresh',
|
||||||
|
'content' => '0; URL=' . $url,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
drupal_add_html_head($element, 'batch_progress_meta_refresh');
|
||||||
|
|
||||||
|
return theme('progress_bar', array('percent' => $percentage, 'message' => $message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processes sets in a batch.
|
||||||
|
*
|
||||||
|
* If the batch was marked for progressive execution (default), this executes as
|
||||||
|
* many operations in batch sets until an execution time of 1 second has been
|
||||||
|
* exceeded. It will continue with the next operation of the same batch set in
|
||||||
|
* the next request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array containing a completion value (in percent) and a status message.
|
||||||
|
*/
|
||||||
|
function _batch_process() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
$current_set = &_batch_current_set();
|
||||||
|
// Indicate that this batch set needs to be initialized.
|
||||||
|
$set_changed = TRUE;
|
||||||
|
|
||||||
|
// If this batch was marked for progressive execution (e.g. forms submitted by
|
||||||
|
// drupal_form_submit()), initialize a timer to determine whether we need to
|
||||||
|
// proceed with the same batch phase when a processing time of 1 second has
|
||||||
|
// been exceeded.
|
||||||
|
if ($batch['progressive']) {
|
||||||
|
timer_start('batch_processing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($current_set['start'])) {
|
||||||
|
$current_set['start'] = microtime(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue = _batch_queue($current_set);
|
||||||
|
|
||||||
|
while (!$current_set['success']) {
|
||||||
|
// If this is the first time we iterate this batch set in the current
|
||||||
|
// request, we check if it requires an additional file for functions
|
||||||
|
// definitions.
|
||||||
|
if ($set_changed && isset($current_set['file']) && is_file($current_set['file'])) {
|
||||||
|
include_once DRUPAL_ROOT . '/' . $current_set['file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$task_message = '';
|
||||||
|
// Assume a single pass operation and set the completion level to 1 by
|
||||||
|
// default.
|
||||||
|
$finished = 1;
|
||||||
|
|
||||||
|
if ($item = $queue->claimItem()) {
|
||||||
|
list($function, $args) = $item->data;
|
||||||
|
|
||||||
|
// Build the 'context' array and execute the function call.
|
||||||
|
$batch_context = array(
|
||||||
|
'sandbox' => &$current_set['sandbox'],
|
||||||
|
'results' => &$current_set['results'],
|
||||||
|
'finished' => &$finished,
|
||||||
|
'message' => &$task_message,
|
||||||
|
);
|
||||||
|
call_user_func_array($function, array_merge($args, array(&$batch_context)));
|
||||||
|
|
||||||
|
if ($finished >= 1) {
|
||||||
|
// Make sure this step is not counted twice when computing $current.
|
||||||
|
$finished = 0;
|
||||||
|
// Remove the processed operation and clear the sandbox.
|
||||||
|
$queue->deleteItem($item);
|
||||||
|
$current_set['count']--;
|
||||||
|
$current_set['sandbox'] = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When all operations in the current batch set are completed, browse
|
||||||
|
// through the remaining sets, marking them 'successfully processed'
|
||||||
|
// along the way, until we find a set that contains operations.
|
||||||
|
// _batch_next_set() executes form submit handlers stored in 'control'
|
||||||
|
// sets (see form_execute_handlers()), which can in turn add new sets to
|
||||||
|
// the batch.
|
||||||
|
$set_changed = FALSE;
|
||||||
|
$old_set = $current_set;
|
||||||
|
while (empty($current_set['count']) && ($current_set['success'] = TRUE) && _batch_next_set()) {
|
||||||
|
$current_set = &_batch_current_set();
|
||||||
|
$current_set['start'] = microtime(TRUE);
|
||||||
|
$set_changed = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, either $current_set contains operations that need to be
|
||||||
|
// processed or all sets have been completed.
|
||||||
|
$queue = _batch_queue($current_set);
|
||||||
|
|
||||||
|
// If we are in progressive mode, break processing after 1 second.
|
||||||
|
if ($batch['progressive'] && timer_read('batch_processing') > 1000) {
|
||||||
|
// Record elapsed wall clock time.
|
||||||
|
$current_set['elapsed'] = round((microtime(TRUE) - $current_set['start']) * 1000, 2);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($batch['progressive']) {
|
||||||
|
// Gather progress information.
|
||||||
|
|
||||||
|
// Reporting 100% progress will cause the whole batch to be considered
|
||||||
|
// processed. If processing was paused right after moving to a new set,
|
||||||
|
// we have to use the info from the new (unprocessed) set.
|
||||||
|
if ($set_changed && isset($current_set['queue'])) {
|
||||||
|
// Processing will continue with a fresh batch set.
|
||||||
|
$remaining = $current_set['count'];
|
||||||
|
$total = $current_set['total'];
|
||||||
|
$progress_message = $current_set['init_message'];
|
||||||
|
$task_message = '';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Processing will continue with the current batch set.
|
||||||
|
$remaining = $old_set['count'];
|
||||||
|
$total = $old_set['total'];
|
||||||
|
$progress_message = $old_set['progress_message'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total progress is the number of operations that have fully run plus the
|
||||||
|
// completion level of the current operation.
|
||||||
|
$current = $total - $remaining + $finished;
|
||||||
|
$percentage = _batch_api_percentage($total, $current);
|
||||||
|
$elapsed = isset($current_set['elapsed']) ? $current_set['elapsed'] : 0;
|
||||||
|
$values = array(
|
||||||
|
'@remaining' => $remaining,
|
||||||
|
'@total' => $total,
|
||||||
|
'@current' => floor($current),
|
||||||
|
'@percentage' => $percentage,
|
||||||
|
'@elapsed' => format_interval($elapsed / 1000),
|
||||||
|
// If possible, estimate remaining processing time.
|
||||||
|
'@estimate' => ($current > 0) ? format_interval(($elapsed * ($total - $current) / $current) / 1000) : '-',
|
||||||
|
);
|
||||||
|
$message = strtr($progress_message, $values);
|
||||||
|
if (!empty($message)) {
|
||||||
|
$message .= '<br />';
|
||||||
|
}
|
||||||
|
if (!empty($task_message)) {
|
||||||
|
$message .= $task_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($percentage, $message);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If we are not in progressive mode, the entire batch has been processed.
|
||||||
|
return _batch_finished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the percent completion for a batch set.
|
||||||
|
*
|
||||||
|
* @param $total
|
||||||
|
* The total number of operations.
|
||||||
|
* @param $current
|
||||||
|
* The number of the current operation. This may be a floating point number
|
||||||
|
* rather than an integer in the case of a multi-step operation that is not
|
||||||
|
* yet complete; in that case, the fractional part of $current represents the
|
||||||
|
* fraction of the operation that has been completed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The properly formatted percentage, as a string. We output percentages
|
||||||
|
* using the correct number of decimal places so that we never print "100%"
|
||||||
|
* until we are finished, but we also never print more decimal places than
|
||||||
|
* are meaningful.
|
||||||
|
*
|
||||||
|
* @see _batch_process()
|
||||||
|
*/
|
||||||
|
function _batch_api_percentage($total, $current) {
|
||||||
|
if (!$total || $total == $current) {
|
||||||
|
// If $total doesn't evaluate as true or is equal to the current set, then
|
||||||
|
// we're finished, and we can return "100".
|
||||||
|
$percentage = "100";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We add a new digit at 200, 2000, etc. (since, for example, 199/200
|
||||||
|
// would round up to 100% if we didn't).
|
||||||
|
$decimal_places = max(0, floor(log10($total / 2.0)) - 1);
|
||||||
|
do {
|
||||||
|
// Calculate the percentage to the specified number of decimal places.
|
||||||
|
$percentage = sprintf('%01.' . $decimal_places . 'f', round($current / $total * 100, $decimal_places));
|
||||||
|
// When $current is an integer, the above calculation will always be
|
||||||
|
// correct. However, if $current is a floating point number (in the case
|
||||||
|
// of a multi-step batch operation that is not yet complete), $percentage
|
||||||
|
// may be erroneously rounded up to 100%. To prevent that, we add one
|
||||||
|
// more decimal place and try again.
|
||||||
|
$decimal_places++;
|
||||||
|
} while ($percentage == '100');
|
||||||
|
}
|
||||||
|
return $percentage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the batch set being currently processed.
|
||||||
|
*/
|
||||||
|
function &_batch_current_set() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
return $batch['sets'][$batch['current_set']];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the next set in a batch.
|
||||||
|
*
|
||||||
|
* If there is a subsequent set in this batch, assign it as the new set to
|
||||||
|
* process and execute its form submit handler (if defined), which may add
|
||||||
|
* further sets to this batch.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if a subsequent set was found in the batch.
|
||||||
|
*/
|
||||||
|
function _batch_next_set() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
if (isset($batch['sets'][$batch['current_set'] + 1])) {
|
||||||
|
$batch['current_set']++;
|
||||||
|
$current_set = &_batch_current_set();
|
||||||
|
if (isset($current_set['form_submit']) && ($function = $current_set['form_submit']) && function_exists($function)) {
|
||||||
|
// We use our stored copies of $form and $form_state to account for
|
||||||
|
// possible alterations by previous form submit handlers.
|
||||||
|
$function($batch['form_state']['complete form'], $batch['form_state']);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the batch processing.
|
||||||
|
*
|
||||||
|
* Call the 'finished' callback of each batch set to allow custom handling of
|
||||||
|
* the results and resolve page redirection.
|
||||||
|
*/
|
||||||
|
function _batch_finished() {
|
||||||
|
$batch = &batch_get();
|
||||||
|
|
||||||
|
// Execute the 'finished' callbacks for each batch set, if defined.
|
||||||
|
foreach ($batch['sets'] as $batch_set) {
|
||||||
|
if (isset($batch_set['finished'])) {
|
||||||
|
// Check if the set requires an additional file for function definitions.
|
||||||
|
if (isset($batch_set['file']) && is_file($batch_set['file'])) {
|
||||||
|
include_once DRUPAL_ROOT . '/' . $batch_set['file'];
|
||||||
|
}
|
||||||
|
if (is_callable($batch_set['finished'])) {
|
||||||
|
$queue = _batch_queue($batch_set);
|
||||||
|
$operations = $queue->getAllItems();
|
||||||
|
call_user_func($batch_set['finished'], $batch_set['success'], $batch_set['results'], $operations, format_interval($batch_set['elapsed'] / 1000));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the batch table and unset the static $batch variable.
|
||||||
|
if ($batch['progressive']) {
|
||||||
|
db_delete('batch')
|
||||||
|
->condition('bid', $batch['id'])
|
||||||
|
->execute();
|
||||||
|
foreach ($batch['sets'] as $batch_set) {
|
||||||
|
if ($queue = _batch_queue($batch_set)) {
|
||||||
|
$queue->deleteQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$_batch = $batch;
|
||||||
|
$batch = NULL;
|
||||||
|
|
||||||
|
// Clean-up the session. Not needed for CLI updates.
|
||||||
|
if (isset($_SESSION)) {
|
||||||
|
unset($_SESSION['batches'][$batch['id']]);
|
||||||
|
if (empty($_SESSION['batches'])) {
|
||||||
|
unset($_SESSION['batches']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect if needed.
|
||||||
|
if ($_batch['progressive']) {
|
||||||
|
// Revert the 'destination' that was saved in batch_process().
|
||||||
|
if (isset($_batch['destination'])) {
|
||||||
|
$_GET['destination'] = $_batch['destination'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the target path to redirect to.
|
||||||
|
if (!isset($_batch['form_state']['redirect'])) {
|
||||||
|
if (isset($_batch['redirect'])) {
|
||||||
|
$_batch['form_state']['redirect'] = $_batch['redirect'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$_batch['form_state']['redirect'] = $_batch['source_url'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use drupal_redirect_form() to handle the redirection logic.
|
||||||
|
drupal_redirect_form($_batch['form_state']);
|
||||||
|
|
||||||
|
// If no redirection happened, redirect to the originating page. In case the
|
||||||
|
// form needs to be rebuilt, save the final $form_state for
|
||||||
|
// drupal_build_form().
|
||||||
|
if (!empty($_batch['form_state']['rebuild'])) {
|
||||||
|
$_SESSION['batch_form_state'] = $_batch['form_state'];
|
||||||
|
}
|
||||||
|
$function = $_batch['redirect_callback'];
|
||||||
|
if (function_exists($function)) {
|
||||||
|
$function($_batch['source_url'], array('query' => array('op' => 'finish', 'id' => $_batch['id'])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shutdown function: Stores the current batch data for the next request.
|
||||||
|
*
|
||||||
|
* @see _batch_page()
|
||||||
|
* @see drupal_register_shutdown_function()
|
||||||
|
*/
|
||||||
|
function _batch_shutdown() {
|
||||||
|
if ($batch = batch_get()) {
|
||||||
|
db_update('batch')
|
||||||
|
->fields(array('batch' => serialize($batch)))
|
||||||
|
->condition('bid', $batch['id'])
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
84
includes/batch.queue.inc
Normal file
84
includes/batch.queue.inc
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Queue handlers used by the Batch API.
|
||||||
|
*
|
||||||
|
* These implementations:
|
||||||
|
* - Ensure FIFO ordering.
|
||||||
|
* - Allow an item to be repeatedly claimed until it is actually deleted (no
|
||||||
|
* notion of lease time or 'expire' date), to allow multipass operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a batch queue.
|
||||||
|
*
|
||||||
|
* Stale items from failed batches are cleaned from the {queue} table on cron
|
||||||
|
* using the 'created' date.
|
||||||
|
*/
|
||||||
|
class BatchQueue extends SystemQueue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides SystemQueue::claimItem().
|
||||||
|
*
|
||||||
|
* Unlike SystemQueue::claimItem(), this method provides a default lease
|
||||||
|
* time of 0 (no expiration) instead of 30. This allows the item to be
|
||||||
|
* claimed repeatedly until it is deleted.
|
||||||
|
*/
|
||||||
|
public function claimItem($lease_time = 0) {
|
||||||
|
$item = db_query_range('SELECT data, item_id FROM {queue} q WHERE name = :name ORDER BY item_id ASC', 0, 1, array(':name' => $this->name))->fetchObject();
|
||||||
|
if ($item) {
|
||||||
|
$item->data = unserialize($item->data);
|
||||||
|
return $item;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all remaining items in the queue.
|
||||||
|
*
|
||||||
|
* This is specific to Batch API and is not part of the DrupalQueueInterface.
|
||||||
|
*/
|
||||||
|
public function getAllItems() {
|
||||||
|
$result = array();
|
||||||
|
$items = db_query('SELECT data FROM {queue} q WHERE name = :name ORDER BY item_id ASC', array(':name' => $this->name))->fetchAll();
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$result[] = unserialize($item->data);
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a batch queue for non-progressive batches.
|
||||||
|
*/
|
||||||
|
class BatchMemoryQueue extends MemoryQueue {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides MemoryQueue::claimItem().
|
||||||
|
*
|
||||||
|
* Unlike MemoryQueue::claimItem(), this method provides a default lease
|
||||||
|
* time of 0 (no expiration) instead of 30. This allows the item to be
|
||||||
|
* claimed repeatedly until it is deleted.
|
||||||
|
*/
|
||||||
|
public function claimItem($lease_time = 0) {
|
||||||
|
if (!empty($this->queue)) {
|
||||||
|
reset($this->queue);
|
||||||
|
return current($this->queue);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all remaining items in the queue.
|
||||||
|
*
|
||||||
|
* This is specific to Batch API and is not part of the DrupalQueueInterface.
|
||||||
|
*/
|
||||||
|
public function getAllItems() {
|
||||||
|
$result = array();
|
||||||
|
foreach ($this->queue as $item) {
|
||||||
|
$result[] = $item->data;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
3851
includes/bootstrap.inc
Normal file
3851
includes/bootstrap.inc
Normal file
File diff suppressed because it is too large
Load diff
74
includes/cache-install.inc
Normal file
74
includes/cache-install.inc
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides a stub cache implementation to be used during installation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a stub cache implementation to be used during installation.
|
||||||
|
*
|
||||||
|
* The stub implementation is needed when database access is not yet available.
|
||||||
|
* Because Drupal's caching system never requires that cached data be present,
|
||||||
|
* these stub functions can short-circuit the process and sidestep the need for
|
||||||
|
* any persistent storage. Obviously, using this cache implementation during
|
||||||
|
* normal operations would have a negative impact on performance.
|
||||||
|
*/
|
||||||
|
class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides DrupalDatabaseCache::get().
|
||||||
|
*/
|
||||||
|
function get($cid) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides DrupalDatabaseCache::getMultiple().
|
||||||
|
*/
|
||||||
|
function getMultiple(&$cids) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides DrupalDatabaseCache::set().
|
||||||
|
*/
|
||||||
|
function set($cid, $data, $expire = CACHE_PERMANENT) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides DrupalDatabaseCache::clear().
|
||||||
|
*/
|
||||||
|
function clear($cid = NULL, $wildcard = FALSE) {
|
||||||
|
// If there is a database cache, attempt to clear it whenever possible. The
|
||||||
|
// reason for doing this is that the database cache can accumulate data
|
||||||
|
// during installation due to any full bootstraps that may occur at the
|
||||||
|
// same time (for example, Ajax requests triggered by the installer). If we
|
||||||
|
// didn't try to clear it whenever this function is called, the data in the
|
||||||
|
// cache would become stale; for example, the installer sometimes calls
|
||||||
|
// variable_set(), which updates the {variable} table and then clears the
|
||||||
|
// cache to make sure that the next page request picks up the new value.
|
||||||
|
// Not actually clearing the cache here therefore leads old variables to be
|
||||||
|
// loaded on the first page requests after installation, which can cause
|
||||||
|
// subtle bugs, some of which would not be fixed unless the site
|
||||||
|
// administrator cleared the cache manually.
|
||||||
|
try {
|
||||||
|
if (class_exists('Database')) {
|
||||||
|
parent::clear($cid, $wildcard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the attempt at clearing the cache causes an error, that means that
|
||||||
|
// either the database connection is not set up yet or the relevant cache
|
||||||
|
// table in the database has not yet been created, so we can safely do
|
||||||
|
// nothing here.
|
||||||
|
catch (Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides DrupalDatabaseCache::isEmpty().
|
||||||
|
*/
|
||||||
|
function isEmpty() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
590
includes/cache.inc
Normal file
590
includes/cache.inc
Normal file
|
@ -0,0 +1,590 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Functions and interfaces for cache handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the cache object for a cache bin.
|
||||||
|
*
|
||||||
|
* By default, this returns an instance of the DrupalDatabaseCache class.
|
||||||
|
* Classes implementing DrupalCacheInterface can register themselves both as a
|
||||||
|
* default implementation and for specific bins.
|
||||||
|
*
|
||||||
|
* @param $bin
|
||||||
|
* The cache bin for which the cache object should be returned.
|
||||||
|
*
|
||||||
|
* @return DrupalCacheInterface
|
||||||
|
* The cache object associated with the specified bin.
|
||||||
|
*
|
||||||
|
* @see DrupalCacheInterface
|
||||||
|
*/
|
||||||
|
function _cache_get_object($bin) {
|
||||||
|
// We do not use drupal_static() here because we do not want to change the
|
||||||
|
// storage of a cache bin mid-request.
|
||||||
|
static $cache_objects;
|
||||||
|
if (!isset($cache_objects[$bin])) {
|
||||||
|
$class = variable_get('cache_class_' . $bin);
|
||||||
|
if (!isset($class)) {
|
||||||
|
$class = variable_get('cache_default_class', 'DrupalDatabaseCache');
|
||||||
|
}
|
||||||
|
$cache_objects[$bin] = new $class($bin);
|
||||||
|
}
|
||||||
|
return $cache_objects[$bin];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns data from the persistent cache.
|
||||||
|
*
|
||||||
|
* Data may be stored as either plain text or as serialized data. cache_get
|
||||||
|
* will automatically return unserialized objects and arrays.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* The cache ID of the data to retrieve.
|
||||||
|
* @param $bin
|
||||||
|
* The cache bin to store the data in. Valid core values are 'cache_block',
|
||||||
|
* 'cache_bootstrap', 'cache_field', 'cache_filter', 'cache_form',
|
||||||
|
* 'cache_menu', 'cache_page', 'cache_path', 'cache_update' or 'cache' for
|
||||||
|
* the default cache.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The cache or FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see cache_set()
|
||||||
|
*/
|
||||||
|
function cache_get($cid, $bin = 'cache') {
|
||||||
|
return _cache_get_object($bin)->get($cid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns data from the persistent cache when given an array of cache IDs.
|
||||||
|
*
|
||||||
|
* @param $cids
|
||||||
|
* An array of cache IDs for the data to retrieve. This is passed by
|
||||||
|
* reference, and will have the IDs successfully returned from cache removed.
|
||||||
|
* @param $bin
|
||||||
|
* The cache bin where the data is stored.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of the items successfully returned from cache indexed by cid.
|
||||||
|
*/
|
||||||
|
function cache_get_multiple(array &$cids, $bin = 'cache') {
|
||||||
|
return _cache_get_object($bin)->getMultiple($cids);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores data in the persistent cache.
|
||||||
|
*
|
||||||
|
* The persistent cache is split up into several cache bins. In the default
|
||||||
|
* cache implementation, each cache bin corresponds to a database table by the
|
||||||
|
* same name. Other implementations might want to store several bins in data
|
||||||
|
* structures that get flushed together. While it is not a problem for most
|
||||||
|
* cache bins if the entries in them are flushed before their expire time, some
|
||||||
|
* might break functionality or are extremely expensive to recalculate. The
|
||||||
|
* other bins are expired automatically by core. Contributed modules can add
|
||||||
|
* additional bins and get them expired automatically by implementing
|
||||||
|
* hook_flush_caches().
|
||||||
|
*
|
||||||
|
* The reasons for having several bins are as follows:
|
||||||
|
* - Smaller bins mean smaller database tables and allow for faster selects and
|
||||||
|
* inserts.
|
||||||
|
* - We try to put fast changing cache items and rather static ones into
|
||||||
|
* different bins. The effect is that only the fast changing bins will need a
|
||||||
|
* lot of writes to disk. The more static bins will also be better cacheable
|
||||||
|
* with MySQL's query cache.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* The cache ID of the data to store.
|
||||||
|
* @param $data
|
||||||
|
* The data to store in the cache. Complex data types will be automatically
|
||||||
|
* serialized before insertion. Strings will be stored as plain text and are
|
||||||
|
* not serialized. Some storage engines only allow objects up to a maximum of
|
||||||
|
* 1MB in size to be stored by default. When caching large arrays or similar,
|
||||||
|
* take care to ensure $data does not exceed this size.
|
||||||
|
* @param $bin
|
||||||
|
* (optional) The cache bin to store the data in. Valid core values are:
|
||||||
|
* - cache: (default) Generic cache storage bin (used for theme registry,
|
||||||
|
* locale date, list of simpletest tests, etc.).
|
||||||
|
* - cache_block: Stores the content of various blocks.
|
||||||
|
* - cache_bootstrap: Stores the class registry, the system list of modules,
|
||||||
|
* the list of which modules implement which hooks, and the Drupal variable
|
||||||
|
* list.
|
||||||
|
* - cache_field: Stores the field data belonging to a given object.
|
||||||
|
* - cache_filter: Stores filtered pieces of content.
|
||||||
|
* - cache_form: Stores multistep forms. Flushing this bin means that some
|
||||||
|
* forms displayed to users lose their state and the data already submitted
|
||||||
|
* to them. This bin should not be flushed before its expired time.
|
||||||
|
* - cache_menu: Stores the structure of visible navigation menus per page.
|
||||||
|
* - cache_page: Stores generated pages for anonymous users. It is flushed
|
||||||
|
* very often, whenever a page changes, at least for every node and comment
|
||||||
|
* submission. This is the only bin affected by the page cache setting on
|
||||||
|
* the administrator panel.
|
||||||
|
* - cache_path: Stores the system paths that have an alias.
|
||||||
|
* @param $expire
|
||||||
|
* (optional) Controls the maximum lifetime of this cache entry. Note that
|
||||||
|
* caches might be subject to clearing at any time, so this setting does not
|
||||||
|
* guarantee a minimum lifetime. With this in mind, the cache should not be
|
||||||
|
* used for data that must be kept during a cache clear, like sessions.
|
||||||
|
*
|
||||||
|
* Use one of the following values:
|
||||||
|
* - CACHE_PERMANENT: Indicates that the item should never be removed unless
|
||||||
|
* explicitly told to using cache_clear_all() with a cache ID.
|
||||||
|
* - CACHE_TEMPORARY: Indicates that the item should be removed at the next
|
||||||
|
* general cache wipe.
|
||||||
|
* - A Unix timestamp: Indicates that the item should be kept at least until
|
||||||
|
* the given time, after which it behaves like CACHE_TEMPORARY.
|
||||||
|
*
|
||||||
|
* @see _update_cache_set()
|
||||||
|
* @see cache_get()
|
||||||
|
*/
|
||||||
|
function cache_set($cid, $data, $bin = 'cache', $expire = CACHE_PERMANENT) {
|
||||||
|
return _cache_get_object($bin)->set($cid, $data, $expire);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expires data from the cache.
|
||||||
|
*
|
||||||
|
* If called with the arguments $cid and $bin set to NULL or omitted, then
|
||||||
|
* expirable entries will be cleared from the cache_page and cache_block bins,
|
||||||
|
* and the $wildcard argument is ignored.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* If set, the cache ID or an array of cache IDs. Otherwise, all cache entries
|
||||||
|
* that can expire are deleted. The $wildcard argument will be ignored if set
|
||||||
|
* to NULL.
|
||||||
|
* @param $bin
|
||||||
|
* If set, the cache bin to delete from. Mandatory argument if $cid is set.
|
||||||
|
* @param $wildcard
|
||||||
|
* If TRUE, the $cid argument must contain a string value and cache IDs
|
||||||
|
* starting with $cid are deleted in addition to the exact cache ID specified
|
||||||
|
* by $cid. If $wildcard is TRUE and $cid is '*', the entire cache is emptied.
|
||||||
|
*/
|
||||||
|
function cache_clear_all($cid = NULL, $bin = NULL, $wildcard = FALSE) {
|
||||||
|
if (!isset($cid) && !isset($bin)) {
|
||||||
|
// Clear the block cache first, so stale data will
|
||||||
|
// not end up in the page cache.
|
||||||
|
if (module_exists('block')) {
|
||||||
|
cache_clear_all(NULL, 'cache_block');
|
||||||
|
}
|
||||||
|
cache_clear_all(NULL, 'cache_page');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return _cache_get_object($bin)->clear($cid, $wildcard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a cache bin is empty.
|
||||||
|
*
|
||||||
|
* A cache bin is considered empty if it does not contain any valid data for any
|
||||||
|
* cache ID.
|
||||||
|
*
|
||||||
|
* @param $bin
|
||||||
|
* The cache bin to check.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the cache bin specified is empty.
|
||||||
|
*/
|
||||||
|
function cache_is_empty($bin) {
|
||||||
|
return _cache_get_object($bin)->isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines an interface for cache implementations.
|
||||||
|
*
|
||||||
|
* All cache implementations have to implement this interface.
|
||||||
|
* DrupalDatabaseCache provides the default implementation, which can be
|
||||||
|
* consulted as an example.
|
||||||
|
*
|
||||||
|
* To make Drupal use your implementation for a certain cache bin, you have to
|
||||||
|
* set a variable with the name of the cache bin as its key and the name of
|
||||||
|
* your class as its value. For example, if your implementation of
|
||||||
|
* DrupalCacheInterface was called MyCustomCache, the following line would make
|
||||||
|
* Drupal use it for the 'cache_page' bin:
|
||||||
|
* @code
|
||||||
|
* variable_set('cache_class_cache_page', 'MyCustomCache');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Additionally, you can register your cache implementation to be used by
|
||||||
|
* default for all cache bins by setting the variable 'cache_default_class' to
|
||||||
|
* the name of your implementation of the DrupalCacheInterface, e.g.
|
||||||
|
* @code
|
||||||
|
* variable_set('cache_default_class', 'MyCustomCache');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* To implement a completely custom cache bin, use the same variable format:
|
||||||
|
* @code
|
||||||
|
* variable_set('cache_class_custom_bin', 'MyCustomCache');
|
||||||
|
* @endcode
|
||||||
|
* To access your custom cache bin, specify the name of the bin when storing
|
||||||
|
* or retrieving cached data:
|
||||||
|
* @code
|
||||||
|
* cache_set($cid, $data, 'custom_bin', $expire);
|
||||||
|
* cache_get($cid, 'custom_bin');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @see _cache_get_object()
|
||||||
|
* @see DrupalDatabaseCache
|
||||||
|
*/
|
||||||
|
interface DrupalCacheInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns data from the persistent cache.
|
||||||
|
*
|
||||||
|
* Data may be stored as either plain text or as serialized data. cache_get()
|
||||||
|
* will automatically return unserialized objects and arrays.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* The cache ID of the data to retrieve.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The cache or FALSE on failure.
|
||||||
|
*/
|
||||||
|
function get($cid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns data from the persistent cache when given an array of cache IDs.
|
||||||
|
*
|
||||||
|
* @param $cids
|
||||||
|
* An array of cache IDs for the data to retrieve. This is passed by
|
||||||
|
* reference, and will have the IDs successfully returned from cache
|
||||||
|
* removed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of the items successfully returned from cache indexed by cid.
|
||||||
|
*/
|
||||||
|
function getMultiple(&$cids);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores data in the persistent cache.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* The cache ID of the data to store.
|
||||||
|
* @param $data
|
||||||
|
* The data to store in the cache. Complex data types will be automatically
|
||||||
|
* serialized before insertion. Strings will be stored as plain text and not
|
||||||
|
* serialized. Some storage engines only allow objects up to a maximum of
|
||||||
|
* 1MB in size to be stored by default. When caching large arrays or
|
||||||
|
* similar, take care to ensure $data does not exceed this size.
|
||||||
|
* @param $expire
|
||||||
|
* (optional) Controls the maximum lifetime of this cache entry. Note that
|
||||||
|
* caches might be subject to clearing at any time, so this setting does not
|
||||||
|
* guarantee a minimum lifetime. With this in mind, the cache should not be
|
||||||
|
* used for data that must be kept during a cache clear, like sessions.
|
||||||
|
*
|
||||||
|
* Use one of the following values:
|
||||||
|
* - CACHE_PERMANENT: Indicates that the item should never be removed unless
|
||||||
|
* explicitly told to using cache_clear_all() with a cache ID.
|
||||||
|
* - CACHE_TEMPORARY: Indicates that the item should be removed at the next
|
||||||
|
* general cache wipe.
|
||||||
|
* - A Unix timestamp: Indicates that the item should be kept at least until
|
||||||
|
* the given time, after which it behaves like CACHE_TEMPORARY.
|
||||||
|
*/
|
||||||
|
function set($cid, $data, $expire = CACHE_PERMANENT);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expires data from the cache.
|
||||||
|
*
|
||||||
|
* If called without arguments, expirable entries will be cleared from the
|
||||||
|
* cache_page and cache_block bins.
|
||||||
|
*
|
||||||
|
* @param $cid
|
||||||
|
* If set, the cache ID or an array of cache IDs. Otherwise, all cache
|
||||||
|
* entries that can expire are deleted. The $wildcard argument will be
|
||||||
|
* ignored if set to NULL.
|
||||||
|
* @param $wildcard
|
||||||
|
* If TRUE, the $cid argument must contain a string value and cache IDs
|
||||||
|
* starting with $cid are deleted in addition to the exact cache ID
|
||||||
|
* specified by $cid. If $wildcard is TRUE and $cid is '*', the entire
|
||||||
|
* cache is emptied.
|
||||||
|
*/
|
||||||
|
function clear($cid = NULL, $wildcard = FALSE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a cache bin is empty.
|
||||||
|
*
|
||||||
|
* A cache bin is considered empty if it does not contain any valid data for
|
||||||
|
* any cache ID.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the cache bin specified is empty.
|
||||||
|
*/
|
||||||
|
function isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a default cache implementation.
|
||||||
|
*
|
||||||
|
* This is Drupal's default cache implementation. It uses the database to store
|
||||||
|
* cached data. Each cache bin corresponds to a database table by the same name.
|
||||||
|
*/
|
||||||
|
class DrupalDatabaseCache implements DrupalCacheInterface {
|
||||||
|
protected $bin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a DrupalDatabaseCache object.
|
||||||
|
*
|
||||||
|
* @param $bin
|
||||||
|
* The cache bin for which the object is created.
|
||||||
|
*/
|
||||||
|
function __construct($bin) {
|
||||||
|
$this->bin = $bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements DrupalCacheInterface::get().
|
||||||
|
*/
|
||||||
|
function get($cid) {
|
||||||
|
$cids = array($cid);
|
||||||
|
$cache = $this->getMultiple($cids);
|
||||||
|
return reset($cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements DrupalCacheInterface::getMultiple().
|
||||||
|
*/
|
||||||
|
function getMultiple(&$cids) {
|
||||||
|
try {
|
||||||
|
// Garbage collection necessary when enforcing a minimum cache lifetime.
|
||||||
|
$this->garbageCollection($this->bin);
|
||||||
|
|
||||||
|
// When serving cached pages, the overhead of using db_select() was found
|
||||||
|
// to add around 30% overhead to the request. Since $this->bin is a
|
||||||
|
// variable, this means the call to db_query() here uses a concatenated
|
||||||
|
// string. This is highly discouraged under any other circumstances, and
|
||||||
|
// is used here only due to the performance overhead we would incur
|
||||||
|
// otherwise. When serving an uncached page, the overhead of using
|
||||||
|
// db_select() is a much smaller proportion of the request.
|
||||||
|
$result = db_query('SELECT cid, data, created, expire, serialized FROM {' . db_escape_table($this->bin) . '} WHERE cid IN (:cids)', array(':cids' => $cids));
|
||||||
|
$cache = array();
|
||||||
|
foreach ($result as $item) {
|
||||||
|
$item = $this->prepareItem($item);
|
||||||
|
if ($item) {
|
||||||
|
$cache[$item->cid] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$cids = array_diff($cids, array_keys($cache));
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// If the database is never going to be available, cache requests should
|
||||||
|
// return FALSE in order to allow exception handling to occur.
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garbage collection for get() and getMultiple().
|
||||||
|
*
|
||||||
|
* @param $bin
|
||||||
|
* The bin being requested.
|
||||||
|
*/
|
||||||
|
protected function garbageCollection() {
|
||||||
|
$cache_lifetime = variable_get('cache_lifetime', 0);
|
||||||
|
|
||||||
|
// Clean-up the per-user cache expiration session data, so that the session
|
||||||
|
// handler can properly clean-up the session data for anonymous users.
|
||||||
|
if (isset($_SESSION['cache_expiration'])) {
|
||||||
|
$expire = REQUEST_TIME - $cache_lifetime;
|
||||||
|
foreach ($_SESSION['cache_expiration'] as $bin => $timestamp) {
|
||||||
|
if ($timestamp < $expire) {
|
||||||
|
unset($_SESSION['cache_expiration'][$bin]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!$_SESSION['cache_expiration']) {
|
||||||
|
unset($_SESSION['cache_expiration']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garbage collection of temporary items is only necessary when enforcing
|
||||||
|
// a minimum cache lifetime.
|
||||||
|
if (!$cache_lifetime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// When cache lifetime is in force, avoid running garbage collection too
|
||||||
|
// often since this will remove temporary cache items indiscriminately.
|
||||||
|
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
|
||||||
|
if ($cache_flush && ($cache_flush + $cache_lifetime <= REQUEST_TIME)) {
|
||||||
|
// Reset the variable immediately to prevent a meltdown in heavy load situations.
|
||||||
|
variable_set('cache_flush_' . $this->bin, 0);
|
||||||
|
// Time to flush old cache data
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('expire', CACHE_PERMANENT, '<>')
|
||||||
|
->condition('expire', $cache_flush, '<=')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a cached item.
|
||||||
|
*
|
||||||
|
* Checks that items are either permanent or did not expire, and unserializes
|
||||||
|
* data as appropriate.
|
||||||
|
*
|
||||||
|
* @param $cache
|
||||||
|
* An item loaded from cache_get() or cache_get_multiple().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The item with data unserialized as appropriate or FALSE if there is no
|
||||||
|
* valid item to load.
|
||||||
|
*/
|
||||||
|
protected function prepareItem($cache) {
|
||||||
|
global $user;
|
||||||
|
|
||||||
|
if (!isset($cache->data)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// If the cached data is temporary and subject to a per-user minimum
|
||||||
|
// lifetime, compare the cache entry timestamp with the user session
|
||||||
|
// cache_expiration timestamp. If the cache entry is too old, ignore it.
|
||||||
|
if ($cache->expire != CACHE_PERMANENT && variable_get('cache_lifetime', 0) && isset($_SESSION['cache_expiration'][$this->bin]) && $_SESSION['cache_expiration'][$this->bin] > $cache->created) {
|
||||||
|
// Ignore cache data that is too old and thus not valid for this user.
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the data is permanent or not subject to a minimum cache lifetime,
|
||||||
|
// unserialize and return the cached data.
|
||||||
|
if ($cache->serialized) {
|
||||||
|
$cache->data = unserialize($cache->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements DrupalCacheInterface::set().
|
||||||
|
*/
|
||||||
|
function set($cid, $data, $expire = CACHE_PERMANENT) {
|
||||||
|
$fields = array(
|
||||||
|
'serialized' => 0,
|
||||||
|
'created' => REQUEST_TIME,
|
||||||
|
'expire' => $expire,
|
||||||
|
);
|
||||||
|
if (!is_string($data)) {
|
||||||
|
$fields['data'] = serialize($data);
|
||||||
|
$fields['serialized'] = 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$fields['data'] = $data;
|
||||||
|
$fields['serialized'] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db_merge($this->bin)
|
||||||
|
->key(array('cid' => $cid))
|
||||||
|
->fields($fields)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// The database may not be available, so we'll ignore cache_set requests.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements DrupalCacheInterface::clear().
|
||||||
|
*/
|
||||||
|
function clear($cid = NULL, $wildcard = FALSE) {
|
||||||
|
global $user;
|
||||||
|
|
||||||
|
if (empty($cid)) {
|
||||||
|
if (variable_get('cache_lifetime', 0)) {
|
||||||
|
// We store the time in the current user's session. We then simulate
|
||||||
|
// that the cache was flushed for this user by not returning cached
|
||||||
|
// data that was cached before the timestamp.
|
||||||
|
$_SESSION['cache_expiration'][$this->bin] = REQUEST_TIME;
|
||||||
|
|
||||||
|
$cache_flush = variable_get('cache_flush_' . $this->bin, 0);
|
||||||
|
if ($cache_flush == 0) {
|
||||||
|
// This is the first request to clear the cache, start a timer.
|
||||||
|
variable_set('cache_flush_' . $this->bin, REQUEST_TIME);
|
||||||
|
}
|
||||||
|
elseif (REQUEST_TIME > ($cache_flush + variable_get('cache_lifetime', 0))) {
|
||||||
|
// Clear the cache for everyone, cache_lifetime seconds have
|
||||||
|
// passed since the first request to clear the cache.
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('expire', CACHE_PERMANENT, '<>')
|
||||||
|
->condition('expire', REQUEST_TIME, '<')
|
||||||
|
->execute();
|
||||||
|
variable_set('cache_flush_' . $this->bin, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// No minimum cache lifetime, flush all temporary cache entries now.
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('expire', CACHE_PERMANENT, '<>')
|
||||||
|
->condition('expire', REQUEST_TIME, '<')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if ($wildcard) {
|
||||||
|
if ($cid == '*') {
|
||||||
|
// Check if $this->bin is a cache table before truncating. Other
|
||||||
|
// cache_clear_all() operations throw a PDO error in this situation,
|
||||||
|
// so we don't need to verify them first. This ensures that non-cache
|
||||||
|
// tables cannot be truncated accidentally.
|
||||||
|
if ($this->isValidBin()) {
|
||||||
|
db_truncate($this->bin)->execute();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Exception(t('Invalid or missing cache bin specified: %bin', array('%bin' => $this->bin)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('cid', db_like($cid) . '%', 'LIKE')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (is_array($cid)) {
|
||||||
|
// Delete in chunks when a large array is passed.
|
||||||
|
do {
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('cid', array_splice($cid, 0, 1000), 'IN')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
while (count($cid));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
db_delete($this->bin)
|
||||||
|
->condition('cid', $cid)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements DrupalCacheInterface::isEmpty().
|
||||||
|
*/
|
||||||
|
function isEmpty() {
|
||||||
|
$this->garbageCollection();
|
||||||
|
$query = db_select($this->bin);
|
||||||
|
$query->addExpression('1');
|
||||||
|
$result = $query->range(0, 1)
|
||||||
|
->execute()
|
||||||
|
->fetchField();
|
||||||
|
return empty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if $this->bin represents a valid cache table.
|
||||||
|
*
|
||||||
|
* This check is required to ensure that non-cache tables are not truncated
|
||||||
|
* accidentally when calling cache_clear_all().
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
function isValidBin() {
|
||||||
|
if ($this->bin == 'cache' || substr($this->bin, 0, 6) == 'cache_') {
|
||||||
|
// Skip schema check for bins with standard table names.
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
// These fields are required for any cache table.
|
||||||
|
$fields = array('cid', 'data', 'expire', 'created', 'serialized');
|
||||||
|
// Load the table schema.
|
||||||
|
$schema = drupal_get_schema($this->bin);
|
||||||
|
// Confirm that all fields are present.
|
||||||
|
return isset($schema['fields']) && !array_diff($fields, array_keys($schema['fields']));
|
||||||
|
}
|
||||||
|
}
|
8485
includes/common.inc
Normal file
8485
includes/common.inc
Normal file
File diff suppressed because it is too large
Load diff
3098
includes/database/database.inc
Normal file
3098
includes/database/database.inc
Normal file
File diff suppressed because it is too large
Load diff
161
includes/database/log.inc
Normal file
161
includes/database/log.inc
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Logging classes for the database layer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database query logger.
|
||||||
|
*
|
||||||
|
* We log queries in a separate object rather than in the connection object
|
||||||
|
* because we want to be able to see all queries sent to a given database, not
|
||||||
|
* database target. If we logged the queries in each connection object we
|
||||||
|
* would not be able to track what queries went to which target.
|
||||||
|
*
|
||||||
|
* Every connection has one and only one logging object on it for all targets
|
||||||
|
* and logging keys.
|
||||||
|
*/
|
||||||
|
class DatabaseLog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache of logged queries. This will only be used if the query logger is enabled.
|
||||||
|
*
|
||||||
|
* The structure for the logging array is as follows:
|
||||||
|
*
|
||||||
|
* array(
|
||||||
|
* $logging_key = array(
|
||||||
|
* array(query => '', args => array(), caller => '', target => '', time => 0),
|
||||||
|
* array(query => '', args => array(), caller => '', target => '', time => 0),
|
||||||
|
* ),
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $queryLog = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The connection key for which this object is logging.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $connectionKey = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor.
|
||||||
|
*
|
||||||
|
* @param $key
|
||||||
|
* The database connection key for which to enable logging.
|
||||||
|
*/
|
||||||
|
public function __construct($key = 'default') {
|
||||||
|
$this->connectionKey = $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Begin logging queries to the specified connection and logging key.
|
||||||
|
*
|
||||||
|
* If the specified logging key is already running this method does nothing.
|
||||||
|
*
|
||||||
|
* @param $logging_key
|
||||||
|
* The identification key for this log request. By specifying different
|
||||||
|
* logging keys we are able to start and stop multiple logging runs
|
||||||
|
* simultaneously without them colliding.
|
||||||
|
*/
|
||||||
|
public function start($logging_key) {
|
||||||
|
if (empty($this->queryLog[$logging_key])) {
|
||||||
|
$this->clear($logging_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the query log for the specified logging key so far.
|
||||||
|
*
|
||||||
|
* @param $logging_key
|
||||||
|
* The logging key to fetch.
|
||||||
|
* @return
|
||||||
|
* An indexed array of all query records for this logging key.
|
||||||
|
*/
|
||||||
|
public function get($logging_key) {
|
||||||
|
return $this->queryLog[$logging_key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty the query log for the specified logging key.
|
||||||
|
*
|
||||||
|
* This method does not stop logging, it simply clears the log. To stop
|
||||||
|
* logging, use the end() method.
|
||||||
|
*
|
||||||
|
* @param $logging_key
|
||||||
|
* The logging key to empty.
|
||||||
|
*/
|
||||||
|
public function clear($logging_key) {
|
||||||
|
$this->queryLog[$logging_key] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop logging for the specified logging key.
|
||||||
|
*
|
||||||
|
* @param $logging_key
|
||||||
|
* The logging key to stop.
|
||||||
|
*/
|
||||||
|
public function end($logging_key) {
|
||||||
|
unset($this->queryLog[$logging_key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a query to all active logging keys.
|
||||||
|
*
|
||||||
|
* @param $statement
|
||||||
|
* The prepared statement object to log.
|
||||||
|
* @param $args
|
||||||
|
* The arguments passed to the statement object.
|
||||||
|
* @param $time
|
||||||
|
* The time in milliseconds the query took to execute.
|
||||||
|
*/
|
||||||
|
public function log(DatabaseStatementInterface $statement, $args, $time) {
|
||||||
|
foreach (array_keys($this->queryLog) as $key) {
|
||||||
|
$this->queryLog[$key][] = array(
|
||||||
|
'query' => $statement->getQueryString(),
|
||||||
|
'args' => $args,
|
||||||
|
'target' => $statement->dbh->getTarget(),
|
||||||
|
'caller' => $this->findCaller(),
|
||||||
|
'time' => $time,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the routine that called this query.
|
||||||
|
*
|
||||||
|
* We define "the routine that called this query" as the first entry in
|
||||||
|
* the call stack that is not inside includes/database and does have a file
|
||||||
|
* (which excludes call_user_func_array(), anonymous functions and similar).
|
||||||
|
* That makes the climbing logic very simple, and handles the variable stack
|
||||||
|
* depth caused by the query builders.
|
||||||
|
*
|
||||||
|
* @link http://www.php.net/debug_backtrace
|
||||||
|
* @return
|
||||||
|
* This method returns a stack trace entry similar to that generated by
|
||||||
|
* debug_backtrace(). However, it flattens the trace entry and the trace
|
||||||
|
* entry before it so that we get the function and args of the function that
|
||||||
|
* called into the database system, not the function and args of the
|
||||||
|
* database call itself.
|
||||||
|
*/
|
||||||
|
public function findCaller() {
|
||||||
|
$stack = debug_backtrace();
|
||||||
|
$stack_count = count($stack);
|
||||||
|
for ($i = 0; $i < $stack_count; ++$i) {
|
||||||
|
if (!empty($stack[$i]['file']) && strpos($stack[$i]['file'], 'includes' . DIRECTORY_SEPARATOR . 'database') === FALSE) {
|
||||||
|
$stack[$i] += array('args' => array());
|
||||||
|
return array(
|
||||||
|
'file' => $stack[$i]['file'],
|
||||||
|
'line' => $stack[$i]['line'],
|
||||||
|
'function' => $stack[$i + 1]['function'],
|
||||||
|
'class' => isset($stack[$i + 1]['class']) ? $stack[$i + 1]['class'] : NULL,
|
||||||
|
'type' => isset($stack[$i + 1]['type']) ? $stack[$i + 1]['type'] : NULL,
|
||||||
|
'args' => $stack[$i + 1]['args'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
256
includes/database/mysql/database.inc
Normal file
256
includes/database/mysql/database.inc
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database interface code for MySQL database servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DatabaseConnection_mysql extends DatabaseConnection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flag to indicate if the cleanup function in __destruct() should run.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $needsCleanup = FALSE;
|
||||||
|
|
||||||
|
public function __construct(array $connection_options = array()) {
|
||||||
|
// This driver defaults to transaction support, except if explicitly passed FALSE.
|
||||||
|
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
|
||||||
|
|
||||||
|
// MySQL never supports transactional DDL.
|
||||||
|
$this->transactionalDDLSupport = FALSE;
|
||||||
|
|
||||||
|
$this->connectionOptions = $connection_options;
|
||||||
|
|
||||||
|
$charset = 'utf8';
|
||||||
|
// Check if the charset is overridden to utf8mb4 in settings.php.
|
||||||
|
if ($this->utf8mb4IsActive()) {
|
||||||
|
$charset = 'utf8mb4';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The DSN should use either a socket or a host/port.
|
||||||
|
if (isset($connection_options['unix_socket'])) {
|
||||||
|
$dsn = 'mysql:unix_socket=' . $connection_options['unix_socket'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Default to TCP connection on port 3306.
|
||||||
|
$dsn = 'mysql:host=' . $connection_options['host'] . ';port=' . (empty($connection_options['port']) ? 3306 : $connection_options['port']);
|
||||||
|
}
|
||||||
|
// Character set is added to dsn to ensure PDO uses the proper character
|
||||||
|
// set when escaping. This has security implications. See
|
||||||
|
// https://www.drupal.org/node/1201452 for further discussion.
|
||||||
|
$dsn .= ';charset=' . $charset;
|
||||||
|
$dsn .= ';dbname=' . $connection_options['database'];
|
||||||
|
// Allow PDO options to be overridden.
|
||||||
|
$connection_options += array(
|
||||||
|
'pdo' => array(),
|
||||||
|
);
|
||||||
|
$connection_options['pdo'] += array(
|
||||||
|
// So we don't have to mess around with cursors and unbuffered queries by default.
|
||||||
|
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => TRUE,
|
||||||
|
// Because MySQL's prepared statements skip the query cache, because it's dumb.
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => TRUE,
|
||||||
|
);
|
||||||
|
if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
|
||||||
|
// An added connection option in PHP 5.5.21+ to optionally limit SQL to a
|
||||||
|
// single statement like mysqli.
|
||||||
|
$connection_options['pdo'] += array(PDO::MYSQL_ATTR_MULTI_STATEMENTS => FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
|
||||||
|
|
||||||
|
// Force MySQL to use the UTF-8 character set. Also set the collation, if a
|
||||||
|
// certain one has been set; otherwise, MySQL defaults to 'utf8_general_ci'
|
||||||
|
// for UTF-8.
|
||||||
|
if (!empty($connection_options['collation'])) {
|
||||||
|
$this->exec('SET NAMES ' . $charset . ' COLLATE ' . $connection_options['collation']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->exec('SET NAMES ' . $charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set MySQL init_commands if not already defined. Default Drupal's MySQL
|
||||||
|
// behavior to conform more closely to SQL standards. This allows Drupal
|
||||||
|
// to run almost seamlessly on many different kinds of database systems.
|
||||||
|
// These settings force MySQL to behave the same as postgresql, or sqlite
|
||||||
|
// in regards to syntax interpretation and invalid data handling. See
|
||||||
|
// http://drupal.org/node/344575 for further discussion. Also, as MySQL 5.5
|
||||||
|
// changed the meaning of TRADITIONAL we need to spell out the modes one by
|
||||||
|
// one.
|
||||||
|
$connection_options += array(
|
||||||
|
'init_commands' => array(),
|
||||||
|
);
|
||||||
|
$connection_options['init_commands'] += array(
|
||||||
|
'sql_mode' => "SET sql_mode = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE,STRICT_TRANS_TABLES,STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER'",
|
||||||
|
);
|
||||||
|
// Execute initial commands.
|
||||||
|
foreach ($connection_options['init_commands'] as $sql) {
|
||||||
|
$this->exec($sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
if ($this->needsCleanup) {
|
||||||
|
$this->nextIdDelete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
|
||||||
|
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryTemporary($query, array $args = array(), array $options = array()) {
|
||||||
|
$tablename = $this->generateTemporaryTableName();
|
||||||
|
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} Engine=MEMORY ' . $query, $args, $options);
|
||||||
|
return $tablename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function driver() {
|
||||||
|
return 'mysql';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseType() {
|
||||||
|
return 'mysql';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapConditionOperator($operator) {
|
||||||
|
// We don't want to override any of the defaults.
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextId($existing_id = 0) {
|
||||||
|
$new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
|
||||||
|
// This should only happen after an import or similar event.
|
||||||
|
if ($existing_id >= $new_id) {
|
||||||
|
// If we INSERT a value manually into the sequences table, on the next
|
||||||
|
// INSERT, MySQL will generate a larger value. However, there is no way
|
||||||
|
// of knowing whether this value already exists in the table. MySQL
|
||||||
|
// provides an INSERT IGNORE which would work, but that can mask problems
|
||||||
|
// other than duplicate keys. Instead, we use INSERT ... ON DUPLICATE KEY
|
||||||
|
// UPDATE in such a way that the UPDATE does not do anything. This way,
|
||||||
|
// duplicate keys do not generate errors but everything else does.
|
||||||
|
$this->query('INSERT INTO {sequences} (value) VALUES (:value) ON DUPLICATE KEY UPDATE value = value', array(':value' => $existing_id));
|
||||||
|
$new_id = $this->query('INSERT INTO {sequences} () VALUES ()', array(), array('return' => Database::RETURN_INSERT_ID));
|
||||||
|
}
|
||||||
|
$this->needsCleanup = TRUE;
|
||||||
|
return $new_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextIdDelete() {
|
||||||
|
// While we want to clean up the table to keep it up from occupying too
|
||||||
|
// much storage and memory, we must keep the highest value in the table
|
||||||
|
// because InnoDB uses an in-memory auto-increment counter as long as the
|
||||||
|
// server runs. When the server is stopped and restarted, InnoDB
|
||||||
|
// reinitializes the counter for each table for the first INSERT to the
|
||||||
|
// table based solely on values from the table so deleting all values would
|
||||||
|
// be a problem in this case. Also, TRUNCATE resets the auto increment
|
||||||
|
// counter.
|
||||||
|
try {
|
||||||
|
$max_id = $this->query('SELECT MAX(value) FROM {sequences}')->fetchField();
|
||||||
|
// We know we are using MySQL here, no need for the slower db_delete().
|
||||||
|
$this->query('DELETE FROM {sequences} WHERE value < :value', array(':value' => $max_id));
|
||||||
|
}
|
||||||
|
// During testing, this function is called from shutdown with the
|
||||||
|
// simpletest prefix stored in $this->connection, and those tables are gone
|
||||||
|
// by the time shutdown is called so we need to ignore the database
|
||||||
|
// errors. There is no problem with completely ignoring errors here: if
|
||||||
|
// these queries fail, the sequence will work just fine, just use a bit
|
||||||
|
// more database storage and memory.
|
||||||
|
catch (PDOException $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden to work around issues to MySQL not supporting transactional DDL.
|
||||||
|
*/
|
||||||
|
protected function popCommittableTransactions() {
|
||||||
|
// Commit all the committable layers.
|
||||||
|
foreach (array_reverse($this->transactionLayers) as $name => $active) {
|
||||||
|
// Stop once we found an active transaction.
|
||||||
|
if ($active) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are no more layers left then we should commit.
|
||||||
|
unset($this->transactionLayers[$name]);
|
||||||
|
if (empty($this->transactionLayers)) {
|
||||||
|
if (!PDO::commit()) {
|
||||||
|
throw new DatabaseTransactionCommitFailedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Attempt to release this savepoint in the standard way.
|
||||||
|
try {
|
||||||
|
$this->query('RELEASE SAVEPOINT ' . $name);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
// However, in MySQL (InnoDB), savepoints are automatically committed
|
||||||
|
// when tables are altered or created (DDL transactions are not
|
||||||
|
// supported). This can cause exceptions due to trying to release
|
||||||
|
// savepoints which no longer exist.
|
||||||
|
//
|
||||||
|
// To avoid exceptions when no actual error has occurred, we silently
|
||||||
|
// succeed for MySQL error code 1305 ("SAVEPOINT does not exist").
|
||||||
|
if ($e->errorInfo[1] == '1305') {
|
||||||
|
// If one SAVEPOINT was released automatically, then all were.
|
||||||
|
// Therefore, clean the transaction stack.
|
||||||
|
$this->transactionLayers = array();
|
||||||
|
// We also have to explain to PDO that the transaction stack has
|
||||||
|
// been cleaned-up.
|
||||||
|
PDO::commit();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsConfigurable() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsActive() {
|
||||||
|
return isset($this->connectionOptions['charset']) && $this->connectionOptions['charset'] === 'utf8mb4';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsSupported() {
|
||||||
|
// Ensure that the MySQL driver supports utf8mb4 encoding.
|
||||||
|
$version = $this->getAttribute(PDO::ATTR_CLIENT_VERSION);
|
||||||
|
if (strpos($version, 'mysqlnd') !== FALSE) {
|
||||||
|
// The mysqlnd driver supports utf8mb4 starting at version 5.0.9.
|
||||||
|
$version = preg_replace('/^\D+([\d.]+).*/', '$1', $version);
|
||||||
|
if (version_compare($version, '5.0.9', '<')) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The libmysqlclient driver supports utf8mb4 starting at version 5.5.3.
|
||||||
|
if (version_compare($version, '5.5.3', '<')) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the MySQL server supports large prefixes and utf8mb4.
|
||||||
|
try {
|
||||||
|
$this->query("CREATE TABLE {drupal_utf8mb4_test} (id VARCHAR(255), PRIMARY KEY(id(255))) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ROW_FORMAT=DYNAMIC ENGINE=INNODB");
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$this->query("DROP TABLE {drupal_utf8mb4_test}");
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
33
includes/database/mysql/install.inc
Normal file
33
includes/database/mysql/install.inc
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Installation code for MySQL embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies installation tasks for MySQL and equivalent databases.
|
||||||
|
*/
|
||||||
|
class DatabaseTasks_mysql extends DatabaseTasks {
|
||||||
|
/**
|
||||||
|
* The PDO driver name for MySQL and equivalent databases.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $pdoDriver = 'mysql';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a human-readable name string for MySQL and equivalent databases.
|
||||||
|
*/
|
||||||
|
public function name() {
|
||||||
|
return st('MySQL, MariaDB, or equivalent');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the minimum version for MySQL.
|
||||||
|
*/
|
||||||
|
public function minimumVersion() {
|
||||||
|
return '5.0.15';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
94
includes/database/mysql/query.inc
Normal file
94
includes/database/mysql/query.inc
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Query code for MySQL embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class InsertQuery_mysql extends InsertQuery {
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
if (!$this->preExecute()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're selecting from a SelectQuery, finish building the query and
|
||||||
|
// pass it back, as any remaining options are irrelevant.
|
||||||
|
if (empty($this->fromQuery)) {
|
||||||
|
$max_placeholder = 0;
|
||||||
|
$values = array();
|
||||||
|
foreach ($this->insertValues as $insert_values) {
|
||||||
|
foreach ($insert_values as $value) {
|
||||||
|
$values[':db_insert_placeholder_' . $max_placeholder++] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$values = $this->fromQuery->getArguments();
|
||||||
|
}
|
||||||
|
|
||||||
|
$last_insert_id = $this->connection->query((string) $this, $values, $this->queryOptions);
|
||||||
|
|
||||||
|
// Re-initialize the values array so that we can re-use this query.
|
||||||
|
$this->insertValues = array();
|
||||||
|
|
||||||
|
return $last_insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
// Create a sanitized comment string to prepend to the query.
|
||||||
|
$comments = $this->connection->makeComment($this->comments);
|
||||||
|
|
||||||
|
// Default fields are always placed first for consistency.
|
||||||
|
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||||
|
|
||||||
|
// If we're selecting from a SelectQuery, finish building the query and
|
||||||
|
// pass it back, as any remaining options are irrelevant.
|
||||||
|
if (!empty($this->fromQuery)) {
|
||||||
|
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
|
||||||
|
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||||
|
|
||||||
|
$max_placeholder = 0;
|
||||||
|
$values = array();
|
||||||
|
if (count($this->insertValues)) {
|
||||||
|
foreach ($this->insertValues as $insert_values) {
|
||||||
|
$placeholders = array();
|
||||||
|
|
||||||
|
// Default fields aren't really placeholders, but this is the most convenient
|
||||||
|
// way to handle them.
|
||||||
|
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||||
|
|
||||||
|
$new_placeholder = $max_placeholder + count($insert_values);
|
||||||
|
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||||
|
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||||
|
}
|
||||||
|
$max_placeholder = $new_placeholder;
|
||||||
|
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||||
|
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||||
|
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query .= implode(', ', $values);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TruncateQuery_mysql extends TruncateQuery { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
544
includes/database/mysql/schema.inc
Normal file
544
includes/database/mysql/schema.inc
Normal file
|
@ -0,0 +1,544 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database schema code for MySQL database servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup schemaapi
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DatabaseSchema_mysql extends DatabaseSchema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length of a table comment in MySQL.
|
||||||
|
*/
|
||||||
|
const COMMENT_MAX_TABLE = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum length of a column comment in MySQL.
|
||||||
|
*/
|
||||||
|
const COMMENT_MAX_COLUMN = 255;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the table and database name from the prefix.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A keyed array with information about the database, table name and prefix.
|
||||||
|
*/
|
||||||
|
protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
|
||||||
|
$info = array('prefix' => $this->connection->tablePrefix($table));
|
||||||
|
if ($add_prefix) {
|
||||||
|
$table = $info['prefix'] . $table;
|
||||||
|
}
|
||||||
|
if (($pos = strpos($table, '.')) !== FALSE) {
|
||||||
|
$info['database'] = substr($table, 0, $pos);
|
||||||
|
$info['table'] = substr($table, ++$pos);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$db_info = $this->connection->getConnectionOptions();
|
||||||
|
$info['database'] = $db_info['database'];
|
||||||
|
$info['table'] = $table;
|
||||||
|
}
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a condition to match a table name against a standard information_schema.
|
||||||
|
*
|
||||||
|
* MySQL uses databases like schemas rather than catalogs so when we build
|
||||||
|
* a condition to query the information_schema.tables, we set the default
|
||||||
|
* database as the schema unless specified otherwise, and exclude table_catalog
|
||||||
|
* from the condition criteria.
|
||||||
|
*/
|
||||||
|
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
|
||||||
|
$info = $this->connection->getConnectionOptions();
|
||||||
|
|
||||||
|
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
|
||||||
|
|
||||||
|
$condition = new DatabaseCondition('AND');
|
||||||
|
$condition->condition('table_schema', $table_info['database']);
|
||||||
|
$condition->condition('table_name', $table_info['table'], $operator);
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SQL to create a new table from a Drupal schema definition.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the table to create.
|
||||||
|
* @param $table
|
||||||
|
* A Schema API table definition array.
|
||||||
|
* @return
|
||||||
|
* An array of SQL statements to create the table.
|
||||||
|
*/
|
||||||
|
protected function createTableSql($name, $table) {
|
||||||
|
$info = $this->connection->getConnectionOptions();
|
||||||
|
|
||||||
|
// Provide defaults if needed.
|
||||||
|
$table += array(
|
||||||
|
'mysql_engine' => 'InnoDB',
|
||||||
|
// Allow the default charset to be overridden in settings.php.
|
||||||
|
'mysql_character_set' => $this->connection->utf8mb4IsActive() ? 'utf8mb4' : 'utf8',
|
||||||
|
);
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE {" . $name . "} (\n";
|
||||||
|
|
||||||
|
// Add the SQL statement for each field.
|
||||||
|
foreach ($table['fields'] as $field_name => $field) {
|
||||||
|
$sql .= $this->createFieldSql($field_name, $this->processField($field)) . ", \n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process keys & indexes.
|
||||||
|
$keys = $this->createKeysSql($table);
|
||||||
|
if (count($keys)) {
|
||||||
|
$sql .= implode(", \n", $keys) . ", \n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the last comma and space.
|
||||||
|
$sql = substr($sql, 0, -3) . "\n) ";
|
||||||
|
|
||||||
|
$sql .= 'ENGINE = ' . $table['mysql_engine'] . ' DEFAULT CHARACTER SET ' . $table['mysql_character_set'];
|
||||||
|
// By default, MySQL uses the default collation for new tables, which is
|
||||||
|
// 'utf8_general_ci' for utf8. If an alternate collation has been set, it
|
||||||
|
// needs to be explicitly specified.
|
||||||
|
// @see DatabaseConnection_mysql
|
||||||
|
if (!empty($info['collation'])) {
|
||||||
|
$sql .= ' COLLATE ' . $info['collation'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// The row format needs to be either DYNAMIC or COMPRESSED in order to allow
|
||||||
|
// for the innodb_large_prefix setting to take effect, see
|
||||||
|
// https://dev.mysql.com/doc/refman/5.6/en/create-table.html
|
||||||
|
if ($this->connection->utf8mb4IsActive()) {
|
||||||
|
$sql .= ' ROW_FORMAT=DYNAMIC';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add table comment.
|
||||||
|
if (!empty($table['description'])) {
|
||||||
|
$sql .= ' COMMENT ' . $this->prepareComment($table['description'], self::COMMENT_MAX_TABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SQL string for a field to be used in table creation or alteration.
|
||||||
|
*
|
||||||
|
* Before passing a field out of a schema definition into this function it has
|
||||||
|
* to be processed by _db_process_field().
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* Name of the field.
|
||||||
|
* @param $spec
|
||||||
|
* The field specification, as per the schema data structure format.
|
||||||
|
*/
|
||||||
|
protected function createFieldSql($name, $spec) {
|
||||||
|
$sql = "`" . $name . "` " . $spec['mysql_type'];
|
||||||
|
|
||||||
|
if (in_array($spec['mysql_type'], array('VARCHAR', 'CHAR', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT', 'TEXT'))) {
|
||||||
|
if (isset($spec['length'])) {
|
||||||
|
$sql .= '(' . $spec['length'] . ')';
|
||||||
|
}
|
||||||
|
if (!empty($spec['binary'])) {
|
||||||
|
$sql .= ' BINARY';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (isset($spec['precision']) && isset($spec['scale'])) {
|
||||||
|
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($spec['unsigned'])) {
|
||||||
|
$sql .= ' unsigned';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($spec['not null'])) {
|
||||||
|
if ($spec['not null']) {
|
||||||
|
$sql .= ' NOT NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sql .= ' NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($spec['auto_increment'])) {
|
||||||
|
$sql .= ' auto_increment';
|
||||||
|
}
|
||||||
|
|
||||||
|
// $spec['default'] can be NULL, so we explicitly check for the key here.
|
||||||
|
if (array_key_exists('default', $spec)) {
|
||||||
|
if (is_string($spec['default'])) {
|
||||||
|
$spec['default'] = "'" . $spec['default'] . "'";
|
||||||
|
}
|
||||||
|
elseif (!isset($spec['default'])) {
|
||||||
|
$spec['default'] = 'NULL';
|
||||||
|
}
|
||||||
|
$sql .= ' DEFAULT ' . $spec['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($spec['not null']) && !isset($spec['default'])) {
|
||||||
|
$sql .= ' DEFAULT NULL';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add column comment.
|
||||||
|
if (!empty($spec['description'])) {
|
||||||
|
$sql .= ' COMMENT ' . $this->prepareComment($spec['description'], self::COMMENT_MAX_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set database-engine specific properties for a field.
|
||||||
|
*
|
||||||
|
* @param $field
|
||||||
|
* A field description array, as specified in the schema documentation.
|
||||||
|
*/
|
||||||
|
protected function processField($field) {
|
||||||
|
|
||||||
|
if (!isset($field['size'])) {
|
||||||
|
$field['size'] = 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the correct database-engine specific datatype.
|
||||||
|
// In case one is already provided, force it to uppercase.
|
||||||
|
if (isset($field['mysql_type'])) {
|
||||||
|
$field['mysql_type'] = drupal_strtoupper($field['mysql_type']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$map = $this->getFieldTypeMap();
|
||||||
|
$field['mysql_type'] = $map[$field['type'] . ':' . $field['size']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||||
|
$field['auto_increment'] = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFieldTypeMap() {
|
||||||
|
// Put :normal last so it gets preserved by array_flip. This makes
|
||||||
|
// it much easier for modules (such as schema.module) to map
|
||||||
|
// database types back into schema types.
|
||||||
|
// $map does not use drupal_static as its value never changes.
|
||||||
|
static $map = array(
|
||||||
|
'varchar:normal' => 'VARCHAR',
|
||||||
|
'char:normal' => 'CHAR',
|
||||||
|
|
||||||
|
'text:tiny' => 'TINYTEXT',
|
||||||
|
'text:small' => 'TINYTEXT',
|
||||||
|
'text:medium' => 'MEDIUMTEXT',
|
||||||
|
'text:big' => 'LONGTEXT',
|
||||||
|
'text:normal' => 'TEXT',
|
||||||
|
|
||||||
|
'serial:tiny' => 'TINYINT',
|
||||||
|
'serial:small' => 'SMALLINT',
|
||||||
|
'serial:medium' => 'MEDIUMINT',
|
||||||
|
'serial:big' => 'BIGINT',
|
||||||
|
'serial:normal' => 'INT',
|
||||||
|
|
||||||
|
'int:tiny' => 'TINYINT',
|
||||||
|
'int:small' => 'SMALLINT',
|
||||||
|
'int:medium' => 'MEDIUMINT',
|
||||||
|
'int:big' => 'BIGINT',
|
||||||
|
'int:normal' => 'INT',
|
||||||
|
|
||||||
|
'float:tiny' => 'FLOAT',
|
||||||
|
'float:small' => 'FLOAT',
|
||||||
|
'float:medium' => 'FLOAT',
|
||||||
|
'float:big' => 'DOUBLE',
|
||||||
|
'float:normal' => 'FLOAT',
|
||||||
|
|
||||||
|
'numeric:normal' => 'DECIMAL',
|
||||||
|
|
||||||
|
'blob:big' => 'LONGBLOB',
|
||||||
|
'blob:normal' => 'BLOB',
|
||||||
|
);
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createKeysSql($spec) {
|
||||||
|
$keys = array();
|
||||||
|
|
||||||
|
if (!empty($spec['primary key'])) {
|
||||||
|
$keys[] = 'PRIMARY KEY (' . $this->createKeysSqlHelper($spec['primary key']) . ')';
|
||||||
|
}
|
||||||
|
if (!empty($spec['unique keys'])) {
|
||||||
|
foreach ($spec['unique keys'] as $key => $fields) {
|
||||||
|
$keys[] = 'UNIQUE KEY `' . $key . '` (' . $this->createKeysSqlHelper($fields) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($spec['indexes'])) {
|
||||||
|
foreach ($spec['indexes'] as $index => $fields) {
|
||||||
|
$keys[] = 'INDEX `' . $index . '` (' . $this->createKeysSqlHelper($fields) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createKeySql($fields) {
|
||||||
|
$return = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$return[] = '`' . $field[0] . '`(' . $field[1] . ')';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = '`' . $field . '`';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createKeysSqlHelper($fields) {
|
||||||
|
$return = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$return[] = '`' . $field[0] . '`(' . $field[1] . ')';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = '`' . $field . '`';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameTable($table, $new_name) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
if ($this->tableExists($new_name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $this->getPrefixInfo($new_name);
|
||||||
|
return $this->connection->query('ALTER TABLE {' . $table . '} RENAME TO `' . $info['table'] . '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropTable($table) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('DROP TABLE {' . $table . '}');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addField($table, $field, $spec, $keys_new = array()) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
if ($this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$fixnull = FALSE;
|
||||||
|
if (!empty($spec['not null']) && !isset($spec['default'])) {
|
||||||
|
$fixnull = TRUE;
|
||||||
|
$spec['not null'] = FALSE;
|
||||||
|
}
|
||||||
|
$query = 'ALTER TABLE {' . $table . '} ADD ';
|
||||||
|
$query .= $this->createFieldSql($field, $this->processField($spec));
|
||||||
|
if ($keys_sql = $this->createKeysSql($keys_new)) {
|
||||||
|
$query .= ', ADD ' . implode(', ADD ', $keys_sql);
|
||||||
|
}
|
||||||
|
$this->connection->query($query);
|
||||||
|
if (isset($spec['initial'])) {
|
||||||
|
$this->connection->update($table)
|
||||||
|
->fields(array($field => $spec['initial']))
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
if ($fixnull) {
|
||||||
|
$spec['not null'] = TRUE;
|
||||||
|
$this->changeField($table, $field, $field, $spec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropField($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP `' . $field . '`');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetDefault($table, $field, $default) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($default)) {
|
||||||
|
$default = 'NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$default = is_string($default) ? "'$default'" : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` SET DEFAULT ' . $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetNoDefault($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN `' . $field . '` DROP DEFAULT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexExists($table, $name) {
|
||||||
|
// Returns one row for each column in the index. Result is string or FALSE.
|
||||||
|
// Details at http://dev.mysql.com/doc/refman/5.0/en/show-index.html
|
||||||
|
$row = $this->connection->query('SHOW INDEX FROM {' . $table . "} WHERE key_name = '$name'")->fetchAssoc();
|
||||||
|
return isset($row['Key_name']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPrimaryKey($table, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, 'PRIMARY')) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . $this->createKeySql($fields) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropPrimaryKey($table) {
|
||||||
|
if (!$this->indexExists($table, 'PRIMARY')) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP PRIMARY KEY');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUniqueKey($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, $name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD UNIQUE KEY `' . $name . '` (' . $this->createKeySql($fields) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropUniqueKey($table, $name) {
|
||||||
|
if (!$this->indexExists($table, $name)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP KEY `' . $name . '`');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addIndex($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, $name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD INDEX `' . $name . '` (' . $this->createKeySql($fields) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropIndex($table, $name) {
|
||||||
|
if (!$this->indexExists($table, $name)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP INDEX `' . $name . '`');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", array('@table' => $table, '@name' => $field)));
|
||||||
|
}
|
||||||
|
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = 'ALTER TABLE {' . $table . '} CHANGE `' . $field . '` ' . $this->createFieldSql($field_new, $this->processField($spec));
|
||||||
|
if ($keys_sql = $this->createKeysSql($keys_new)) {
|
||||||
|
$sql .= ', ADD ' . implode(', ADD ', $keys_sql);
|
||||||
|
}
|
||||||
|
$this->connection->query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareComment($comment, $length = NULL) {
|
||||||
|
// Work around a bug in some versions of PDO, see http://bugs.php.net/bug.php?id=41125
|
||||||
|
$comment = str_replace("'", '’', $comment);
|
||||||
|
|
||||||
|
// Truncate comment to maximum comment length.
|
||||||
|
if (isset($length)) {
|
||||||
|
// Add table prefixes before truncating.
|
||||||
|
$comment = truncate_utf8($this->connection->prefixTables($comment), $length, TRUE, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->connection->quote($comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a table or column comment.
|
||||||
|
*/
|
||||||
|
public function getComment($table, $column = NULL) {
|
||||||
|
$condition = $this->buildTableNameCondition($table);
|
||||||
|
if (isset($column)) {
|
||||||
|
$condition->condition('column_name', $column);
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
// Don't use {} around information_schema.columns table.
|
||||||
|
return $this->connection->query("SELECT column_comment FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
|
||||||
|
}
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
// Don't use {} around information_schema.tables table.
|
||||||
|
$comment = $this->connection->query("SELECT table_comment FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
|
||||||
|
// Work-around for MySQL 5.0 bug http://bugs.mysql.com/bug.php?id=11379
|
||||||
|
return preg_replace('/; InnoDB free:.*$/', '', $comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tableExists($table) {
|
||||||
|
// The information_schema table is very slow to query under MySQL 5.0.
|
||||||
|
// Instead, we try to select from the table in question. If it fails,
|
||||||
|
// the most likely reason is that it does not exist. That is dramatically
|
||||||
|
// faster than using information_schema.
|
||||||
|
// @link http://bugs.mysql.com/bug.php?id=19588
|
||||||
|
// @todo: This override should be removed once we require a version of MySQL
|
||||||
|
// that has that bug fixed.
|
||||||
|
try {
|
||||||
|
$this->connection->queryRange("SELECT 1 FROM {" . $table . "}", 0, 1);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldExists($table, $column) {
|
||||||
|
// The information_schema table is very slow to query under MySQL 5.0.
|
||||||
|
// Instead, we try to select from the table and field in question. If it
|
||||||
|
// fails, the most likely reason is that it does not exist. That is
|
||||||
|
// dramatically faster than using information_schema.
|
||||||
|
// @link http://bugs.mysql.com/bug.php?id=19588
|
||||||
|
// @todo: This override should be removed once we require a version of MySQL
|
||||||
|
// that has that bug fixed.
|
||||||
|
try {
|
||||||
|
$this->connection->queryRange("SELECT $column FROM {" . $table . "}", 0, 1);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup schemaapi".
|
||||||
|
*/
|
231
includes/database/pgsql/database.inc
Normal file
231
includes/database/pgsql/database.inc
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database interface code for PostgreSQL database servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name by which to obtain a lock for retrieving the next insert id.
|
||||||
|
*/
|
||||||
|
define('POSTGRESQL_NEXTID_LOCK', 1000);
|
||||||
|
|
||||||
|
class DatabaseConnection_pgsql extends DatabaseConnection {
|
||||||
|
|
||||||
|
public function __construct(array $connection_options = array()) {
|
||||||
|
// This driver defaults to transaction support, except if explicitly passed FALSE.
|
||||||
|
$this->transactionSupport = !isset($connection_options['transactions']) || ($connection_options['transactions'] !== FALSE);
|
||||||
|
|
||||||
|
// Transactional DDL is always available in PostgreSQL,
|
||||||
|
// but we'll only enable it if standard transactions are.
|
||||||
|
$this->transactionalDDLSupport = $this->transactionSupport;
|
||||||
|
|
||||||
|
// Default to TCP connection on port 5432.
|
||||||
|
if (empty($connection_options['port'])) {
|
||||||
|
$connection_options['port'] = 5432;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL in trust mode doesn't require a password to be supplied.
|
||||||
|
if (empty($connection_options['password'])) {
|
||||||
|
$connection_options['password'] = NULL;
|
||||||
|
}
|
||||||
|
// If the password contains a backslash it is treated as an escape character
|
||||||
|
// http://bugs.php.net/bug.php?id=53217
|
||||||
|
// so backslashes in the password need to be doubled up.
|
||||||
|
// The bug was reported against pdo_pgsql 1.0.2, backslashes in passwords
|
||||||
|
// will break on this doubling up when the bug is fixed, so check the version
|
||||||
|
//elseif (phpversion('pdo_pgsql') < 'version_this_was_fixed_in') {
|
||||||
|
else {
|
||||||
|
$connection_options['password'] = str_replace('\\', '\\\\', $connection_options['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connectionOptions = $connection_options;
|
||||||
|
|
||||||
|
$dsn = 'pgsql:host=' . $connection_options['host'] . ' dbname=' . $connection_options['database'] . ' port=' . $connection_options['port'];
|
||||||
|
|
||||||
|
// Allow PDO options to be overridden.
|
||||||
|
$connection_options += array(
|
||||||
|
'pdo' => array(),
|
||||||
|
);
|
||||||
|
$connection_options['pdo'] += array(
|
||||||
|
// Prepared statements are most effective for performance when queries
|
||||||
|
// are recycled (used several times). However, if they are not re-used,
|
||||||
|
// prepared statements become inefficient. Since most of Drupal's
|
||||||
|
// prepared queries are not re-used, it should be faster to emulate
|
||||||
|
// the preparation than to actually ready statements for re-use. If in
|
||||||
|
// doubt, reset to FALSE and measure performance.
|
||||||
|
PDO::ATTR_EMULATE_PREPARES => TRUE,
|
||||||
|
// Convert numeric values to strings when fetching.
|
||||||
|
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
|
||||||
|
);
|
||||||
|
parent::__construct($dsn, $connection_options['username'], $connection_options['password'], $connection_options['pdo']);
|
||||||
|
|
||||||
|
// Force PostgreSQL to use the UTF-8 character set by default.
|
||||||
|
$this->exec("SET NAMES 'UTF8'");
|
||||||
|
|
||||||
|
// Execute PostgreSQL init_commands.
|
||||||
|
if (isset($connection_options['init_commands'])) {
|
||||||
|
$this->exec(implode('; ', $connection_options['init_commands']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareQuery($query) {
|
||||||
|
// mapConditionOperator converts LIKE operations to ILIKE for consistency
|
||||||
|
// with MySQL. However, Postgres does not support ILIKE on bytea (blobs)
|
||||||
|
// fields.
|
||||||
|
// To make the ILIKE operator work, we type-cast bytea fields into text.
|
||||||
|
// @todo This workaround only affects bytea fields, but the involved field
|
||||||
|
// types involved in the query are unknown, so there is no way to
|
||||||
|
// conditionally execute this for affected queries only.
|
||||||
|
return parent::prepareQuery(preg_replace('/ ([^ ]+) +(I*LIKE|NOT +I*LIKE) /i', ' ${1}::text ${2} ', $query));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function query($query, array $args = array(), $options = array()) {
|
||||||
|
|
||||||
|
$options += $this->defaultOptions();
|
||||||
|
|
||||||
|
// The PDO PostgreSQL driver has a bug which
|
||||||
|
// doesn't type cast booleans correctly when
|
||||||
|
// parameters are bound using associative
|
||||||
|
// arrays.
|
||||||
|
// See http://bugs.php.net/bug.php?id=48383
|
||||||
|
foreach ($args as &$value) {
|
||||||
|
if (is_bool($value)) {
|
||||||
|
$value = (int) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($query instanceof DatabaseStatementInterface) {
|
||||||
|
$stmt = $query;
|
||||||
|
$stmt->execute(NULL, $options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->expandArguments($query, $args);
|
||||||
|
$stmt = $this->prepareQuery($query);
|
||||||
|
$stmt->execute($args, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($options['return']) {
|
||||||
|
case Database::RETURN_STATEMENT:
|
||||||
|
return $stmt;
|
||||||
|
case Database::RETURN_AFFECTED:
|
||||||
|
return $stmt->rowCount();
|
||||||
|
case Database::RETURN_INSERT_ID:
|
||||||
|
return $this->lastInsertId($options['sequence_name']);
|
||||||
|
case Database::RETURN_NULL:
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new PDOException('Invalid return directive: ' . $options['return']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
if ($options['throw_exception']) {
|
||||||
|
// Add additional debug information.
|
||||||
|
if ($query instanceof DatabaseStatementInterface) {
|
||||||
|
$e->query_string = $stmt->getQueryString();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$e->query_string = $query;
|
||||||
|
}
|
||||||
|
$e->args = $args;
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
|
||||||
|
return $this->query($query . ' LIMIT ' . (int) $count . ' OFFSET ' . (int) $from, $args, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryTemporary($query, array $args = array(), array $options = array()) {
|
||||||
|
$tablename = $this->generateTemporaryTableName();
|
||||||
|
$this->query('CREATE TEMPORARY TABLE {' . $tablename . '} AS ' . $query, $args, $options);
|
||||||
|
return $tablename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function driver() {
|
||||||
|
return 'pgsql';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseType() {
|
||||||
|
return 'pgsql';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapConditionOperator($operator) {
|
||||||
|
static $specials;
|
||||||
|
|
||||||
|
// Function calls not allowed in static declarations, thus this method.
|
||||||
|
if (!isset($specials)) {
|
||||||
|
$specials = array(
|
||||||
|
// In PostgreSQL, 'LIKE' is case-sensitive. For case-insensitive LIKE
|
||||||
|
// statements, we need to use ILIKE instead.
|
||||||
|
'LIKE' => array('operator' => 'ILIKE'),
|
||||||
|
'NOT LIKE' => array('operator' => 'NOT ILIKE'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($specials[$operator]) ? $specials[$operator] : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the next id in a sequence.
|
||||||
|
*
|
||||||
|
* PostgreSQL has built in sequences. We'll use these instead of inserting
|
||||||
|
* and updating a sequences table.
|
||||||
|
*/
|
||||||
|
public function nextId($existing = 0) {
|
||||||
|
|
||||||
|
// Retrieve the name of the sequence. This information cannot be cached
|
||||||
|
// because the prefix may change, for example, like it does in simpletests.
|
||||||
|
$sequence_name = $this->makeSequenceName('sequences', 'value');
|
||||||
|
|
||||||
|
// When PostgreSQL gets a value too small then it will lock the table,
|
||||||
|
// retry the INSERT and if it's still too small then alter the sequence.
|
||||||
|
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
|
||||||
|
if ($id > $existing) {
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL advisory locks are simply locks to be used by an
|
||||||
|
// application such as Drupal. This will prevent other Drupal processes
|
||||||
|
// from altering the sequence while we are.
|
||||||
|
$this->query("SELECT pg_advisory_lock(" . POSTGRESQL_NEXTID_LOCK . ")");
|
||||||
|
|
||||||
|
// While waiting to obtain the lock, the sequence may have been altered
|
||||||
|
// so lets try again to obtain an adequate value.
|
||||||
|
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
|
||||||
|
if ($id > $existing) {
|
||||||
|
$this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the sequence to a higher value than the existing id.
|
||||||
|
$this->query("ALTER SEQUENCE " . $sequence_name . " RESTART WITH " . ($existing + 1));
|
||||||
|
|
||||||
|
// Retrieve the next id. We know this will be as high as we want it.
|
||||||
|
$id = $this->query("SELECT nextval('" . $sequence_name . "')")->fetchField();
|
||||||
|
|
||||||
|
$this->query("SELECT pg_advisory_unlock(" . POSTGRESQL_NEXTID_LOCK . ")");
|
||||||
|
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsActive() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsSupported() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
197
includes/database/pgsql/install.inc
Normal file
197
includes/database/pgsql/install.inc
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Install functions for PostgreSQL embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// PostgreSQL specific install functions
|
||||||
|
|
||||||
|
class DatabaseTasks_pgsql extends DatabaseTasks {
|
||||||
|
protected $pdoDriver = 'pgsql';
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
$this->tasks[] = array(
|
||||||
|
'function' => 'checkEncoding',
|
||||||
|
'arguments' => array(),
|
||||||
|
);
|
||||||
|
$this->tasks[] = array(
|
||||||
|
'function' => 'checkPHPVersion',
|
||||||
|
'arguments' => array(),
|
||||||
|
);
|
||||||
|
$this->tasks[] = array(
|
||||||
|
'function' => 'checkBinaryOutput',
|
||||||
|
'arguments' => array(),
|
||||||
|
);
|
||||||
|
$this->tasks[] = array(
|
||||||
|
'function' => 'initializeDatabase',
|
||||||
|
'arguments' => array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function name() {
|
||||||
|
return st('PostgreSQL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function minimumVersion() {
|
||||||
|
return '8.3';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check encoding is UTF8.
|
||||||
|
*/
|
||||||
|
protected function checkEncoding() {
|
||||||
|
try {
|
||||||
|
if (db_query('SHOW server_encoding')->fetchField() == 'UTF8') {
|
||||||
|
$this->pass(st('Database is encoded in UTF-8'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$replacements = array(
|
||||||
|
'%encoding' => 'UTF8',
|
||||||
|
'%driver' => $this->name(),
|
||||||
|
'!link' => '<a href="INSTALL.pgsql.txt">INSTALL.pgsql.txt</a>'
|
||||||
|
);
|
||||||
|
$text = 'The %driver database must use %encoding encoding to work with Drupal.';
|
||||||
|
$text .= 'Recreate the database with %encoding encoding. See !link for more details.';
|
||||||
|
$this->fail(st($text, $replacements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
$this->fail(st('Drupal could not determine the encoding of the database was set to UTF-8'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check PHP version.
|
||||||
|
*
|
||||||
|
* There are two bugs in PDO_pgsql affecting Drupal:
|
||||||
|
*
|
||||||
|
* - in versions < 5.2.7, PDO_pgsql refuses to insert an empty string into
|
||||||
|
* a NOT NULL BLOB column. See: http://bugs.php.net/bug.php?id=46249
|
||||||
|
* - in versions < 5.2.11 and < 5.3.1 that prevents inserting integer values
|
||||||
|
* into numeric columns that exceed the PHP_INT_MAX value.
|
||||||
|
* See: http://bugs.php.net/bug.php?id=48924
|
||||||
|
*/
|
||||||
|
function checkPHPVersion() {
|
||||||
|
if (!version_compare(PHP_VERSION, '5.2.11', '>=') || (version_compare(PHP_VERSION, '5.3.0', '>=') && !version_compare(PHP_VERSION, '5.3.1', '>='))) {
|
||||||
|
$this->fail(st('The version of PHP you are using has known issues with PostgreSQL. You need to upgrade PHP to 5.2.11, 5.3.1 or greater.'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check Binary Output.
|
||||||
|
*
|
||||||
|
* Unserializing does not work on Postgresql 9 when bytea_output is 'hex'.
|
||||||
|
*/
|
||||||
|
function checkBinaryOutput() {
|
||||||
|
// PostgreSQL < 9 doesn't support bytea_output, so verify we are running
|
||||||
|
// at least PostgreSQL 9.
|
||||||
|
$database_connection = Database::getConnection();
|
||||||
|
if (version_compare($database_connection->version(), '9') >= 0) {
|
||||||
|
if (!$this->checkBinaryOutputSuccess()) {
|
||||||
|
// First try to alter the database. If it fails, raise an error telling
|
||||||
|
// the user to do it themselves.
|
||||||
|
$connection_options = $database_connection->getConnectionOptions();
|
||||||
|
// It is safe to include the database name directly here, because this
|
||||||
|
// code is only called when a connection to the database is already
|
||||||
|
// established, thus the database name is guaranteed to be a correct
|
||||||
|
// value.
|
||||||
|
$query = "ALTER DATABASE \"" . $connection_options['database'] . "\" SET bytea_output = 'escape';";
|
||||||
|
try {
|
||||||
|
db_query($query);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// Ignore possible errors when the user doesn't have the necessary
|
||||||
|
// privileges to ALTER the database.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the database connection so that the configuration parameter
|
||||||
|
// is applied to the current connection.
|
||||||
|
db_close();
|
||||||
|
|
||||||
|
// Recheck, if it fails, finally just rely on the end user to do the
|
||||||
|
// right thing.
|
||||||
|
if (!$this->checkBinaryOutputSuccess()) {
|
||||||
|
$replacements = array(
|
||||||
|
'%setting' => 'bytea_output',
|
||||||
|
'%current_value' => 'hex',
|
||||||
|
'%needed_value' => 'escape',
|
||||||
|
'!query' => "<code>" . $query . "</code>",
|
||||||
|
);
|
||||||
|
$this->fail(st("The %setting setting is currently set to '%current_value', but needs to be '%needed_value'. Change this by running the following query: !query", $replacements));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a binary data roundtrip returns the original string.
|
||||||
|
*/
|
||||||
|
protected function checkBinaryOutputSuccess() {
|
||||||
|
$bytea_output = db_query("SELECT 'encoding'::bytea AS output")->fetchField();
|
||||||
|
return ($bytea_output == 'encoding');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make PostgreSQL Drupal friendly.
|
||||||
|
*/
|
||||||
|
function initializeDatabase() {
|
||||||
|
// We create some functions using global names instead of prefixing them
|
||||||
|
// like we do with table names. This is so that we don't double up if more
|
||||||
|
// than one instance of Drupal is running on a single database. We therefore
|
||||||
|
// avoid trying to create them again in that case.
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create functions.
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric) RETURNS numeric AS
|
||||||
|
\'SELECT CASE WHEN (($1 > $2) OR ($2 IS NULL)) THEN $1 ELSE $2 END;\'
|
||||||
|
LANGUAGE \'sql\''
|
||||||
|
);
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "greatest"(numeric, numeric, numeric) RETURNS numeric AS
|
||||||
|
\'SELECT greatest($1, greatest($2, $3));\'
|
||||||
|
LANGUAGE \'sql\''
|
||||||
|
);
|
||||||
|
// Don't use {} around pg_proc table.
|
||||||
|
if (!db_query("SELECT COUNT(*) FROM pg_proc WHERE proname = 'rand'")->fetchField()) {
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "rand"() RETURNS float AS
|
||||||
|
\'SELECT random();\'
|
||||||
|
LANGUAGE \'sql\''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "substring_index"(text, text, integer) RETURNS text AS
|
||||||
|
\'SELECT array_to_string((string_to_array($1, $2)) [1:$3], $2);\'
|
||||||
|
LANGUAGE \'sql\''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Using || to concatenate in Drupal is not recommended because there are
|
||||||
|
// database drivers for Drupal that do not support the syntax, however
|
||||||
|
// they do support CONCAT(item1, item2) which we can replicate in
|
||||||
|
// PostgreSQL. PostgreSQL requires the function to be defined for each
|
||||||
|
// different argument variation the function can handle.
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, anynonarray) RETURNS text AS
|
||||||
|
\'SELECT CAST($1 AS text) || CAST($2 AS text);\'
|
||||||
|
LANGUAGE \'sql\'
|
||||||
|
');
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "concat"(text, anynonarray) RETURNS text AS
|
||||||
|
\'SELECT $1 || CAST($2 AS text);\'
|
||||||
|
LANGUAGE \'sql\'
|
||||||
|
');
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "concat"(anynonarray, text) RETURNS text AS
|
||||||
|
\'SELECT CAST($1 AS text) || $2;\'
|
||||||
|
LANGUAGE \'sql\'
|
||||||
|
');
|
||||||
|
db_query('CREATE OR REPLACE FUNCTION "concat"(text, text) RETURNS text AS
|
||||||
|
\'SELECT $1 || $2;\'
|
||||||
|
LANGUAGE \'sql\'
|
||||||
|
');
|
||||||
|
|
||||||
|
$this->pass(st('PostgreSQL has initialized itself.'));
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
$this->fail(st('Drupal could not be correctly setup with the existing database. Revise any errors.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
210
includes/database/pgsql/query.inc
Normal file
210
includes/database/pgsql/query.inc
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ingroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Query code for PostgreSQL embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
class InsertQuery_pgsql extends InsertQuery {
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
if (!$this->preExecute()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $this->connection->prepareQuery((string) $this);
|
||||||
|
|
||||||
|
// Fetch the list of blobs and sequences used on that table.
|
||||||
|
$table_information = $this->connection->schema()->queryTableInformation($this->table);
|
||||||
|
|
||||||
|
$max_placeholder = 0;
|
||||||
|
$blobs = array();
|
||||||
|
$blob_count = 0;
|
||||||
|
foreach ($this->insertValues as $insert_values) {
|
||||||
|
foreach ($this->insertFields as $idx => $field) {
|
||||||
|
if (isset($table_information->blob_fields[$field])) {
|
||||||
|
$blobs[$blob_count] = fopen('php://memory', 'a');
|
||||||
|
fwrite($blobs[$blob_count], $insert_values[$idx]);
|
||||||
|
rewind($blobs[$blob_count]);
|
||||||
|
|
||||||
|
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $blobs[$blob_count], PDO::PARAM_LOB);
|
||||||
|
|
||||||
|
// Pre-increment is faster in PHP than increment.
|
||||||
|
++$blob_count;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stmt->bindParam(':db_insert_placeholder_' . $max_placeholder++, $insert_values[$idx]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if values for a serial field has been passed.
|
||||||
|
if (!empty($table_information->serial_fields)) {
|
||||||
|
foreach ($table_information->serial_fields as $index => $serial_field) {
|
||||||
|
$serial_key = array_search($serial_field, $this->insertFields);
|
||||||
|
if ($serial_key !== FALSE) {
|
||||||
|
$serial_value = $insert_values[$serial_key];
|
||||||
|
|
||||||
|
// Force $last_insert_id to the specified value. This is only done
|
||||||
|
// if $index is 0.
|
||||||
|
if ($index == 0) {
|
||||||
|
$last_insert_id = $serial_value;
|
||||||
|
}
|
||||||
|
// Set the sequence to the bigger value of either the passed
|
||||||
|
// value or the max value of the column. It can happen that another
|
||||||
|
// thread calls nextval() which could lead to a serial number being
|
||||||
|
// used twice. However, trying to insert a value into a serial
|
||||||
|
// column should only be done in very rare cases and is not thread
|
||||||
|
// safe by definition.
|
||||||
|
$this->connection->query("SELECT setval('" . $table_information->sequences[$index] . "', GREATEST(MAX(" . $serial_field . "), :serial_value)) FROM {" . $this->table . "}", array(':serial_value' => (int)$serial_value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($this->fromQuery)) {
|
||||||
|
// bindParam stores only a reference to the variable that is followed when
|
||||||
|
// the statement is executed. We pass $arguments[$key] instead of $value
|
||||||
|
// because the second argument to bindParam is passed by reference and
|
||||||
|
// the foreach statement assigns the element to the existing reference.
|
||||||
|
$arguments = $this->fromQuery->getArguments();
|
||||||
|
foreach ($arguments as $key => $value) {
|
||||||
|
$stmt->bindParam($key, $arguments[$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgreSQL requires the table name to be specified explicitly
|
||||||
|
// when requesting the last insert ID, so we pass that in via
|
||||||
|
// the options array.
|
||||||
|
$options = $this->queryOptions;
|
||||||
|
|
||||||
|
if (!empty($table_information->sequences)) {
|
||||||
|
$options['sequence_name'] = $table_information->sequences[0];
|
||||||
|
}
|
||||||
|
// If there are no sequences then we can't get a last insert id.
|
||||||
|
elseif ($options['return'] == Database::RETURN_INSERT_ID) {
|
||||||
|
$options['return'] = Database::RETURN_NULL;
|
||||||
|
}
|
||||||
|
// Only use the returned last_insert_id if it is not already set.
|
||||||
|
if (!empty($last_insert_id)) {
|
||||||
|
$this->connection->query($stmt, array(), $options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$last_insert_id = $this->connection->query($stmt, array(), $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-initialize the values array so that we can re-use this query.
|
||||||
|
$this->insertValues = array();
|
||||||
|
|
||||||
|
return $last_insert_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
// Create a sanitized comment string to prepend to the query.
|
||||||
|
$comments = $this->connection->makeComment($this->comments);
|
||||||
|
|
||||||
|
// Default fields are always placed first for consistency.
|
||||||
|
$insert_fields = array_merge($this->defaultFields, $this->insertFields);
|
||||||
|
|
||||||
|
// If we're selecting from a SelectQuery, finish building the query and
|
||||||
|
// pass it back, as any remaining options are irrelevant.
|
||||||
|
if (!empty($this->fromQuery)) {
|
||||||
|
$insert_fields_string = $insert_fields ? ' (' . implode(', ', $insert_fields) . ') ' : ' ';
|
||||||
|
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $insert_fields) . ') VALUES ';
|
||||||
|
|
||||||
|
$max_placeholder = 0;
|
||||||
|
$values = array();
|
||||||
|
if (count($this->insertValues)) {
|
||||||
|
foreach ($this->insertValues as $insert_values) {
|
||||||
|
$placeholders = array();
|
||||||
|
|
||||||
|
// Default fields aren't really placeholders, but this is the most convenient
|
||||||
|
// way to handle them.
|
||||||
|
$placeholders = array_pad($placeholders, count($this->defaultFields), 'default');
|
||||||
|
|
||||||
|
$new_placeholder = $max_placeholder + count($insert_values);
|
||||||
|
for ($i = $max_placeholder; $i < $new_placeholder; ++$i) {
|
||||||
|
$placeholders[] = ':db_insert_placeholder_' . $i;
|
||||||
|
}
|
||||||
|
$max_placeholder = $new_placeholder;
|
||||||
|
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If there are no values, then this is a default-only query. We still need to handle that.
|
||||||
|
$placeholders = array_fill(0, count($this->defaultFields), 'default');
|
||||||
|
$values[] = '(' . implode(', ', $placeholders) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
$query .= implode(', ', $values);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateQuery_pgsql extends UpdateQuery {
|
||||||
|
public function execute() {
|
||||||
|
$max_placeholder = 0;
|
||||||
|
$blobs = array();
|
||||||
|
$blob_count = 0;
|
||||||
|
|
||||||
|
// Because we filter $fields the same way here and in __toString(), the
|
||||||
|
// placeholders will all match up properly.
|
||||||
|
$stmt = $this->connection->prepareQuery((string) $this);
|
||||||
|
|
||||||
|
// Fetch the list of blobs and sequences used on that table.
|
||||||
|
$table_information = $this->connection->schema()->queryTableInformation($this->table);
|
||||||
|
|
||||||
|
// Expressions take priority over literal fields, so we process those first
|
||||||
|
// and remove any literal fields that conflict.
|
||||||
|
$fields = $this->fields;
|
||||||
|
$expression_fields = array();
|
||||||
|
foreach ($this->expressionFields as $field => $data) {
|
||||||
|
if (!empty($data['arguments'])) {
|
||||||
|
foreach ($data['arguments'] as $placeholder => $argument) {
|
||||||
|
// We assume that an expression will never happen on a BLOB field,
|
||||||
|
// which is a fairly safe assumption to make since in most cases
|
||||||
|
// it would be an invalid query anyway.
|
||||||
|
$stmt->bindParam($placeholder, $data['arguments'][$placeholder]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unset($fields[$field]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($fields as $field => $value) {
|
||||||
|
$placeholder = ':db_update_placeholder_' . ($max_placeholder++);
|
||||||
|
|
||||||
|
if (isset($table_information->blob_fields[$field])) {
|
||||||
|
$blobs[$blob_count] = fopen('php://memory', 'a');
|
||||||
|
fwrite($blobs[$blob_count], $value);
|
||||||
|
rewind($blobs[$blob_count]);
|
||||||
|
$stmt->bindParam($placeholder, $blobs[$blob_count], PDO::PARAM_LOB);
|
||||||
|
++$blob_count;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stmt->bindParam($placeholder, $fields[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($this->condition)) {
|
||||||
|
$this->condition->compile($this->connection, $this);
|
||||||
|
|
||||||
|
$arguments = $this->condition->arguments();
|
||||||
|
foreach ($arguments as $placeholder => $value) {
|
||||||
|
$stmt->bindParam($placeholder, $arguments[$placeholder]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = $this->queryOptions;
|
||||||
|
$options['already_prepared'] = TRUE;
|
||||||
|
$this->connection->query($stmt, $options);
|
||||||
|
|
||||||
|
return $stmt->rowCount();
|
||||||
|
}
|
||||||
|
}
|
617
includes/database/pgsql/schema.inc
Normal file
617
includes/database/pgsql/schema.inc
Normal file
|
@ -0,0 +1,617 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database schema code for PostgreSQL database servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ingroup schemaapi
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DatabaseSchema_pgsql extends DatabaseSchema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of information about blob columns and sequences of tables.
|
||||||
|
*
|
||||||
|
* This is collected by DatabaseConnection_pgsql->queryTableInformation(),
|
||||||
|
* by introspecting the database.
|
||||||
|
*
|
||||||
|
* @see DatabaseConnection_pgsql->queryTableInformation()
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $tableInformation = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the list of blobs and sequences used on a table.
|
||||||
|
*
|
||||||
|
* We introspect the database to collect the information required by insert
|
||||||
|
* and update queries.
|
||||||
|
*
|
||||||
|
* @param $table_name
|
||||||
|
* The non-prefixed name of the table.
|
||||||
|
* @return
|
||||||
|
* An object with two member variables:
|
||||||
|
* - 'blob_fields' that lists all the blob fields in the table.
|
||||||
|
* - 'sequences' that lists the sequences used in that table.
|
||||||
|
*/
|
||||||
|
public function queryTableInformation($table) {
|
||||||
|
// Generate a key to reference this table's information on.
|
||||||
|
$key = $this->connection->prefixTables('{' . $table . '}');
|
||||||
|
if (!strpos($key, '.')) {
|
||||||
|
$key = 'public.' . $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->tableInformation[$key])) {
|
||||||
|
// Split the key into schema and table for querying.
|
||||||
|
list($schema, $table_name) = explode('.', $key);
|
||||||
|
$table_information = (object) array(
|
||||||
|
'blob_fields' => array(),
|
||||||
|
'sequences' => array(),
|
||||||
|
);
|
||||||
|
// Don't use {} around information_schema.columns table.
|
||||||
|
$result = $this->connection->query("SELECT column_name, data_type, column_default FROM information_schema.columns WHERE table_schema = :schema AND table_name = :table AND (data_type = 'bytea' OR (numeric_precision IS NOT NULL AND column_default LIKE :default))", array(
|
||||||
|
':schema' => $schema,
|
||||||
|
':table' => $table_name,
|
||||||
|
':default' => '%nextval%',
|
||||||
|
));
|
||||||
|
foreach ($result as $column) {
|
||||||
|
if ($column->data_type == 'bytea') {
|
||||||
|
$table_information->blob_fields[$column->column_name] = TRUE;
|
||||||
|
}
|
||||||
|
elseif (preg_match("/nextval\('([^']+)'/", $column->column_default, $matches)) {
|
||||||
|
// We must know of any sequences in the table structure to help us
|
||||||
|
// return the last insert id. If there is more than 1 sequences the
|
||||||
|
// first one (index 0 of the sequences array) will be used.
|
||||||
|
$table_information->sequences[] = $matches[1];
|
||||||
|
$table_information->serial_fields[] = $column->column_name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->tableInformation[$key] = $table_information;
|
||||||
|
}
|
||||||
|
return $this->tableInformation[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the list of CHECK constraints used on a field.
|
||||||
|
*
|
||||||
|
* We introspect the database to collect the information required by field
|
||||||
|
* alteration.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The non-prefixed name of the table.
|
||||||
|
* @param $field
|
||||||
|
* The name of the field.
|
||||||
|
* @return
|
||||||
|
* An array of all the checks for the field.
|
||||||
|
*/
|
||||||
|
public function queryFieldInformation($table, $field) {
|
||||||
|
$prefixInfo = $this->getPrefixInfo($table, TRUE);
|
||||||
|
|
||||||
|
// Split the key into schema and table for querying.
|
||||||
|
$schema = $prefixInfo['schema'];
|
||||||
|
$table_name = $prefixInfo['table'];
|
||||||
|
|
||||||
|
$field_information = (object) array(
|
||||||
|
'checks' => array(),
|
||||||
|
);
|
||||||
|
$checks = $this->connection->query("SELECT conname FROM pg_class cl INNER JOIN pg_constraint co ON co.conrelid = cl.oid INNER JOIN pg_attribute attr ON attr.attrelid = cl.oid AND attr.attnum = ANY (co.conkey) INNER JOIN pg_namespace ns ON cl.relnamespace = ns.oid WHERE co.contype = 'c' AND ns.nspname = :schema AND cl.relname = :table AND attr.attname = :column", array(
|
||||||
|
':schema' => $schema,
|
||||||
|
':table' => $table_name,
|
||||||
|
':column' => $field,
|
||||||
|
));
|
||||||
|
$field_information = $checks->fetchCol();
|
||||||
|
|
||||||
|
return $field_information;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SQL to create a new table from a Drupal schema definition.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the table to create.
|
||||||
|
* @param $table
|
||||||
|
* A Schema API table definition array.
|
||||||
|
* @return
|
||||||
|
* An array of SQL statements to create the table.
|
||||||
|
*/
|
||||||
|
protected function createTableSql($name, $table) {
|
||||||
|
$sql_fields = array();
|
||||||
|
foreach ($table['fields'] as $field_name => $field) {
|
||||||
|
$sql_fields[] = $this->createFieldSql($field_name, $this->processField($field));
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql_keys = array();
|
||||||
|
if (isset($table['primary key']) && is_array($table['primary key'])) {
|
||||||
|
$sql_keys[] = 'PRIMARY KEY (' . implode(', ', $table['primary key']) . ')';
|
||||||
|
}
|
||||||
|
if (isset($table['unique keys']) && is_array($table['unique keys'])) {
|
||||||
|
foreach ($table['unique keys'] as $key_name => $key) {
|
||||||
|
$sql_keys[] = 'CONSTRAINT ' . $this->prefixNonTable($name, $key_name, 'key') . ' UNIQUE (' . implode(', ', $key) . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE {" . $name . "} (\n\t";
|
||||||
|
$sql .= implode(",\n\t", $sql_fields);
|
||||||
|
if (count($sql_keys) > 0) {
|
||||||
|
$sql .= ",\n\t";
|
||||||
|
}
|
||||||
|
$sql .= implode(",\n\t", $sql_keys);
|
||||||
|
$sql .= "\n)";
|
||||||
|
$statements[] = $sql;
|
||||||
|
|
||||||
|
if (isset($table['indexes']) && is_array($table['indexes'])) {
|
||||||
|
foreach ($table['indexes'] as $key_name => $key) {
|
||||||
|
$statements[] = $this->_createIndexSql($name, $key_name, $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add table comment.
|
||||||
|
if (!empty($table['description'])) {
|
||||||
|
$statements[] = 'COMMENT ON TABLE {' . $name . '} IS ' . $this->prepareComment($table['description']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add column comments.
|
||||||
|
foreach ($table['fields'] as $field_name => $field) {
|
||||||
|
if (!empty($field['description'])) {
|
||||||
|
$statements[] = 'COMMENT ON COLUMN {' . $name . '}.' . $field_name . ' IS ' . $this->prepareComment($field['description']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SQL string for a field to be used in table creation or
|
||||||
|
* alteration.
|
||||||
|
*
|
||||||
|
* Before passing a field out of a schema definition into this
|
||||||
|
* function it has to be processed by _db_process_field().
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* Name of the field.
|
||||||
|
* @param $spec
|
||||||
|
* The field specification, as per the schema data structure format.
|
||||||
|
*/
|
||||||
|
protected function createFieldSql($name, $spec) {
|
||||||
|
$sql = $name . ' ' . $spec['pgsql_type'];
|
||||||
|
|
||||||
|
if (isset($spec['type']) && $spec['type'] == 'serial') {
|
||||||
|
unset($spec['not null']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
|
||||||
|
$sql .= '(' . $spec['length'] . ')';
|
||||||
|
}
|
||||||
|
elseif (isset($spec['precision']) && isset($spec['scale'])) {
|
||||||
|
$sql .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($spec['unsigned'])) {
|
||||||
|
$sql .= " CHECK ($name >= 0)";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($spec['not null'])) {
|
||||||
|
if ($spec['not null']) {
|
||||||
|
$sql .= ' NOT NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sql .= ' NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($spec['default'])) {
|
||||||
|
$default = is_string($spec['default']) ? "'" . $spec['default'] . "'" : $spec['default'];
|
||||||
|
$sql .= " default $default";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set database-engine specific properties for a field.
|
||||||
|
*
|
||||||
|
* @param $field
|
||||||
|
* A field description array, as specified in the schema documentation.
|
||||||
|
*/
|
||||||
|
protected function processField($field) {
|
||||||
|
if (!isset($field['size'])) {
|
||||||
|
$field['size'] = 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the correct database-engine specific datatype.
|
||||||
|
// In case one is already provided, force it to lowercase.
|
||||||
|
if (isset($field['pgsql_type'])) {
|
||||||
|
$field['pgsql_type'] = drupal_strtolower($field['pgsql_type']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$map = $this->getFieldTypeMap();
|
||||||
|
$field['pgsql_type'] = $map[$field['type'] . ':' . $field['size']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($field['unsigned'])) {
|
||||||
|
// Unsigned datatypes are not supported in PostgreSQL 8.3. In MySQL,
|
||||||
|
// they are used to ensure a positive number is inserted and it also
|
||||||
|
// doubles the maximum integer size that can be stored in a field.
|
||||||
|
// The PostgreSQL schema in Drupal creates a check constraint
|
||||||
|
// to ensure that a value inserted is >= 0. To provide the extra
|
||||||
|
// integer capacity, here, we bump up the column field size.
|
||||||
|
if (!isset($map)) {
|
||||||
|
$map = $this->getFieldTypeMap();
|
||||||
|
}
|
||||||
|
switch ($field['pgsql_type']) {
|
||||||
|
case 'smallint':
|
||||||
|
$field['pgsql_type'] = $map['int:medium'];
|
||||||
|
break;
|
||||||
|
case 'int' :
|
||||||
|
$field['pgsql_type'] = $map['int:big'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||||
|
unset($field['not null']);
|
||||||
|
}
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This maps a generic data type in combination with its data size
|
||||||
|
* to the engine-specific data type.
|
||||||
|
*/
|
||||||
|
function getFieldTypeMap() {
|
||||||
|
// Put :normal last so it gets preserved by array_flip. This makes
|
||||||
|
// it much easier for modules (such as schema.module) to map
|
||||||
|
// database types back into schema types.
|
||||||
|
// $map does not use drupal_static as its value never changes.
|
||||||
|
static $map = array(
|
||||||
|
'varchar:normal' => 'varchar',
|
||||||
|
'char:normal' => 'character',
|
||||||
|
|
||||||
|
'text:tiny' => 'text',
|
||||||
|
'text:small' => 'text',
|
||||||
|
'text:medium' => 'text',
|
||||||
|
'text:big' => 'text',
|
||||||
|
'text:normal' => 'text',
|
||||||
|
|
||||||
|
'int:tiny' => 'smallint',
|
||||||
|
'int:small' => 'smallint',
|
||||||
|
'int:medium' => 'int',
|
||||||
|
'int:big' => 'bigint',
|
||||||
|
'int:normal' => 'int',
|
||||||
|
|
||||||
|
'float:tiny' => 'real',
|
||||||
|
'float:small' => 'real',
|
||||||
|
'float:medium' => 'real',
|
||||||
|
'float:big' => 'double precision',
|
||||||
|
'float:normal' => 'real',
|
||||||
|
|
||||||
|
'numeric:normal' => 'numeric',
|
||||||
|
|
||||||
|
'blob:big' => 'bytea',
|
||||||
|
'blob:normal' => 'bytea',
|
||||||
|
|
||||||
|
'serial:tiny' => 'serial',
|
||||||
|
'serial:small' => 'serial',
|
||||||
|
'serial:medium' => 'serial',
|
||||||
|
'serial:big' => 'bigserial',
|
||||||
|
'serial:normal' => 'serial',
|
||||||
|
);
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _createKeySql($fields) {
|
||||||
|
$return = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$return[] = 'substr(' . $field[0] . ', 1, ' . $field[1] . ')';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = '"' . $field . '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renameTable($table, $new_name) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
if ($this->tableExists($new_name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the schema and tablename for the old table.
|
||||||
|
$old_full_name = $this->connection->prefixTables('{' . $table . '}');
|
||||||
|
list($old_schema, $old_table_name) = strpos($old_full_name, '.') ? explode('.', $old_full_name) : array('public', $old_full_name);
|
||||||
|
|
||||||
|
// Index names and constraint names are global in PostgreSQL, so we need to
|
||||||
|
// rename them when renaming the table.
|
||||||
|
$indexes = $this->connection->query('SELECT indexname FROM pg_indexes WHERE schemaname = :schema AND tablename = :table', array(':schema' => $old_schema, ':table' => $old_table_name));
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
if (preg_match('/^' . preg_quote($old_full_name) . '_(.*)$/', $index->indexname, $matches)) {
|
||||||
|
$index_name = $matches[1];
|
||||||
|
$this->connection->query('ALTER INDEX ' . $index->indexname . ' RENAME TO {' . $new_name . '}_' . $index_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now rename the table.
|
||||||
|
// Ensure the new table name does not include schema syntax.
|
||||||
|
$prefixInfo = $this->getPrefixInfo($new_name);
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $prefixInfo['table']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropTable($table) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('DROP TABLE {' . $table . '}');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addField($table, $field, $spec, $new_keys = array()) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
if ($this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$fixnull = FALSE;
|
||||||
|
if (!empty($spec['not null']) && !isset($spec['default'])) {
|
||||||
|
$fixnull = TRUE;
|
||||||
|
$spec['not null'] = FALSE;
|
||||||
|
}
|
||||||
|
$query = 'ALTER TABLE {' . $table . '} ADD COLUMN ';
|
||||||
|
$query .= $this->createFieldSql($field, $this->processField($spec));
|
||||||
|
$this->connection->query($query);
|
||||||
|
if (isset($spec['initial'])) {
|
||||||
|
$this->connection->update($table)
|
||||||
|
->fields(array($field => $spec['initial']))
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
if ($fixnull) {
|
||||||
|
$this->connection->query("ALTER TABLE {" . $table . "} ALTER $field SET NOT NULL");
|
||||||
|
}
|
||||||
|
if (isset($new_keys)) {
|
||||||
|
$this->_createKeys($table, $new_keys);
|
||||||
|
}
|
||||||
|
// Add column comment.
|
||||||
|
if (!empty($spec['description'])) {
|
||||||
|
$this->connection->query('COMMENT ON COLUMN {' . $table . '}.' . $field . ' IS ' . $this->prepareComment($spec['description']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropField($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP COLUMN "' . $field . '"');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetDefault($table, $field, $default) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($default)) {
|
||||||
|
$default = 'NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$default = is_string($default) ? "'$default'" : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" SET DEFAULT ' . $default);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetNoDefault($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER COLUMN "' . $field . '" DROP DEFAULT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexExists($table, $name) {
|
||||||
|
// Details http://www.postgresql.org/docs/8.3/interactive/view-pg-indexes.html
|
||||||
|
$index_name = '{' . $table . '}_' . $name . '_idx';
|
||||||
|
return (bool) $this->connection->query("SELECT 1 FROM pg_indexes WHERE indexname = '$index_name'")->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function: check if a constraint (PK, FK, UK) exists.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The name of the table.
|
||||||
|
* @param $name
|
||||||
|
* The name of the constraint (typically 'pkey' or '[constraint]_key').
|
||||||
|
*/
|
||||||
|
protected function constraintExists($table, $name) {
|
||||||
|
$constraint_name = '{' . $table . '}_' . $name;
|
||||||
|
return (bool) $this->connection->query("SELECT 1 FROM pg_constraint WHERE conname = '$constraint_name'")->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPrimaryKey($table, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
if ($this->constraintExists($table, 'pkey')) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD PRIMARY KEY (' . implode(',', $fields) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropPrimaryKey($table) {
|
||||||
|
if (!$this->constraintExists($table, 'pkey')) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT ' . $this->prefixNonTable($table, 'pkey'));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addUniqueKey($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->constraintExists($table, $name . '_key')) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '" UNIQUE (' . implode(',', $fields) . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropUniqueKey($table, $name) {
|
||||||
|
if (!$this->constraintExists($table, $name . '_key')) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $this->prefixNonTable($table, $name, 'key') . '"');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addIndex($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, $name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query($this->_createIndexSql($table, $name, $fields));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropIndex($table, $name) {
|
||||||
|
if (!$this->indexExists($table, $name)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->connection->query('DROP INDEX ' . $this->prefixNonTable($table, $name, 'idx'));
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeField($table, $field, $field_new, $spec, $new_keys = array()) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", array('@table' => $table, '@name' => $field)));
|
||||||
|
}
|
||||||
|
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$spec = $this->processField($spec);
|
||||||
|
|
||||||
|
// We need to typecast the new column to best be able to transfer the data
|
||||||
|
// Schema_pgsql::getFieldTypeMap() will return possibilities that are not
|
||||||
|
// 'cast-able' such as 'serial' - so they need to be casted int instead.
|
||||||
|
if (in_array($spec['pgsql_type'], array('serial', 'bigserial', 'numeric'))) {
|
||||||
|
$typecast = 'int';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$typecast = $spec['pgsql_type'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($spec['pgsql_type'], array('varchar', 'character', 'text')) && isset($spec['length'])) {
|
||||||
|
$typecast .= '(' . $spec['length'] . ')';
|
||||||
|
}
|
||||||
|
elseif (isset($spec['precision']) && isset($spec['scale'])) {
|
||||||
|
$typecast .= '(' . $spec['precision'] . ', ' . $spec['scale'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old check constraints.
|
||||||
|
$field_info = $this->queryFieldInformation($table, $field);
|
||||||
|
|
||||||
|
foreach ($field_info as $check) {
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} DROP CONSTRAINT "' . $check . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old default.
|
||||||
|
$this->fieldSetNoDefault($table, $field);
|
||||||
|
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" TYPE ' . $typecast . ' USING "' . $field . '"::' . $typecast);
|
||||||
|
|
||||||
|
if (isset($spec['not null'])) {
|
||||||
|
if ($spec['not null']) {
|
||||||
|
$nullaction = 'SET NOT NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$nullaction = 'DROP NOT NULL';
|
||||||
|
}
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" ' . $nullaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($spec['pgsql_type'], array('serial', 'bigserial'))) {
|
||||||
|
// Type "serial" is known to PostgreSQL, but *only* during table creation,
|
||||||
|
// not when altering. Because of that, the sequence needs to be created
|
||||||
|
// and initialized by hand.
|
||||||
|
$seq = "{" . $table . "}_" . $field_new . "_seq";
|
||||||
|
$this->connection->query("CREATE SEQUENCE " . $seq);
|
||||||
|
// Set sequence to maximal field value to not conflict with existing
|
||||||
|
// entries.
|
||||||
|
$this->connection->query("SELECT setval('" . $seq . "', MAX(\"" . $field . '")) FROM {' . $table . "}");
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ALTER "' . $field . '" SET DEFAULT nextval(\'' . $seq . '\')');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the column if necessary.
|
||||||
|
if ($field != $field_new) {
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} RENAME "' . $field . '" TO "' . $field_new . '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add unsigned check if necessary.
|
||||||
|
if (!empty($spec['unsigned'])) {
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} ADD CHECK ("' . $field_new . '" >= 0)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default if necessary.
|
||||||
|
if (isset($spec['default'])) {
|
||||||
|
$this->fieldSetDefault($table, $field_new, $spec['default']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change description if necessary.
|
||||||
|
if (!empty($spec['description'])) {
|
||||||
|
$this->connection->query('COMMENT ON COLUMN {' . $table . '}."' . $field_new . '" IS ' . $this->prepareComment($spec['description']));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($new_keys)) {
|
||||||
|
$this->_createKeys($table, $new_keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _createIndexSql($table, $name, $fields) {
|
||||||
|
$query = 'CREATE INDEX "' . $this->prefixNonTable($table, $name, 'idx') . '" ON {' . $table . '} (';
|
||||||
|
$query .= $this->_createKeySql($fields) . ')';
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function _createKeys($table, $new_keys) {
|
||||||
|
if (isset($new_keys['primary key'])) {
|
||||||
|
$this->addPrimaryKey($table, $new_keys['primary key']);
|
||||||
|
}
|
||||||
|
if (isset($new_keys['unique keys'])) {
|
||||||
|
foreach ($new_keys['unique keys'] as $name => $fields) {
|
||||||
|
$this->addUniqueKey($table, $name, $fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isset($new_keys['indexes'])) {
|
||||||
|
foreach ($new_keys['indexes'] as $name => $fields) {
|
||||||
|
$this->addIndex($table, $name, $fields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve a table or column comment.
|
||||||
|
*/
|
||||||
|
public function getComment($table, $column = NULL) {
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
// Don't use {} around pg_class, pg_attribute tables.
|
||||||
|
if (isset($column)) {
|
||||||
|
return $this->connection->query('SELECT col_description(oid, attnum) FROM pg_class, pg_attribute WHERE attrelid = oid AND relname = ? AND attname = ?', array($info['table'], $column))->fetchField();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $this->connection->query('SELECT obj_description(oid, ?) FROM pg_class WHERE relname = ?', array('pg_class', $info['table']))->fetchField();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
includes/database/pgsql/select.inc
Normal file
108
includes/database/pgsql/select.inc
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Select builder for PostgreSQL database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
class SelectQuery_pgsql extends SelectQuery {
|
||||||
|
|
||||||
|
public function orderRandom() {
|
||||||
|
$alias = $this->addExpression('RANDOM()', 'random_field');
|
||||||
|
$this->orderBy($alias);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides SelectQuery::orderBy().
|
||||||
|
*
|
||||||
|
* PostgreSQL adheres strictly to the SQL-92 standard and requires that when
|
||||||
|
* using DISTINCT or GROUP BY conditions, fields and expressions that are
|
||||||
|
* ordered on also need to be selected. This is a best effort implementation
|
||||||
|
* to handle the cases that can be automated by adding the field if it is not
|
||||||
|
* yet selected.
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* $query = db_select('node', 'n');
|
||||||
|
* $query->join('node_revision', 'nr', 'n.vid = nr.vid');
|
||||||
|
* $query
|
||||||
|
* ->distinct()
|
||||||
|
* ->fields('n')
|
||||||
|
* ->orderBy('timestamp');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* In this query, it is not possible (without relying on the schema) to know
|
||||||
|
* whether timestamp belongs to node_revisions and needs to be added or
|
||||||
|
* belongs to node and is already selected. Queries like this will need to be
|
||||||
|
* corrected in the original query by adding an explicit call to
|
||||||
|
* SelectQuery::addField() or SelectQuery::fields().
|
||||||
|
*
|
||||||
|
* Since this has a small performance impact, both by the additional
|
||||||
|
* processing in this function and in the database that needs to return the
|
||||||
|
* additional fields, this is done as an override instead of implementing it
|
||||||
|
* directly in SelectQuery::orderBy().
|
||||||
|
*/
|
||||||
|
public function orderBy($field, $direction = 'ASC') {
|
||||||
|
// Call parent function to order on this.
|
||||||
|
$return = parent::orderBy($field, $direction);
|
||||||
|
|
||||||
|
// If there is a table alias specified, split it up.
|
||||||
|
if (strpos($field, '.') !== FALSE) {
|
||||||
|
list($table, $table_field) = explode('.', $field);
|
||||||
|
}
|
||||||
|
// Figure out if the field has already been added.
|
||||||
|
foreach ($this->fields as $existing_field) {
|
||||||
|
if (!empty($table)) {
|
||||||
|
// If table alias is given, check if field and table exists.
|
||||||
|
if ($existing_field['table'] == $table && $existing_field['field'] == $table_field) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If there is no table, simply check if the field exists as a field or
|
||||||
|
// an aliased field.
|
||||||
|
if ($existing_field['alias'] == $field) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check expression aliases.
|
||||||
|
foreach ($this->expressions as $expression) {
|
||||||
|
if ($expression['alias'] == $field) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a table loads all fields, it can not be added again. It would
|
||||||
|
// result in an ambiguous alias error because that field would be loaded
|
||||||
|
// twice: Once through table_alias.* and once directly. If the field
|
||||||
|
// actually belongs to a different table, it must be added manually.
|
||||||
|
foreach ($this->tables as $table) {
|
||||||
|
if (!empty($table['all_fields'])) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If $field contains an characters which are not allowed in a field name
|
||||||
|
// it is considered an expression, these can't be handled automatically
|
||||||
|
// either.
|
||||||
|
if ($this->connection->escapeField($field) != $field) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a case that can be handled automatically, add the field.
|
||||||
|
$this->addField(NULL, $field);
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
||||||
|
|
507
includes/database/prefetch.inc
Normal file
507
includes/database/prefetch.inc
Normal file
|
@ -0,0 +1,507 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database interface code for engines that need complete control over their
|
||||||
|
* result sets. For example, SQLite will prefix some column names by the name
|
||||||
|
* of the table. We post-process the data, by renaming the column names
|
||||||
|
* using the same convention as MySQL and PostgreSQL.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of DatabaseStatementInterface that prefetches all data.
|
||||||
|
*
|
||||||
|
* This class behaves very similar to a PDOStatement but as it always fetches
|
||||||
|
* every row it is possible to manipulate those results.
|
||||||
|
*/
|
||||||
|
class DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The query string.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $queryString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Driver-specific options. Can be used by child classes.
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $driverOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to the database connection object for this statement.
|
||||||
|
*
|
||||||
|
* The name $dbh is inherited from PDOStatement.
|
||||||
|
*
|
||||||
|
* @var DatabaseConnection
|
||||||
|
*/
|
||||||
|
public $dbh;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main data store.
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $data = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current row, retrieved in PDO::FETCH_ASSOC format.
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $currentRow = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key of the current row.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $currentKey = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of column names in this result set.
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $columnNames = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of rows affected by the last query.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $rowCount = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of rows in this result set.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $resultRowCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the current fetch style (which will be used by the next fetch).
|
||||||
|
* @see PDOStatement::fetch()
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $fetchStyle = PDO::FETCH_OBJ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds supplementary current fetch options (which will be used by the next fetch).
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $fetchOptions = array(
|
||||||
|
'class' => 'stdClass',
|
||||||
|
'constructor_args' => array(),
|
||||||
|
'object' => NULL,
|
||||||
|
'column' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the default fetch style.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $defaultFetchStyle = PDO::FETCH_OBJ;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds supplementary default fetch options.
|
||||||
|
*
|
||||||
|
* @var Array
|
||||||
|
*/
|
||||||
|
protected $defaultFetchOptions = array(
|
||||||
|
'class' => 'stdClass',
|
||||||
|
'constructor_args' => array(),
|
||||||
|
'object' => NULL,
|
||||||
|
'column' => 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
public function __construct(DatabaseConnection $connection, $query, array $driver_options = array()) {
|
||||||
|
$this->dbh = $connection;
|
||||||
|
$this->queryString = $query;
|
||||||
|
$this->driverOptions = $driver_options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a prepared statement.
|
||||||
|
*
|
||||||
|
* @param $args
|
||||||
|
* An array of values with as many elements as there are bound parameters in the SQL statement being executed.
|
||||||
|
* @param $options
|
||||||
|
* An array of options for this query.
|
||||||
|
* @return
|
||||||
|
* TRUE on success, or FALSE on failure.
|
||||||
|
*/
|
||||||
|
public function execute($args = array(), $options = array()) {
|
||||||
|
if (isset($options['fetch'])) {
|
||||||
|
if (is_string($options['fetch'])) {
|
||||||
|
// Default to an object. Note: db fields will be added to the object
|
||||||
|
// before the constructor is run. If you need to assign fields after
|
||||||
|
// the constructor is run, see http://drupal.org/node/315092.
|
||||||
|
$this->setFetchMode(PDO::FETCH_CLASS, $options['fetch']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->setFetchMode($options['fetch']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$logger = $this->dbh->getLogger();
|
||||||
|
if (!empty($logger)) {
|
||||||
|
$query_start = microtime(TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the query.
|
||||||
|
$statement = $this->getStatement($this->queryString, $args);
|
||||||
|
if (!$statement) {
|
||||||
|
$this->throwPDOException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$return = $statement->execute($args);
|
||||||
|
if (!$return) {
|
||||||
|
$this->throwPDOException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch all the data from the reply, in order to release any lock
|
||||||
|
// as soon as possible.
|
||||||
|
$this->rowCount = $statement->rowCount();
|
||||||
|
$this->data = $statement->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
// Destroy the statement as soon as possible. See
|
||||||
|
// DatabaseConnection_sqlite::PDOPrepare() for explanation.
|
||||||
|
unset($statement);
|
||||||
|
|
||||||
|
$this->resultRowCount = count($this->data);
|
||||||
|
|
||||||
|
if ($this->resultRowCount) {
|
||||||
|
$this->columnNames = array_keys($this->data[0]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->columnNames = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($logger)) {
|
||||||
|
$query_end = microtime(TRUE);
|
||||||
|
$logger->log($this, $args, $query_end - $query_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the first row in $this->currentRow.
|
||||||
|
$this->next();
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw a PDO Exception based on the last PDO error.
|
||||||
|
*/
|
||||||
|
protected function throwPDOException() {
|
||||||
|
$error_info = $this->dbh->errorInfo();
|
||||||
|
// We rebuild a message formatted in the same way as PDO.
|
||||||
|
$exception = new PDOException("SQLSTATE[" . $error_info[0] . "]: General error " . $error_info[1] . ": " . $error_info[2]);
|
||||||
|
$exception->errorInfo = $error_info;
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grab a PDOStatement object from a given query and its arguments.
|
||||||
|
*
|
||||||
|
* Some drivers (including SQLite) will need to perform some preparation
|
||||||
|
* themselves to get the statement right.
|
||||||
|
*
|
||||||
|
* @param $query
|
||||||
|
* The query.
|
||||||
|
* @param array $args
|
||||||
|
* An array of arguments.
|
||||||
|
* @return PDOStatement
|
||||||
|
* A PDOStatement object.
|
||||||
|
*/
|
||||||
|
protected function getStatement($query, &$args = array()) {
|
||||||
|
return $this->dbh->prepare($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the object's SQL query string.
|
||||||
|
*/
|
||||||
|
public function getQueryString() {
|
||||||
|
return $this->queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see PDOStatement::setFetchMode()
|
||||||
|
*/
|
||||||
|
public function setFetchMode($fetchStyle, $a2 = NULL, $a3 = NULL) {
|
||||||
|
$this->defaultFetchStyle = $fetchStyle;
|
||||||
|
switch ($fetchStyle) {
|
||||||
|
case PDO::FETCH_CLASS:
|
||||||
|
$this->defaultFetchOptions['class'] = $a2;
|
||||||
|
if ($a3) {
|
||||||
|
$this->defaultFetchOptions['constructor_args'] = $a3;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case PDO::FETCH_COLUMN:
|
||||||
|
$this->defaultFetchOptions['column'] = $a2;
|
||||||
|
break;
|
||||||
|
case PDO::FETCH_INTO:
|
||||||
|
$this->defaultFetchOptions['object'] = $a2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the values for the next fetch.
|
||||||
|
$this->fetchStyle = $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current row formatted according to the current fetch style.
|
||||||
|
*
|
||||||
|
* This is the core method of this class. It grabs the value at the current
|
||||||
|
* array position in $this->data and format it according to $this->fetchStyle
|
||||||
|
* and $this->fetchMode.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The current row formatted as requested.
|
||||||
|
*/
|
||||||
|
public function current() {
|
||||||
|
if (isset($this->currentRow)) {
|
||||||
|
switch ($this->fetchStyle) {
|
||||||
|
case PDO::FETCH_ASSOC:
|
||||||
|
return $this->currentRow;
|
||||||
|
case PDO::FETCH_BOTH:
|
||||||
|
// PDO::FETCH_BOTH returns an array indexed by both the column name
|
||||||
|
// and the column number.
|
||||||
|
return $this->currentRow + array_values($this->currentRow);
|
||||||
|
case PDO::FETCH_NUM:
|
||||||
|
return array_values($this->currentRow);
|
||||||
|
case PDO::FETCH_LAZY:
|
||||||
|
// We do not do lazy as everything is fetched already. Fallback to
|
||||||
|
// PDO::FETCH_OBJ.
|
||||||
|
case PDO::FETCH_OBJ:
|
||||||
|
return (object) $this->currentRow;
|
||||||
|
case PDO::FETCH_CLASS | PDO::FETCH_CLASSTYPE:
|
||||||
|
$class_name = array_unshift($this->currentRow);
|
||||||
|
// Deliberate no break.
|
||||||
|
case PDO::FETCH_CLASS:
|
||||||
|
if (!isset($class_name)) {
|
||||||
|
$class_name = $this->fetchOptions['class'];
|
||||||
|
}
|
||||||
|
if (count($this->fetchOptions['constructor_args'])) {
|
||||||
|
$reflector = new ReflectionClass($class_name);
|
||||||
|
$result = $reflector->newInstanceArgs($this->fetchOptions['constructor_args']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$result = new $class_name();
|
||||||
|
}
|
||||||
|
foreach ($this->currentRow as $k => $v) {
|
||||||
|
$result->$k = $v;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
case PDO::FETCH_INTO:
|
||||||
|
foreach ($this->currentRow as $k => $v) {
|
||||||
|
$this->fetchOptions['object']->$k = $v;
|
||||||
|
}
|
||||||
|
return $this->fetchOptions['object'];
|
||||||
|
case PDO::FETCH_COLUMN:
|
||||||
|
if (isset($this->columnNames[$this->fetchOptions['column']])) {
|
||||||
|
return $this->currentRow[$k][$this->columnNames[$this->fetchOptions['column']]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Implementations of Iterator. */
|
||||||
|
|
||||||
|
public function key() {
|
||||||
|
return $this->currentKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rewind() {
|
||||||
|
// Nothing to do: our DatabaseStatement can't be rewound.
|
||||||
|
}
|
||||||
|
|
||||||
|
public function next() {
|
||||||
|
if (!empty($this->data)) {
|
||||||
|
$this->currentRow = reset($this->data);
|
||||||
|
$this->currentKey = key($this->data);
|
||||||
|
unset($this->data[$this->currentKey]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->currentRow = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function valid() {
|
||||||
|
return isset($this->currentRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Implementations of DatabaseStatementInterface. */
|
||||||
|
|
||||||
|
public function rowCount() {
|
||||||
|
return $this->rowCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetch($fetch_style = NULL, $cursor_orientation = PDO::FETCH_ORI_NEXT, $cursor_offset = NULL) {
|
||||||
|
if (isset($this->currentRow)) {
|
||||||
|
// Set the fetch parameter.
|
||||||
|
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
|
||||||
|
// Grab the row in the format specified above.
|
||||||
|
$return = $this->current();
|
||||||
|
// Advance the cursor.
|
||||||
|
$this->next();
|
||||||
|
|
||||||
|
// Reset the fetch parameters to the value stored using setFetchMode().
|
||||||
|
$this->fetchStyle = $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchColumn($index = 0) {
|
||||||
|
if (isset($this->currentRow) && isset($this->columnNames[$index])) {
|
||||||
|
// We grab the value directly from $this->data, and format it.
|
||||||
|
$return = $this->currentRow[$this->columnNames[$index]];
|
||||||
|
$this->next();
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchField($index = 0) {
|
||||||
|
return $this->fetchColumn($index);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchObject($class_name = NULL, $constructor_args = array()) {
|
||||||
|
if (isset($this->currentRow)) {
|
||||||
|
if (!isset($class_name)) {
|
||||||
|
// Directly cast to an object to avoid a function call.
|
||||||
|
$result = (object) $this->currentRow;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->fetchStyle = PDO::FETCH_CLASS;
|
||||||
|
$this->fetchOptions = array('constructor_args' => $constructor_args);
|
||||||
|
// Grab the row in the format specified above.
|
||||||
|
$result = $this->current();
|
||||||
|
// Reset the fetch parameters to the value stored using setFetchMode().
|
||||||
|
$this->fetchStyle = $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->next();
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchAssoc() {
|
||||||
|
if (isset($this->currentRow)) {
|
||||||
|
$result = $this->currentRow;
|
||||||
|
$this->next();
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchAll($fetch_style = NULL, $fetch_column = NULL, $constructor_args = NULL) {
|
||||||
|
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
if (isset($fetch_column)) {
|
||||||
|
$this->fetchOptions['column'] = $fetch_column;
|
||||||
|
}
|
||||||
|
if (isset($constructor_args)) {
|
||||||
|
$this->fetchOptions['constructor_args'] = $constructor_args;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
// Traverse the array as PHP would have done.
|
||||||
|
while (isset($this->currentRow)) {
|
||||||
|
// Grab the row in the format specified above.
|
||||||
|
$result[] = $this->current();
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the fetch parameters to the value stored using setFetchMode().
|
||||||
|
$this->fetchStyle = $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchCol($index = 0) {
|
||||||
|
if (isset($this->columnNames[$index])) {
|
||||||
|
$column = $this->columnNames[$index];
|
||||||
|
$result = array();
|
||||||
|
// Traverse the array as PHP would have done.
|
||||||
|
while (isset($this->currentRow)) {
|
||||||
|
$result[] = $this->currentRow[$this->columnNames[$index]];
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchAllKeyed($key_index = 0, $value_index = 1) {
|
||||||
|
if (!isset($this->columnNames[$key_index]) || !isset($this->columnNames[$value_index]))
|
||||||
|
return array();
|
||||||
|
|
||||||
|
$key = $this->columnNames[$key_index];
|
||||||
|
$value = $this->columnNames[$value_index];
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
// Traverse the array as PHP would have done.
|
||||||
|
while (isset($this->currentRow)) {
|
||||||
|
$result[$this->currentRow[$key]] = $this->currentRow[$value];
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchAllAssoc($key, $fetch_style = NULL) {
|
||||||
|
$this->fetchStyle = isset($fetch_style) ? $fetch_style : $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
|
||||||
|
$result = array();
|
||||||
|
// Traverse the array as PHP would have done.
|
||||||
|
while (isset($this->currentRow)) {
|
||||||
|
// Grab the row in its raw PDO::FETCH_ASSOC format.
|
||||||
|
$row = $this->currentRow;
|
||||||
|
// Grab the row in the format specified above.
|
||||||
|
$result_row = $this->current();
|
||||||
|
$result[$this->currentRow[$key]] = $result_row;
|
||||||
|
$this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the fetch parameters to the value stored using setFetchMode().
|
||||||
|
$this->fetchStyle = $this->defaultFetchStyle;
|
||||||
|
$this->fetchOptions = $this->defaultFetchOptions;
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
||||||
|
|
1963
includes/database/query.inc
Normal file
1963
includes/database/query.inc
Normal file
File diff suppressed because it is too large
Load diff
733
includes/database/schema.inc
Normal file
733
includes/database/schema.inc
Normal file
|
@ -0,0 +1,733 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Generic Database schema code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
require_once dirname(__FILE__) . '/query.inc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup schemaapi Schema API
|
||||||
|
* @{
|
||||||
|
* API to handle database schemas.
|
||||||
|
*
|
||||||
|
* A Drupal schema definition is an array structure representing one or
|
||||||
|
* more tables and their related keys and indexes. A schema is defined by
|
||||||
|
* hook_schema(), which usually lives in a modulename.install file.
|
||||||
|
*
|
||||||
|
* By implementing hook_schema() and specifying the tables your module
|
||||||
|
* declares, you can easily create and drop these tables on all
|
||||||
|
* supported database engines. You don't have to deal with the
|
||||||
|
* different SQL dialects for table creation and alteration of the
|
||||||
|
* supported database engines.
|
||||||
|
*
|
||||||
|
* hook_schema() should return an array with a key for each table that
|
||||||
|
* the module defines.
|
||||||
|
*
|
||||||
|
* The following keys are defined:
|
||||||
|
* - 'description': A string in non-markup plain text describing this table
|
||||||
|
* and its purpose. References to other tables should be enclosed in
|
||||||
|
* curly-brackets. For example, the node_revisions table
|
||||||
|
* description field might contain "Stores per-revision title and
|
||||||
|
* body data for each {node}."
|
||||||
|
* - 'fields': An associative array ('fieldname' => specification)
|
||||||
|
* that describes the table's database columns. The specification
|
||||||
|
* is also an array. The following specification parameters are defined:
|
||||||
|
* - 'description': A string in non-markup plain text describing this field
|
||||||
|
* and its purpose. References to other tables should be enclosed in
|
||||||
|
* curly-brackets. For example, the node table vid field
|
||||||
|
* description might contain "Always holds the largest (most
|
||||||
|
* recent) {node_revision}.vid value for this nid."
|
||||||
|
* - 'type': The generic datatype: 'char', 'varchar', 'text', 'blob', 'int',
|
||||||
|
* 'float', 'numeric', or 'serial'. Most types just map to the according
|
||||||
|
* database engine specific datatypes. Use 'serial' for auto incrementing
|
||||||
|
* fields. This will expand to 'INT auto_increment' on MySQL.
|
||||||
|
* - 'mysql_type', 'pgsql_type', 'sqlite_type', etc.: If you need to
|
||||||
|
* use a record type not included in the officially supported list
|
||||||
|
* of types above, you can specify a type for each database
|
||||||
|
* backend. In this case, you can leave out the type parameter,
|
||||||
|
* but be advised that your schema will fail to load on backends that
|
||||||
|
* do not have a type specified. A possible solution can be to
|
||||||
|
* use the "text" type as a fallback.
|
||||||
|
* - 'serialize': A boolean indicating whether the field will be stored as
|
||||||
|
* a serialized string.
|
||||||
|
* - 'size': The data size: 'tiny', 'small', 'medium', 'normal',
|
||||||
|
* 'big'. This is a hint about the largest value the field will
|
||||||
|
* store and determines which of the database engine specific
|
||||||
|
* datatypes will be used (e.g. on MySQL, TINYINT vs. INT vs. BIGINT).
|
||||||
|
* 'normal', the default, selects the base type (e.g. on MySQL,
|
||||||
|
* INT, VARCHAR, BLOB, etc.).
|
||||||
|
* Not all sizes are available for all data types. See
|
||||||
|
* DatabaseSchema::getFieldTypeMap() for possible combinations.
|
||||||
|
* - 'not null': If true, no NULL values will be allowed in this
|
||||||
|
* database column. Defaults to false.
|
||||||
|
* - 'default': The field's default value. The PHP type of the
|
||||||
|
* value matters: '', '0', and 0 are all different. If you
|
||||||
|
* specify '0' as the default value for a type 'int' field it
|
||||||
|
* will not work because '0' is a string containing the
|
||||||
|
* character "zero", not an integer.
|
||||||
|
* - 'length': The maximal length of a type 'char', 'varchar' or 'text'
|
||||||
|
* field. Ignored for other field types.
|
||||||
|
* - 'unsigned': A boolean indicating whether a type 'int', 'float'
|
||||||
|
* and 'numeric' only is signed or unsigned. Defaults to
|
||||||
|
* FALSE. Ignored for other field types.
|
||||||
|
* - 'precision', 'scale': For type 'numeric' fields, indicates
|
||||||
|
* the precision (total number of significant digits) and scale
|
||||||
|
* (decimal digits right of the decimal point). Both values are
|
||||||
|
* mandatory. Ignored for other field types.
|
||||||
|
* - 'binary': A boolean indicating that MySQL should force 'char',
|
||||||
|
* 'varchar' or 'text' fields to use case-sensitive binary collation.
|
||||||
|
* This has no effect on other database types for which case sensitivity
|
||||||
|
* is already the default behavior.
|
||||||
|
* All parameters apart from 'type' are optional except that type
|
||||||
|
* 'numeric' columns must specify 'precision' and 'scale', and type
|
||||||
|
* 'varchar' must specify the 'length' parameter.
|
||||||
|
* - 'primary key': An array of one or more key column specifiers (see below)
|
||||||
|
* that form the primary key.
|
||||||
|
* - 'unique keys': An associative array of unique keys ('keyname' =>
|
||||||
|
* specification). Each specification is an array of one or more
|
||||||
|
* key column specifiers (see below) that form a unique key on the table.
|
||||||
|
* - 'foreign keys': An associative array of relations ('my_relation' =>
|
||||||
|
* specification). Each specification is an array containing the name of
|
||||||
|
* the referenced table ('table'), and an array of column mappings
|
||||||
|
* ('columns'). Column mappings are defined by key pairs ('source_column' =>
|
||||||
|
* 'referenced_column'). This key is for documentation purposes only; foreign
|
||||||
|
* keys are not created in the database, nor are they enforced by Drupal.
|
||||||
|
* - 'indexes': An associative array of indexes ('indexname' =>
|
||||||
|
* specification). Each specification is an array of one or more
|
||||||
|
* key column specifiers (see below) that form an index on the
|
||||||
|
* table.
|
||||||
|
*
|
||||||
|
* A key column specifier is either a string naming a column or an
|
||||||
|
* array of two elements, column name and length, specifying a prefix
|
||||||
|
* of the named column.
|
||||||
|
*
|
||||||
|
* As an example, here is a SUBSET of the schema definition for
|
||||||
|
* Drupal's 'node' table. It show four fields (nid, vid, type, and
|
||||||
|
* title), the primary key on field 'nid', a unique key named 'vid' on
|
||||||
|
* field 'vid', and two indexes, one named 'nid' on field 'nid' and
|
||||||
|
* one named 'node_title_type' on the field 'title' and the first four
|
||||||
|
* bytes of the field 'type':
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* $schema['node'] = array(
|
||||||
|
* 'description' => 'The base table for nodes.',
|
||||||
|
* 'fields' => array(
|
||||||
|
* 'nid' => array('type' => 'serial', 'unsigned' => TRUE, 'not null' => TRUE),
|
||||||
|
* 'vid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE,'default' => 0),
|
||||||
|
* 'type' => array('type' => 'varchar','length' => 32,'not null' => TRUE, 'default' => ''),
|
||||||
|
* 'language' => array('type' => 'varchar','length' => 12,'not null' => TRUE,'default' => ''),
|
||||||
|
* 'title' => array('type' => 'varchar','length' => 255,'not null' => TRUE, 'default' => ''),
|
||||||
|
* 'uid' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'status' => array('type' => 'int', 'not null' => TRUE, 'default' => 1),
|
||||||
|
* 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'changed' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'comment' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'promote' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'moderate' => array('type' => 'int', 'not null' => TRUE,'default' => 0),
|
||||||
|
* 'sticky' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'tnid' => array('type' => 'int', 'unsigned' => TRUE, 'not null' => TRUE, 'default' => 0),
|
||||||
|
* 'translate' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
|
||||||
|
* ),
|
||||||
|
* 'indexes' => array(
|
||||||
|
* 'node_changed' => array('changed'),
|
||||||
|
* 'node_created' => array('created'),
|
||||||
|
* 'node_moderate' => array('moderate'),
|
||||||
|
* 'node_frontpage' => array('promote', 'status', 'sticky', 'created'),
|
||||||
|
* 'node_status_type' => array('status', 'type', 'nid'),
|
||||||
|
* 'node_title_type' => array('title', array('type', 4)),
|
||||||
|
* 'node_type' => array(array('type', 4)),
|
||||||
|
* 'uid' => array('uid'),
|
||||||
|
* 'tnid' => array('tnid'),
|
||||||
|
* 'translate' => array('translate'),
|
||||||
|
* ),
|
||||||
|
* 'unique keys' => array(
|
||||||
|
* 'vid' => array('vid'),
|
||||||
|
* ),
|
||||||
|
* // For documentation purposes only; foreign keys are not created in the
|
||||||
|
* // database.
|
||||||
|
* 'foreign keys' => array(
|
||||||
|
* 'node_revision' => array(
|
||||||
|
* 'table' => 'node_revision',
|
||||||
|
* 'columns' => array('vid' => 'vid'),
|
||||||
|
* ),
|
||||||
|
* 'node_author' => array(
|
||||||
|
* 'table' => 'users',
|
||||||
|
* 'columns' => array('uid' => 'uid'),
|
||||||
|
* ),
|
||||||
|
* ),
|
||||||
|
* 'primary key' => array('nid'),
|
||||||
|
* );
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @see drupal_install_schema()
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for database schema definitions.
|
||||||
|
*/
|
||||||
|
abstract class DatabaseSchema implements QueryPlaceholderInterface {
|
||||||
|
|
||||||
|
protected $connection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The placeholder counter.
|
||||||
|
*/
|
||||||
|
protected $placeholder = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Definition of prefixInfo array structure.
|
||||||
|
*
|
||||||
|
* Rather than redefining DatabaseSchema::getPrefixInfo() for each driver,
|
||||||
|
* by defining the defaultSchema variable only MySQL has to re-write the
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @see DatabaseSchema::getPrefixInfo()
|
||||||
|
*/
|
||||||
|
protected $defaultSchema = 'public';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique identifier for this query object.
|
||||||
|
*/
|
||||||
|
protected $uniqueIdentifier;
|
||||||
|
|
||||||
|
public function __construct($connection) {
|
||||||
|
$this->uniqueIdentifier = uniqid('', TRUE);
|
||||||
|
$this->connection = $connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements the magic __clone function.
|
||||||
|
*/
|
||||||
|
public function __clone() {
|
||||||
|
$this->uniqueIdentifier = uniqid('', TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements QueryPlaceHolderInterface::uniqueIdentifier().
|
||||||
|
*/
|
||||||
|
public function uniqueIdentifier() {
|
||||||
|
return $this->uniqueIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements QueryPlaceHolderInterface::nextPlaceholder().
|
||||||
|
*/
|
||||||
|
public function nextPlaceholder() {
|
||||||
|
return $this->placeholder++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get information about the table name and schema from the prefix.
|
||||||
|
*
|
||||||
|
* @param
|
||||||
|
* Name of table to look prefix up for. Defaults to 'default' because thats
|
||||||
|
* default key for prefix.
|
||||||
|
* @param $add_prefix
|
||||||
|
* Boolean that indicates whether the given table name should be prefixed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A keyed array with information about the schema, table name and prefix.
|
||||||
|
*/
|
||||||
|
protected function getPrefixInfo($table = 'default', $add_prefix = TRUE) {
|
||||||
|
$info = array(
|
||||||
|
'schema' => $this->defaultSchema,
|
||||||
|
'prefix' => $this->connection->tablePrefix($table),
|
||||||
|
);
|
||||||
|
if ($add_prefix) {
|
||||||
|
$table = $info['prefix'] . $table;
|
||||||
|
}
|
||||||
|
// If the prefix contains a period in it, then that means the prefix also
|
||||||
|
// contains a schema reference in which case we will change the schema key
|
||||||
|
// to the value before the period in the prefix. Everything after the dot
|
||||||
|
// will be prefixed onto the front of the table.
|
||||||
|
if (($pos = strpos($table, '.')) !== FALSE) {
|
||||||
|
// Grab everything before the period.
|
||||||
|
$info['schema'] = substr($table, 0, $pos);
|
||||||
|
// Grab everything after the dot.
|
||||||
|
$info['table'] = substr($table, ++$pos);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$info['table'] = $table;
|
||||||
|
}
|
||||||
|
return $info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create names for indexes, primary keys and constraints.
|
||||||
|
*
|
||||||
|
* This prevents using {} around non-table names like indexes and keys.
|
||||||
|
*/
|
||||||
|
function prefixNonTable($table) {
|
||||||
|
$args = func_get_args();
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
$args[0] = $info['table'];
|
||||||
|
return implode('_', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a condition to match a table name against a standard information_schema.
|
||||||
|
*
|
||||||
|
* The information_schema is a SQL standard that provides information about the
|
||||||
|
* database server and the databases, schemas, tables, columns and users within
|
||||||
|
* it. This makes information_schema a useful tool to use across the drupal
|
||||||
|
* database drivers and is used by a few different functions. The function below
|
||||||
|
* describes the conditions to be meet when querying information_schema.tables
|
||||||
|
* for drupal tables or information associated with drupal tables. Even though
|
||||||
|
* this is the standard method, not all databases follow standards and so this
|
||||||
|
* method should be overwritten by a database driver if the database provider
|
||||||
|
* uses alternate methods. Because information_schema.tables is used in a few
|
||||||
|
* different functions, a database driver will only need to override this function
|
||||||
|
* to make all the others work. For example see includes/databases/mysql/schema.inc.
|
||||||
|
*
|
||||||
|
* @param $table_name
|
||||||
|
* The name of the table in question.
|
||||||
|
* @param $operator
|
||||||
|
* The operator to apply on the 'table' part of the condition.
|
||||||
|
* @param $add_prefix
|
||||||
|
* Boolean to indicate whether the table name needs to be prefixed.
|
||||||
|
*
|
||||||
|
* @return QueryConditionInterface
|
||||||
|
* A DatabaseCondition object.
|
||||||
|
*/
|
||||||
|
protected function buildTableNameCondition($table_name, $operator = '=', $add_prefix = TRUE) {
|
||||||
|
$info = $this->connection->getConnectionOptions();
|
||||||
|
|
||||||
|
// Retrieve the table name and schema
|
||||||
|
$table_info = $this->getPrefixInfo($table_name, $add_prefix);
|
||||||
|
|
||||||
|
$condition = new DatabaseCondition('AND');
|
||||||
|
$condition->condition('table_catalog', $info['database']);
|
||||||
|
$condition->condition('table_schema', $table_info['schema']);
|
||||||
|
$condition->condition('table_name', $table_info['table'], $operator);
|
||||||
|
return $condition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a table exists.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The name of the table in drupal (no prefixing).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the given table exists, otherwise FALSE.
|
||||||
|
*/
|
||||||
|
public function tableExists($table) {
|
||||||
|
$condition = $this->buildTableNameCondition($table);
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
// Normally, we would heartily discourage the use of string
|
||||||
|
// concatenation for conditionals like this however, we
|
||||||
|
// couldn't use db_select() here because it would prefix
|
||||||
|
// information_schema.tables and the query would fail.
|
||||||
|
// Don't use {} around information_schema.tables table.
|
||||||
|
return (bool) $this->connection->query("SELECT 1 FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all tables that are like the specified base table name.
|
||||||
|
*
|
||||||
|
* @param $table_expression
|
||||||
|
* An SQL expression, for example "simpletest%" (without the quotes).
|
||||||
|
* BEWARE: this is not prefixed, the caller should take care of that.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Array, both the keys and the values are the matching tables.
|
||||||
|
*/
|
||||||
|
public function findTables($table_expression) {
|
||||||
|
$condition = $this->buildTableNameCondition($table_expression, 'LIKE', FALSE);
|
||||||
|
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
// Normally, we would heartily discourage the use of string
|
||||||
|
// concatenation for conditionals like this however, we
|
||||||
|
// couldn't use db_select() here because it would prefix
|
||||||
|
// information_schema.tables and the query would fail.
|
||||||
|
// Don't use {} around information_schema.tables table.
|
||||||
|
return $this->connection->query("SELECT table_name FROM information_schema.tables WHERE " . (string) $condition, $condition->arguments())->fetchAllKeyed(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a column exists in the given table.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The name of the table in drupal (no prefixing).
|
||||||
|
* @param $name
|
||||||
|
* The name of the column.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the given column exists, otherwise FALSE.
|
||||||
|
*/
|
||||||
|
public function fieldExists($table, $column) {
|
||||||
|
$condition = $this->buildTableNameCondition($table);
|
||||||
|
$condition->condition('column_name', $column);
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
// Normally, we would heartily discourage the use of string
|
||||||
|
// concatenation for conditionals like this however, we
|
||||||
|
// couldn't use db_select() here because it would prefix
|
||||||
|
// information_schema.tables and the query would fail.
|
||||||
|
// Don't use {} around information_schema.columns table.
|
||||||
|
return (bool) $this->connection->query("SELECT 1 FROM information_schema.columns WHERE " . (string) $condition, $condition->arguments())->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a mapping of Drupal schema field names to DB-native field types.
|
||||||
|
*
|
||||||
|
* Because different field types do not map 1:1 between databases, Drupal has
|
||||||
|
* its own normalized field type names. This function returns a driver-specific
|
||||||
|
* mapping table from Drupal names to the native names for each database.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of Schema API field types to driver-specific field types.
|
||||||
|
*/
|
||||||
|
abstract public function getFieldTypeMap();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename a table.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be renamed.
|
||||||
|
* @param $new_name
|
||||||
|
* The new name for the table.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If a table with the specified new name already exists.
|
||||||
|
*/
|
||||||
|
abstract public function renameTable($table, $new_name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a table.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be dropped.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the table was successfully dropped, FALSE if there was no table
|
||||||
|
* by that name to begin with.
|
||||||
|
*/
|
||||||
|
abstract public function dropTable($table);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new field to a table.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* Name of the table to be altered.
|
||||||
|
* @param $field
|
||||||
|
* Name of the field to be added.
|
||||||
|
* @param $spec
|
||||||
|
* The field specification array, as taken from a schema definition.
|
||||||
|
* The specification may also contain the key 'initial', the newly
|
||||||
|
* created field will be set to the value of the key in all rows.
|
||||||
|
* This is most useful for creating NOT NULL columns with no default
|
||||||
|
* value in existing tables.
|
||||||
|
* @param $keys_new
|
||||||
|
* (optional) Keys and indexes specification to be created on the
|
||||||
|
* table along with adding the field. The format is the same as a
|
||||||
|
* table specification but without the 'fields' element. If you are
|
||||||
|
* adding a type 'serial' field, you MUST specify at least one key
|
||||||
|
* or index including it in this array. See db_change_field() for more
|
||||||
|
* explanation why.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified table already has a field by that name.
|
||||||
|
*/
|
||||||
|
abstract public function addField($table, $field, $spec, $keys_new = array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a field.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $field
|
||||||
|
* The field to be dropped.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the field was successfully dropped, FALSE if there was no field
|
||||||
|
* by that name to begin with.
|
||||||
|
*/
|
||||||
|
abstract public function dropField($table, $field);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the default value for a field.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $field
|
||||||
|
* The field to be altered.
|
||||||
|
* @param $default
|
||||||
|
* Default value to be set. NULL for 'default NULL'.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table or field doesn't exist.
|
||||||
|
*/
|
||||||
|
abstract public function fieldSetDefault($table, $field, $default);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a field to have no default value.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $field
|
||||||
|
* The field to be altered.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table or field doesn't exist.
|
||||||
|
*/
|
||||||
|
abstract public function fieldSetNoDefault($table, $field);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if an index exists in the given table.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The name of the table in drupal (no prefixing).
|
||||||
|
* @param $name
|
||||||
|
* The name of the index in drupal (no prefixing).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the given index exists, otherwise FALSE.
|
||||||
|
*/
|
||||||
|
abstract public function indexExists($table, $name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a primary key.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $fields
|
||||||
|
* Fields for the primary key.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified table already has a primary key.
|
||||||
|
*/
|
||||||
|
abstract public function addPrimaryKey($table, $fields);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop the primary key.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the primary key was successfully dropped, FALSE if there was no
|
||||||
|
* primary key on this table to begin with.
|
||||||
|
*/
|
||||||
|
abstract public function dropPrimaryKey($table);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a unique key.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $name
|
||||||
|
* The name of the key.
|
||||||
|
* @param $fields
|
||||||
|
* An array of field names.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified table already has a key by that name.
|
||||||
|
*/
|
||||||
|
abstract public function addUniqueKey($table, $name, $fields);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop a unique key.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $name
|
||||||
|
* The name of the key.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the key was successfully dropped, FALSE if there was no key by
|
||||||
|
* that name to begin with.
|
||||||
|
*/
|
||||||
|
abstract public function dropUniqueKey($table, $name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an index.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $name
|
||||||
|
* The name of the index.
|
||||||
|
* @param $fields
|
||||||
|
* An array of field names.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified table already has an index by that name.
|
||||||
|
*/
|
||||||
|
abstract public function addIndex($table, $name, $fields);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop an index.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* The table to be altered.
|
||||||
|
* @param $name
|
||||||
|
* The name of the index.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the index was successfully dropped, FALSE if there was no index
|
||||||
|
* by that name to begin with.
|
||||||
|
*/
|
||||||
|
abstract public function dropIndex($table, $name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change a field definition.
|
||||||
|
*
|
||||||
|
* IMPORTANT NOTE: To maintain database portability, you have to explicitly
|
||||||
|
* recreate all indices and primary keys that are using the changed field.
|
||||||
|
*
|
||||||
|
* That means that you have to drop all affected keys and indexes with
|
||||||
|
* db_drop_{primary_key,unique_key,index}() before calling db_change_field().
|
||||||
|
* To recreate the keys and indices, pass the key definitions as the
|
||||||
|
* optional $keys_new argument directly to db_change_field().
|
||||||
|
*
|
||||||
|
* For example, suppose you have:
|
||||||
|
* @code
|
||||||
|
* $schema['foo'] = array(
|
||||||
|
* 'fields' => array(
|
||||||
|
* 'bar' => array('type' => 'int', 'not null' => TRUE)
|
||||||
|
* ),
|
||||||
|
* 'primary key' => array('bar')
|
||||||
|
* );
|
||||||
|
* @endcode
|
||||||
|
* and you want to change foo.bar to be type serial, leaving it as the
|
||||||
|
* primary key. The correct sequence is:
|
||||||
|
* @code
|
||||||
|
* db_drop_primary_key('foo');
|
||||||
|
* db_change_field('foo', 'bar', 'bar',
|
||||||
|
* array('type' => 'serial', 'not null' => TRUE),
|
||||||
|
* array('primary key' => array('bar')));
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* The reasons for this are due to the different database engines:
|
||||||
|
*
|
||||||
|
* On PostgreSQL, changing a field definition involves adding a new field
|
||||||
|
* and dropping an old one which* causes any indices, primary keys and
|
||||||
|
* sequences (from serial-type fields) that use the changed field to be dropped.
|
||||||
|
*
|
||||||
|
* On MySQL, all type 'serial' fields must be part of at least one key
|
||||||
|
* or index as soon as they are created. You cannot use
|
||||||
|
* db_add_{primary_key,unique_key,index}() for this purpose because
|
||||||
|
* the ALTER TABLE command will fail to add the column without a key
|
||||||
|
* or index specification. The solution is to use the optional
|
||||||
|
* $keys_new argument to create the key or index at the same time as
|
||||||
|
* field.
|
||||||
|
*
|
||||||
|
* You could use db_add_{primary_key,unique_key,index}() in all cases
|
||||||
|
* unless you are converting a field to be type serial. You can use
|
||||||
|
* the $keys_new argument in all cases.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* Name of the table.
|
||||||
|
* @param $field
|
||||||
|
* Name of the field to change.
|
||||||
|
* @param $field_new
|
||||||
|
* New name for the field (set to the same as $field if you don't want to change the name).
|
||||||
|
* @param $spec
|
||||||
|
* The field specification for the new field.
|
||||||
|
* @param $keys_new
|
||||||
|
* (optional) Keys and indexes specification to be created on the
|
||||||
|
* table along with changing the field. The format is the same as a
|
||||||
|
* table specification but without the 'fields' element.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectDoesNotExistException
|
||||||
|
* If the specified table or source field doesn't exist.
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified destination field already exists.
|
||||||
|
*/
|
||||||
|
abstract public function changeField($table, $field, $field_new, $spec, $keys_new = array());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new table from a Drupal table definition.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the table to create.
|
||||||
|
* @param $table
|
||||||
|
* A Schema API table definition array.
|
||||||
|
*
|
||||||
|
* @throws DatabaseSchemaObjectExistsException
|
||||||
|
* If the specified table already exists.
|
||||||
|
*/
|
||||||
|
public function createTable($name, $table) {
|
||||||
|
if ($this->tableExists($name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t('Table @name already exists.', array('@name' => $name)));
|
||||||
|
}
|
||||||
|
$statements = $this->createTableSql($name, $table);
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
$this->connection->query($statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of field names from an array of key/index column specifiers.
|
||||||
|
*
|
||||||
|
* This is usually an identity function but if a key/index uses a column prefix
|
||||||
|
* specification, this function extracts just the name.
|
||||||
|
*
|
||||||
|
* @param $fields
|
||||||
|
* An array of key/index column specifiers.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of field names.
|
||||||
|
*/
|
||||||
|
public function fieldNames($fields) {
|
||||||
|
$return = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$return[] = $field[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare a table or column comment for database query.
|
||||||
|
*
|
||||||
|
* @param $comment
|
||||||
|
* The comment string to prepare.
|
||||||
|
* @param $length
|
||||||
|
* Optional upper limit on the returned string length.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The prepared comment.
|
||||||
|
*/
|
||||||
|
public function prepareComment($comment, $length = NULL) {
|
||||||
|
return $this->connection->quote($comment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown if an object being created already exists.
|
||||||
|
*
|
||||||
|
* For example, this exception should be thrown whenever there is an attempt to
|
||||||
|
* create a new database table, field, or index that already exists in the
|
||||||
|
* database schema.
|
||||||
|
*/
|
||||||
|
class DatabaseSchemaObjectExistsException extends Exception {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown if an object being modified doesn't exist yet.
|
||||||
|
*
|
||||||
|
* For example, this exception should be thrown whenever there is an attempt to
|
||||||
|
* modify a database table, field, or index that does not currently exist in
|
||||||
|
* the database schema.
|
||||||
|
*/
|
||||||
|
class DatabaseSchemaObjectDoesNotExistException extends Exception {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "defgroup schemaapi".
|
||||||
|
*/
|
||||||
|
|
1631
includes/database/select.inc
Normal file
1631
includes/database/select.inc
Normal file
File diff suppressed because it is too large
Load diff
527
includes/database/sqlite/database.inc
Normal file
527
includes/database/sqlite/database.inc
Normal file
|
@ -0,0 +1,527 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database interface code for SQLite embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
include_once DRUPAL_ROOT . '/includes/database/prefetch.inc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific SQLite implementation of DatabaseConnection.
|
||||||
|
*/
|
||||||
|
class DatabaseConnection_sqlite extends DatabaseConnection {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this database connection supports savepoints.
|
||||||
|
*
|
||||||
|
* Version of sqlite lower then 3.6.8 can't use savepoints.
|
||||||
|
* See http://www.sqlite.org/releaselog/3_6_8.html
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $savepointSupport = FALSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the active transaction (if any) will be rolled back.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $willRollback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All databases attached to the current database. This is used to allow
|
||||||
|
* prefixes to be safely handled without locking the table
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $attachedDatabases = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not a table has been dropped this request: the destructor will
|
||||||
|
* only try to get rid of unnecessary databases if there is potential of them
|
||||||
|
* being empty.
|
||||||
|
*
|
||||||
|
* This variable is set to public because DatabaseSchema_sqlite needs to
|
||||||
|
* access it. However, it should not be manually set.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
var $tableDropped = FALSE;
|
||||||
|
|
||||||
|
public function __construct(array $connection_options = array()) {
|
||||||
|
// We don't need a specific PDOStatement class here, we simulate it below.
|
||||||
|
$this->statementClass = NULL;
|
||||||
|
|
||||||
|
// This driver defaults to transaction support, except if explicitly passed FALSE.
|
||||||
|
$this->transactionSupport = $this->transactionalDDLSupport = !isset($connection_options['transactions']) || $connection_options['transactions'] !== FALSE;
|
||||||
|
|
||||||
|
$this->connectionOptions = $connection_options;
|
||||||
|
|
||||||
|
// Allow PDO options to be overridden.
|
||||||
|
$connection_options += array(
|
||||||
|
'pdo' => array(),
|
||||||
|
);
|
||||||
|
$connection_options['pdo'] += array(
|
||||||
|
// Convert numeric values to strings when fetching.
|
||||||
|
PDO::ATTR_STRINGIFY_FETCHES => TRUE,
|
||||||
|
);
|
||||||
|
parent::__construct('sqlite:' . $connection_options['database'], '', '', $connection_options['pdo']);
|
||||||
|
|
||||||
|
// Attach one database for each registered prefix.
|
||||||
|
$prefixes = $this->prefixes;
|
||||||
|
foreach ($prefixes as $table => &$prefix) {
|
||||||
|
// Empty prefix means query the main database -- no need to attach anything.
|
||||||
|
if (!empty($prefix)) {
|
||||||
|
// Only attach the database once.
|
||||||
|
if (!isset($this->attachedDatabases[$prefix])) {
|
||||||
|
$this->attachedDatabases[$prefix] = $prefix;
|
||||||
|
$this->query('ATTACH DATABASE :database AS :prefix', array(':database' => $connection_options['database'] . '-' . $prefix, ':prefix' => $prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a ., so queries become prefix.table, which is proper syntax for
|
||||||
|
// querying an attached database.
|
||||||
|
$prefix .= '.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Regenerate the prefixes replacement table.
|
||||||
|
$this->setPrefix($prefixes);
|
||||||
|
|
||||||
|
// Detect support for SAVEPOINT.
|
||||||
|
$version = $this->query('SELECT sqlite_version()')->fetchField();
|
||||||
|
$this->savepointSupport = (version_compare($version, '3.6.8') >= 0);
|
||||||
|
|
||||||
|
// Create functions needed by SQLite.
|
||||||
|
$this->sqliteCreateFunction('if', array($this, 'sqlFunctionIf'));
|
||||||
|
$this->sqliteCreateFunction('greatest', array($this, 'sqlFunctionGreatest'));
|
||||||
|
$this->sqliteCreateFunction('pow', 'pow', 2);
|
||||||
|
$this->sqliteCreateFunction('length', 'strlen', 1);
|
||||||
|
$this->sqliteCreateFunction('md5', 'md5', 1);
|
||||||
|
$this->sqliteCreateFunction('concat', array($this, 'sqlFunctionConcat'));
|
||||||
|
$this->sqliteCreateFunction('substring', array($this, 'sqlFunctionSubstring'), 3);
|
||||||
|
$this->sqliteCreateFunction('substring_index', array($this, 'sqlFunctionSubstringIndex'), 3);
|
||||||
|
$this->sqliteCreateFunction('rand', array($this, 'sqlFunctionRand'));
|
||||||
|
|
||||||
|
// Execute sqlite init_commands.
|
||||||
|
if (isset($connection_options['init_commands'])) {
|
||||||
|
$this->exec(implode('; ', $connection_options['init_commands']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destructor for the SQLite connection.
|
||||||
|
*
|
||||||
|
* We prune empty databases on destruct, but only if tables have been
|
||||||
|
* dropped. This is especially needed when running the test suite, which
|
||||||
|
* creates and destroy databases several times in a row.
|
||||||
|
*/
|
||||||
|
public function __destruct() {
|
||||||
|
if ($this->tableDropped && !empty($this->attachedDatabases)) {
|
||||||
|
foreach ($this->attachedDatabases as $prefix) {
|
||||||
|
// Check if the database is now empty, ignore the internal SQLite tables.
|
||||||
|
try {
|
||||||
|
$count = $this->query('SELECT COUNT(*) FROM ' . $prefix . '.sqlite_master WHERE type = :type AND name NOT LIKE :pattern', array(':type' => 'table', ':pattern' => 'sqlite_%'))->fetchField();
|
||||||
|
|
||||||
|
// We can prune the database file if it doesn't have any tables.
|
||||||
|
if ($count == 0) {
|
||||||
|
// Detach the database.
|
||||||
|
$this->query('DETACH DATABASE :schema', array(':schema' => $prefix));
|
||||||
|
// Destroy the database file.
|
||||||
|
unlink($this->connectionOptions['database'] . '-' . $prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
// Ignore the exception and continue. There is nothing we can do here
|
||||||
|
// to report the error or fail safe.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the IF() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionIf($condition, $expr1, $expr2 = NULL) {
|
||||||
|
return $condition ? $expr1 : $expr2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the GREATEST() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionGreatest() {
|
||||||
|
$args = func_get_args();
|
||||||
|
foreach ($args as $k => $v) {
|
||||||
|
if (!isset($v)) {
|
||||||
|
unset($args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($args)) {
|
||||||
|
return max($args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the CONCAT() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionConcat() {
|
||||||
|
$args = func_get_args();
|
||||||
|
return implode('', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the SUBSTRING() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionSubstring($string, $from, $length) {
|
||||||
|
return substr($string, $from - 1, $length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the SUBSTRING_INDEX() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionSubstringIndex($string, $delimiter, $count) {
|
||||||
|
// If string is empty, simply return an empty string.
|
||||||
|
if (empty($string)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$end = 0;
|
||||||
|
for ($i = 0; $i < $count; $i++) {
|
||||||
|
$end = strpos($string, $delimiter, $end + 1);
|
||||||
|
if ($end === FALSE) {
|
||||||
|
$end = strlen($string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return substr($string, 0, $end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite compatibility implementation for the RAND() SQL function.
|
||||||
|
*/
|
||||||
|
public function sqlFunctionRand($seed = NULL) {
|
||||||
|
if (isset($seed)) {
|
||||||
|
mt_srand($seed);
|
||||||
|
}
|
||||||
|
return mt_rand() / mt_getrandmax();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite-specific implementation of DatabaseConnection::prepare().
|
||||||
|
*
|
||||||
|
* We don't use prepared statements at all at this stage. We just create
|
||||||
|
* a DatabaseStatement_sqlite object, that will create a PDOStatement
|
||||||
|
* using the semi-private PDOPrepare() method below.
|
||||||
|
*/
|
||||||
|
public function prepare($query, $options = array()) {
|
||||||
|
return new DatabaseStatement_sqlite($this, $query, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEVER CALL THIS FUNCTION: YOU MIGHT DEADLOCK YOUR PHP PROCESS.
|
||||||
|
*
|
||||||
|
* This is a wrapper around the parent PDO::prepare method. However, as
|
||||||
|
* the PDO SQLite driver only closes SELECT statements when the PDOStatement
|
||||||
|
* destructor is called and SQLite does not allow data change (INSERT,
|
||||||
|
* UPDATE etc) on a table which has open SELECT statements, you should never
|
||||||
|
* call this function and keep a PDOStatement object alive as that can lead
|
||||||
|
* to a deadlock. This really, really should be private, but as
|
||||||
|
* DatabaseStatement_sqlite needs to call it, we have no other choice but to
|
||||||
|
* expose this function to the world.
|
||||||
|
*/
|
||||||
|
public function PDOPrepare($query, array $options = array()) {
|
||||||
|
return parent::prepare($query, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryRange($query, $from, $count, array $args = array(), array $options = array()) {
|
||||||
|
return $this->query($query . ' LIMIT ' . (int) $from . ', ' . (int) $count, $args, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function queryTemporary($query, array $args = array(), array $options = array()) {
|
||||||
|
// Generate a new temporary table name and protect it from prefixing.
|
||||||
|
// SQLite requires that temporary tables to be non-qualified.
|
||||||
|
$tablename = $this->generateTemporaryTableName();
|
||||||
|
$prefixes = $this->prefixes;
|
||||||
|
$prefixes[$tablename] = '';
|
||||||
|
$this->setPrefix($prefixes);
|
||||||
|
|
||||||
|
$this->query('CREATE TEMPORARY TABLE ' . $tablename . ' AS ' . $query, $args, $options);
|
||||||
|
return $tablename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function driver() {
|
||||||
|
return 'sqlite';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function databaseType() {
|
||||||
|
return 'sqlite';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mapConditionOperator($operator) {
|
||||||
|
// We don't want to override any of the defaults.
|
||||||
|
static $specials = array(
|
||||||
|
'LIKE' => array('postfix' => " ESCAPE '\\'"),
|
||||||
|
'NOT LIKE' => array('postfix' => " ESCAPE '\\'"),
|
||||||
|
);
|
||||||
|
return isset($specials[$operator]) ? $specials[$operator] : NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepareQuery($query) {
|
||||||
|
return $this->prepare($this->prefixTables($query));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextId($existing_id = 0) {
|
||||||
|
$transaction = $this->startTransaction();
|
||||||
|
// We can safely use literal queries here instead of the slower query
|
||||||
|
// builder because if a given database breaks here then it can simply
|
||||||
|
// override nextId. However, this is unlikely as we deal with short strings
|
||||||
|
// and integers and no known databases require special handling for those
|
||||||
|
// simple cases. If another transaction wants to write the same row, it will
|
||||||
|
// wait until this transaction commits.
|
||||||
|
$stmt = $this->query('UPDATE {sequences} SET value = GREATEST(value, :existing_id) + 1', array(
|
||||||
|
':existing_id' => $existing_id,
|
||||||
|
));
|
||||||
|
if (!$stmt->rowCount()) {
|
||||||
|
$this->query('INSERT INTO {sequences} (value) VALUES (:existing_id + 1)', array(
|
||||||
|
':existing_id' => $existing_id,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
// The transaction gets committed when the transaction object gets destroyed
|
||||||
|
// because it gets out of scope.
|
||||||
|
return $this->query('SELECT value FROM {sequences}')->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rollback($savepoint_name = 'drupal_transaction') {
|
||||||
|
if ($this->savepointSupport) {
|
||||||
|
return parent::rollBack($savepoint_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->inTransaction()) {
|
||||||
|
throw new DatabaseTransactionNoActiveException();
|
||||||
|
}
|
||||||
|
// A previous rollback to an earlier savepoint may mean that the savepoint
|
||||||
|
// in question has already been rolled back.
|
||||||
|
if (!in_array($savepoint_name, $this->transactionLayers)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to find the point we're rolling back to, all other savepoints
|
||||||
|
// before are no longer needed.
|
||||||
|
while ($savepoint = array_pop($this->transactionLayers)) {
|
||||||
|
if ($savepoint == $savepoint_name) {
|
||||||
|
// Mark whole stack of transactions as needed roll back.
|
||||||
|
$this->willRollback = TRUE;
|
||||||
|
// If it is the last the transaction in the stack, then it is not a
|
||||||
|
// savepoint, it is the transaction itself so we will need to roll back
|
||||||
|
// the transaction rather than a savepoint.
|
||||||
|
if (empty($this->transactionLayers)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($this->supportsTransactions()) {
|
||||||
|
PDO::rollBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pushTransaction($name) {
|
||||||
|
if ($this->savepointSupport) {
|
||||||
|
return parent::pushTransaction($name);
|
||||||
|
}
|
||||||
|
if (!$this->supportsTransactions()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isset($this->transactionLayers[$name])) {
|
||||||
|
throw new DatabaseTransactionNameNonUniqueException($name . " is already in use.");
|
||||||
|
}
|
||||||
|
if (!$this->inTransaction()) {
|
||||||
|
PDO::beginTransaction();
|
||||||
|
}
|
||||||
|
$this->transactionLayers[$name] = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function popTransaction($name) {
|
||||||
|
if ($this->savepointSupport) {
|
||||||
|
return parent::popTransaction($name);
|
||||||
|
}
|
||||||
|
if (!$this->supportsTransactions()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$this->inTransaction()) {
|
||||||
|
throw new DatabaseTransactionNoActiveException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit everything since SAVEPOINT $name.
|
||||||
|
while($savepoint = array_pop($this->transactionLayers)) {
|
||||||
|
if ($savepoint != $name) continue;
|
||||||
|
|
||||||
|
// If there are no more layers left then we should commit or rollback.
|
||||||
|
if (empty($this->transactionLayers)) {
|
||||||
|
// If there was any rollback() we should roll back whole transaction.
|
||||||
|
if ($this->willRollback) {
|
||||||
|
$this->willRollback = FALSE;
|
||||||
|
PDO::rollBack();
|
||||||
|
}
|
||||||
|
elseif (!PDO::commit()) {
|
||||||
|
throw new DatabaseTransactionCommitFailedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsActive() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function utf8mb4IsSupported() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specific SQLite implementation of DatabaseConnection.
|
||||||
|
*
|
||||||
|
* See DatabaseConnection_sqlite::PDOPrepare() for reasons why we must prefetch
|
||||||
|
* the data instead of using PDOStatement.
|
||||||
|
*
|
||||||
|
* @see DatabaseConnection_sqlite::PDOPrepare()
|
||||||
|
*/
|
||||||
|
class DatabaseStatement_sqlite extends DatabaseStatementPrefetch implements Iterator, DatabaseStatementInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific implementation of getStatement().
|
||||||
|
*
|
||||||
|
* The PDO SQLite layer doesn't replace numeric placeholders in queries
|
||||||
|
* correctly, and this makes numeric expressions (such as COUNT(*) >= :count)
|
||||||
|
* fail. We replace numeric placeholders in the query ourselves to work
|
||||||
|
* around this bug.
|
||||||
|
*
|
||||||
|
* See http://bugs.php.net/bug.php?id=45259 for more details.
|
||||||
|
*/
|
||||||
|
protected function getStatement($query, &$args = array()) {
|
||||||
|
if (count($args)) {
|
||||||
|
// Check if $args is a simple numeric array.
|
||||||
|
if (range(0, count($args) - 1) === array_keys($args)) {
|
||||||
|
// In that case, we have unnamed placeholders.
|
||||||
|
$count = 0;
|
||||||
|
$new_args = array();
|
||||||
|
foreach ($args as $value) {
|
||||||
|
if (is_float($value) || is_int($value)) {
|
||||||
|
if (is_float($value)) {
|
||||||
|
// Force the conversion to float so as not to loose precision
|
||||||
|
// in the automatic cast.
|
||||||
|
$value = sprintf('%F', $value);
|
||||||
|
}
|
||||||
|
$query = substr_replace($query, $value, strpos($query, '?'), 1);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$placeholder = ':db_statement_placeholder_' . $count++;
|
||||||
|
$query = substr_replace($query, $placeholder, strpos($query, '?'), 1);
|
||||||
|
$new_args[$placeholder] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$args = $new_args;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Else, this is using named placeholders.
|
||||||
|
foreach ($args as $placeholder => $value) {
|
||||||
|
if (is_float($value) || is_int($value)) {
|
||||||
|
if (is_float($value)) {
|
||||||
|
// Force the conversion to float so as not to loose precision
|
||||||
|
// in the automatic cast.
|
||||||
|
$value = sprintf('%F', $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will remove this placeholder from the query as PDO throws an
|
||||||
|
// exception if the number of placeholders in the query and the
|
||||||
|
// arguments does not match.
|
||||||
|
unset($args[$placeholder]);
|
||||||
|
// PDO allows placeholders to not be prefixed by a colon. See
|
||||||
|
// http://marc.info/?l=php-internals&m=111234321827149&w=2 for
|
||||||
|
// more.
|
||||||
|
if ($placeholder[0] != ':') {
|
||||||
|
$placeholder = ":$placeholder";
|
||||||
|
}
|
||||||
|
// When replacing the placeholders, make sure we search for the
|
||||||
|
// exact placeholder. For example, if searching for
|
||||||
|
// ':db_placeholder_1', do not replace ':db_placeholder_11'.
|
||||||
|
$query = preg_replace('/' . preg_quote($placeholder) . '\b/', $value, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->dbh->PDOPrepare($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute($args = array(), $options = array()) {
|
||||||
|
try {
|
||||||
|
$return = parent::execute($args, $options);
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
if (!empty($e->errorInfo[1]) && $e->errorInfo[1] === 17) {
|
||||||
|
// The schema has changed. SQLite specifies that we must resend the query.
|
||||||
|
$return = parent::execute($args, $options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Rethrow the exception.
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In some weird cases, SQLite will prefix some column names by the name
|
||||||
|
// of the table. We post-process the data, by renaming the column names
|
||||||
|
// using the same convention as MySQL and PostgreSQL.
|
||||||
|
$rename_columns = array();
|
||||||
|
foreach ($this->columnNames as $k => $column) {
|
||||||
|
// In some SQLite versions, SELECT DISTINCT(field) will return "(field)"
|
||||||
|
// instead of "field".
|
||||||
|
if (preg_match("/^\((.*)\)$/", $column, $matches)) {
|
||||||
|
$rename_columns[$column] = $matches[1];
|
||||||
|
$this->columnNames[$k] = $matches[1];
|
||||||
|
$column = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove "table." prefixes.
|
||||||
|
if (preg_match("/^.*\.(.*)$/", $column, $matches)) {
|
||||||
|
$rename_columns[$column] = $matches[1];
|
||||||
|
$this->columnNames[$k] = $matches[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($rename_columns) {
|
||||||
|
// DatabaseStatementPrefetch already extracted the first row,
|
||||||
|
// put it back into the result set.
|
||||||
|
if (isset($this->currentRow)) {
|
||||||
|
$this->data[0] = &$this->currentRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then rename all the columns across the result set.
|
||||||
|
foreach ($this->data as $k => $row) {
|
||||||
|
foreach ($rename_columns as $old_column => $new_column) {
|
||||||
|
$this->data[$k][$new_column] = $this->data[$k][$old_column];
|
||||||
|
unset($this->data[$k][$old_column]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, extract the first row again.
|
||||||
|
$this->currentRow = $this->data[0];
|
||||||
|
unset($this->data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
49
includes/database/sqlite/install.inc
Normal file
49
includes/database/sqlite/install.inc
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* SQLite specific install functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DatabaseTasks_sqlite extends DatabaseTasks {
|
||||||
|
protected $pdoDriver = 'sqlite';
|
||||||
|
|
||||||
|
public function name() {
|
||||||
|
return st('SQLite');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum engine version.
|
||||||
|
*/
|
||||||
|
public function minimumVersion() {
|
||||||
|
return '3.3.7';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormOptions($database) {
|
||||||
|
$form = parent::getFormOptions($database);
|
||||||
|
|
||||||
|
// Remove the options that only apply to client/server style databases.
|
||||||
|
unset($form['username'], $form['password'], $form['advanced_options']['host'], $form['advanced_options']['port']);
|
||||||
|
|
||||||
|
// Make the text more accurate for SQLite.
|
||||||
|
$form['database']['#title'] = st('Database file');
|
||||||
|
$form['database']['#description'] = st('The absolute path to the file where @drupal data will be stored. This must be writable by the web server and should exist outside of the web root.', array('@drupal' => drupal_install_profile_distribution_name()));
|
||||||
|
$default_database = conf_path(FALSE, TRUE) . '/files/.ht.sqlite';
|
||||||
|
$form['database']['#default_value'] = empty($database['database']) ? $default_database : $database['database'];
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function validateDatabaseSettings($database) {
|
||||||
|
// Perform standard validation.
|
||||||
|
$errors = parent::validateDatabaseSettings($database);
|
||||||
|
|
||||||
|
// Verify the database is writable.
|
||||||
|
$db_directory = new SplFileInfo(dirname($database['database']));
|
||||||
|
if (!$db_directory->isWritable()) {
|
||||||
|
$errors[$database['driver'] . '][database'] = st('The directory you specified is not writable by the web server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
139
includes/database/sqlite/query.inc
Normal file
139
includes/database/sqlite/query.inc
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Query code for SQLite embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific implementation of InsertQuery.
|
||||||
|
*
|
||||||
|
* We ignore all the default fields and use the clever SQLite syntax:
|
||||||
|
* INSERT INTO table DEFAULT VALUES
|
||||||
|
* for degenerated "default only" queries.
|
||||||
|
*/
|
||||||
|
class InsertQuery_sqlite extends InsertQuery {
|
||||||
|
|
||||||
|
public function execute() {
|
||||||
|
if (!$this->preExecute()) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (count($this->insertFields)) {
|
||||||
|
return parent::execute();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $this->connection->query('INSERT INTO {' . $this->table . '} DEFAULT VALUES', array(), $this->queryOptions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString() {
|
||||||
|
// Create a sanitized comment string to prepend to the query.
|
||||||
|
$comments = $this->connection->makeComment($this->comments);
|
||||||
|
|
||||||
|
// Produce as many generic placeholders as necessary.
|
||||||
|
$placeholders = array_fill(0, count($this->insertFields), '?');
|
||||||
|
|
||||||
|
// If we're selecting from a SelectQuery, finish building the query and
|
||||||
|
// pass it back, as any remaining options are irrelevant.
|
||||||
|
if (!empty($this->fromQuery)) {
|
||||||
|
$insert_fields_string = $this->insertFields ? ' (' . implode(', ', $this->insertFields) . ') ' : ' ';
|
||||||
|
return $comments . 'INSERT INTO {' . $this->table . '}' . $insert_fields_string . $this->fromQuery;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $comments . 'INSERT INTO {' . $this->table . '} (' . implode(', ', $this->insertFields) . ') VALUES (' . implode(', ', $placeholders) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific implementation of UpdateQuery.
|
||||||
|
*
|
||||||
|
* SQLite counts all the rows that match the conditions as modified, even if they
|
||||||
|
* will not be affected by the query. We workaround this by ensuring that
|
||||||
|
* we don't select those rows.
|
||||||
|
*
|
||||||
|
* A query like this one:
|
||||||
|
* UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1
|
||||||
|
* will become:
|
||||||
|
* UPDATE test SET col1 = 'newcol1', col2 = 'newcol2' WHERE tid = 1 AND (col1 <> 'newcol1' OR col2 <> 'newcol2')
|
||||||
|
*/
|
||||||
|
class UpdateQuery_sqlite extends UpdateQuery {
|
||||||
|
public function execute() {
|
||||||
|
if (!empty($this->queryOptions['sqlite_return_matched_rows'])) {
|
||||||
|
return parent::execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the fields used in the update query.
|
||||||
|
$fields = $this->expressionFields + $this->fields;
|
||||||
|
|
||||||
|
// Add the inverse of the fields to the condition.
|
||||||
|
$condition = new DatabaseCondition('OR');
|
||||||
|
foreach ($fields as $field => $data) {
|
||||||
|
if (is_array($data)) {
|
||||||
|
// The field is an expression.
|
||||||
|
$condition->where($field . ' <> ' . $data['expression']);
|
||||||
|
$condition->isNull($field);
|
||||||
|
}
|
||||||
|
elseif (!isset($data)) {
|
||||||
|
// The field will be set to NULL.
|
||||||
|
$condition->isNotNull($field);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$condition->condition($field, $data, '<>');
|
||||||
|
$condition->isNull($field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count($condition)) {
|
||||||
|
$condition->compile($this->connection, $this);
|
||||||
|
$this->condition->where((string) $condition, $condition->arguments());
|
||||||
|
}
|
||||||
|
return parent::execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific implementation of DeleteQuery.
|
||||||
|
*/
|
||||||
|
class DeleteQuery_sqlite extends DeleteQuery {
|
||||||
|
public function execute() {
|
||||||
|
// When the WHERE is omitted from a DELETE statement and the table being
|
||||||
|
// deleted has no triggers, SQLite uses an optimization to erase the entire
|
||||||
|
// table content without having to visit each row of the table individually.
|
||||||
|
// Prior to SQLite 3.6.5, SQLite does not return the actual number of rows
|
||||||
|
// deleted by that optimized "truncate" optimization. But we want to return
|
||||||
|
// the number of rows affected, so we calculate it directly.
|
||||||
|
if (!count($this->condition)) {
|
||||||
|
$total_rows = $this->connection->query('SELECT COUNT(*) FROM {' . $this->connection->escapeTable($this->table) . '}')->fetchField();
|
||||||
|
parent::execute();
|
||||||
|
return $total_rows;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return parent::execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific implementation of TruncateQuery.
|
||||||
|
*
|
||||||
|
* SQLite doesn't support TRUNCATE, but a DELETE query with no condition has
|
||||||
|
* exactly the effect (it is implemented by DROPing the table).
|
||||||
|
*/
|
||||||
|
class TruncateQuery_sqlite extends TruncateQuery {
|
||||||
|
public function __toString() {
|
||||||
|
// Create a sanitized comment string to prepend to the query.
|
||||||
|
$comments = $this->connection->makeComment($this->comments);
|
||||||
|
|
||||||
|
return $comments . 'DELETE FROM {' . $this->connection->escapeTable($this->table) . '} ';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
683
includes/database/sqlite/schema.inc
Normal file
683
includes/database/sqlite/schema.inc
Normal file
|
@ -0,0 +1,683 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Database schema code for SQLite databases.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ingroup schemaapi
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
class DatabaseSchema_sqlite extends DatabaseSchema {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override DatabaseSchema::$defaultSchema
|
||||||
|
*/
|
||||||
|
protected $defaultSchema = 'main';
|
||||||
|
|
||||||
|
public function tableExists($table) {
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
|
||||||
|
// Don't use {} around sqlite_master table.
|
||||||
|
return (bool) $this->connection->query('SELECT 1 FROM ' . $info['schema'] . '.sqlite_master WHERE type = :type AND name = :name', array(':type' => 'table', ':name' => $info['table']))->fetchField();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldExists($table, $column) {
|
||||||
|
$schema = $this->introspectSchema($table);
|
||||||
|
return !empty($schema['fields'][$column]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SQL to create a new table from a Drupal schema definition.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the table to create.
|
||||||
|
* @param $table
|
||||||
|
* A Schema API table definition array.
|
||||||
|
* @return
|
||||||
|
* An array of SQL statements to create the table.
|
||||||
|
*/
|
||||||
|
public function createTableSql($name, $table) {
|
||||||
|
$sql = array();
|
||||||
|
$sql[] = "CREATE TABLE {" . $name . "} (\n" . $this->createColumsSql($name, $table) . "\n);\n";
|
||||||
|
return array_merge($sql, $this->createIndexSql($name, $table));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the SQL expression for indexes.
|
||||||
|
*/
|
||||||
|
protected function createIndexSql($tablename, $schema) {
|
||||||
|
$sql = array();
|
||||||
|
$info = $this->getPrefixInfo($tablename);
|
||||||
|
if (!empty($schema['unique keys'])) {
|
||||||
|
foreach ($schema['unique keys'] as $key => $fields) {
|
||||||
|
$sql[] = 'CREATE UNIQUE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($schema['indexes'])) {
|
||||||
|
foreach ($schema['indexes'] as $key => $fields) {
|
||||||
|
$sql[] = 'CREATE INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $key . ' ON ' . $info['table'] . ' (' . $this->createKeySql($fields) . "); \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the SQL expression for creating columns.
|
||||||
|
*/
|
||||||
|
protected function createColumsSql($tablename, $schema) {
|
||||||
|
$sql_array = array();
|
||||||
|
|
||||||
|
// Add the SQL statement for each field.
|
||||||
|
foreach ($schema['fields'] as $name => $field) {
|
||||||
|
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||||
|
if (isset($schema['primary key']) && ($key = array_search($name, $schema['primary key'])) !== FALSE) {
|
||||||
|
unset($schema['primary key'][$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$sql_array[] = $this->createFieldSql($name, $this->processField($field));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process keys.
|
||||||
|
if (!empty($schema['primary key'])) {
|
||||||
|
$sql_array[] = " PRIMARY KEY (" . $this->createKeySql($schema['primary key']) . ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(", \n", $sql_array);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the SQL expression for keys.
|
||||||
|
*/
|
||||||
|
protected function createKeySql($fields) {
|
||||||
|
$return = array();
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
if (is_array($field)) {
|
||||||
|
$return[] = $field[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = $field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return implode(', ', $return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set database-engine specific properties for a field.
|
||||||
|
*
|
||||||
|
* @param $field
|
||||||
|
* A field description array, as specified in the schema documentation.
|
||||||
|
*/
|
||||||
|
protected function processField($field) {
|
||||||
|
if (!isset($field['size'])) {
|
||||||
|
$field['size'] = 'normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the correct database-engine specific datatype.
|
||||||
|
// In case one is already provided, force it to uppercase.
|
||||||
|
if (isset($field['sqlite_type'])) {
|
||||||
|
$field['sqlite_type'] = drupal_strtoupper($field['sqlite_type']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$map = $this->getFieldTypeMap();
|
||||||
|
$field['sqlite_type'] = $map[$field['type'] . ':' . $field['size']];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($field['type']) && $field['type'] == 'serial') {
|
||||||
|
$field['auto_increment'] = TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an SQL string for a field to be used in table creation or alteration.
|
||||||
|
*
|
||||||
|
* Before passing a field out of a schema definition into this function it has
|
||||||
|
* to be processed by db_processField().
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* Name of the field.
|
||||||
|
* @param $spec
|
||||||
|
* The field specification, as per the schema data structure format.
|
||||||
|
*/
|
||||||
|
protected function createFieldSql($name, $spec) {
|
||||||
|
if (!empty($spec['auto_increment'])) {
|
||||||
|
$sql = $name . " INTEGER PRIMARY KEY AUTOINCREMENT";
|
||||||
|
if (!empty($spec['unsigned'])) {
|
||||||
|
$sql .= ' CHECK (' . $name . '>= 0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sql = $name . ' ' . $spec['sqlite_type'];
|
||||||
|
|
||||||
|
if (in_array($spec['sqlite_type'], array('VARCHAR', 'TEXT')) && isset($spec['length'])) {
|
||||||
|
$sql .= '(' . $spec['length'] . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($spec['not null'])) {
|
||||||
|
if ($spec['not null']) {
|
||||||
|
$sql .= ' NOT NULL';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sql .= ' NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($spec['unsigned'])) {
|
||||||
|
$sql .= ' CHECK (' . $name . '>= 0)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($spec['default'])) {
|
||||||
|
if (is_string($spec['default'])) {
|
||||||
|
$spec['default'] = "'" . $spec['default'] . "'";
|
||||||
|
}
|
||||||
|
$sql .= ' DEFAULT ' . $spec['default'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($spec['not null']) && !isset($spec['default'])) {
|
||||||
|
$sql .= ' DEFAULT NULL';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This maps a generic data type in combination with its data size
|
||||||
|
* to the engine-specific data type.
|
||||||
|
*/
|
||||||
|
public function getFieldTypeMap() {
|
||||||
|
// Put :normal last so it gets preserved by array_flip. This makes
|
||||||
|
// it much easier for modules (such as schema.module) to map
|
||||||
|
// database types back into schema types.
|
||||||
|
// $map does not use drupal_static as its value never changes.
|
||||||
|
static $map = array(
|
||||||
|
'varchar:normal' => 'VARCHAR',
|
||||||
|
'char:normal' => 'CHAR',
|
||||||
|
|
||||||
|
'text:tiny' => 'TEXT',
|
||||||
|
'text:small' => 'TEXT',
|
||||||
|
'text:medium' => 'TEXT',
|
||||||
|
'text:big' => 'TEXT',
|
||||||
|
'text:normal' => 'TEXT',
|
||||||
|
|
||||||
|
'serial:tiny' => 'INTEGER',
|
||||||
|
'serial:small' => 'INTEGER',
|
||||||
|
'serial:medium' => 'INTEGER',
|
||||||
|
'serial:big' => 'INTEGER',
|
||||||
|
'serial:normal' => 'INTEGER',
|
||||||
|
|
||||||
|
'int:tiny' => 'INTEGER',
|
||||||
|
'int:small' => 'INTEGER',
|
||||||
|
'int:medium' => 'INTEGER',
|
||||||
|
'int:big' => 'INTEGER',
|
||||||
|
'int:normal' => 'INTEGER',
|
||||||
|
|
||||||
|
'float:tiny' => 'FLOAT',
|
||||||
|
'float:small' => 'FLOAT',
|
||||||
|
'float:medium' => 'FLOAT',
|
||||||
|
'float:big' => 'FLOAT',
|
||||||
|
'float:normal' => 'FLOAT',
|
||||||
|
|
||||||
|
'numeric:normal' => 'NUMERIC',
|
||||||
|
|
||||||
|
'blob:big' => 'BLOB',
|
||||||
|
'blob:normal' => 'BLOB',
|
||||||
|
);
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renameTable($table, $new_name) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot rename @table to @table_new: table @table doesn't exist.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
if ($this->tableExists($new_name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename @table to @table_new: table @table_new already exists.", array('@table' => $table, '@table_new' => $new_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema = $this->introspectSchema($table);
|
||||||
|
|
||||||
|
// SQLite doesn't allow you to rename tables outside of the current
|
||||||
|
// database. So the syntax '...RENAME TO database.table' would fail.
|
||||||
|
// So we must determine the full table name here rather than surrounding
|
||||||
|
// the table with curly braces incase the db_prefix contains a reference
|
||||||
|
// to a database outside of our existing database.
|
||||||
|
$info = $this->getPrefixInfo($new_name);
|
||||||
|
$this->connection->query('ALTER TABLE {' . $table . '} RENAME TO ' . $info['table']);
|
||||||
|
|
||||||
|
// Drop the indexes, there is no RENAME INDEX command in SQLite.
|
||||||
|
if (!empty($schema['unique keys'])) {
|
||||||
|
foreach ($schema['unique keys'] as $key => $fields) {
|
||||||
|
$this->dropIndex($table, $key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!empty($schema['indexes'])) {
|
||||||
|
foreach ($schema['indexes'] as $index => $fields) {
|
||||||
|
$this->dropIndex($table, $index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recreate the indexes.
|
||||||
|
$statements = $this->createIndexSql($new_name, $schema);
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
$this->connection->query($statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropTable($table) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$this->connection->tableDropped = TRUE;
|
||||||
|
$this->connection->query('DROP TABLE {' . $table . '}');
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addField($table, $field, $specification, $keys_new = array()) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add field @table.@field: table doesn't exist.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
if ($this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add field @table.@field: field already exists.", array('@field' => $field, '@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite doesn't have a full-featured ALTER TABLE statement. It only
|
||||||
|
// supports adding new fields to a table, in some simple cases. In most
|
||||||
|
// cases, we have to create a new table and copy the data over.
|
||||||
|
if (empty($keys_new) && (empty($specification['not null']) || isset($specification['default']))) {
|
||||||
|
// When we don't have to create new keys and we are not creating a
|
||||||
|
// NOT NULL column without a default value, we can use the quicker version.
|
||||||
|
$query = 'ALTER TABLE {' . $table . '} ADD ' . $this->createFieldSql($field, $this->processField($specification));
|
||||||
|
$this->connection->query($query);
|
||||||
|
|
||||||
|
// Apply the initial value if set.
|
||||||
|
if (isset($specification['initial'])) {
|
||||||
|
$this->connection->update($table)
|
||||||
|
->fields(array($field => $specification['initial']))
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We cannot add the field directly. Use the slower table alteration
|
||||||
|
// method, starting from the old schema.
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
// Add the new field.
|
||||||
|
$new_schema['fields'][$field] = $specification;
|
||||||
|
|
||||||
|
// Build the mapping between the old fields and the new fields.
|
||||||
|
$mapping = array();
|
||||||
|
if (isset($specification['initial'])) {
|
||||||
|
// If we have a initial value, copy it over.
|
||||||
|
$mapping[$field] = array(
|
||||||
|
'expression' => ':newfieldinitial',
|
||||||
|
'arguments' => array(':newfieldinitial' => $specification['initial']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Else use the default of the field.
|
||||||
|
$mapping[$field] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new indexes.
|
||||||
|
$new_schema += $keys_new;
|
||||||
|
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema, $mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a table with a new schema containing the old content.
|
||||||
|
*
|
||||||
|
* As SQLite does not support ALTER TABLE (with a few exceptions) it is
|
||||||
|
* necessary to create a new table and copy over the old content.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* Name of the table to be altered.
|
||||||
|
* @param $old_schema
|
||||||
|
* The old schema array for the table.
|
||||||
|
* @param $new_schema
|
||||||
|
* The new schema array for the table.
|
||||||
|
* @param $mapping
|
||||||
|
* An optional mapping between the fields of the old specification and the
|
||||||
|
* fields of the new specification. An associative array, whose keys are
|
||||||
|
* the fields of the new table, and values can take two possible forms:
|
||||||
|
* - a simple string, which is interpreted as the name of a field of the
|
||||||
|
* old table,
|
||||||
|
* - an associative array with two keys 'expression' and 'arguments',
|
||||||
|
* that will be used as an expression field.
|
||||||
|
*/
|
||||||
|
protected function alterTable($table, $old_schema, $new_schema, array $mapping = array()) {
|
||||||
|
$i = 0;
|
||||||
|
do {
|
||||||
|
$new_table = $table . '_' . $i++;
|
||||||
|
} while ($this->tableExists($new_table));
|
||||||
|
|
||||||
|
$this->createTable($new_table, $new_schema);
|
||||||
|
|
||||||
|
// Build a SQL query to migrate the data from the old table to the new.
|
||||||
|
$select = $this->connection->select($table);
|
||||||
|
|
||||||
|
// Complete the mapping.
|
||||||
|
$possible_keys = array_keys($new_schema['fields']);
|
||||||
|
$mapping += array_combine($possible_keys, $possible_keys);
|
||||||
|
|
||||||
|
// Now add the fields.
|
||||||
|
foreach ($mapping as $field_alias => $field_source) {
|
||||||
|
// Just ignore this field (ie. use it's default value).
|
||||||
|
if (!isset($field_source)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($field_source)) {
|
||||||
|
$select->addExpression($field_source['expression'], $field_alias, $field_source['arguments']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$select->addField($table, $field_source, $field_alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute the data migration query.
|
||||||
|
$this->connection->insert($new_table)
|
||||||
|
->from($select)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
$old_count = $this->connection->query('SELECT COUNT(*) FROM {' . $table . '}')->fetchField();
|
||||||
|
$new_count = $this->connection->query('SELECT COUNT(*) FROM {' . $new_table . '}')->fetchField();
|
||||||
|
if ($old_count == $new_count) {
|
||||||
|
$this->dropTable($table);
|
||||||
|
$this->renameTable($new_table, $table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find out the schema of a table.
|
||||||
|
*
|
||||||
|
* This function uses introspection methods provided by the database to
|
||||||
|
* create a schema array. This is useful, for example, during update when
|
||||||
|
* the old schema is not available.
|
||||||
|
*
|
||||||
|
* @param $table
|
||||||
|
* Name of the table.
|
||||||
|
* @return
|
||||||
|
* An array representing the schema, from drupal_get_schema().
|
||||||
|
* @see drupal_get_schema()
|
||||||
|
*/
|
||||||
|
protected function introspectSchema($table) {
|
||||||
|
$mapped_fields = array_flip($this->getFieldTypeMap());
|
||||||
|
$schema = array(
|
||||||
|
'fields' => array(),
|
||||||
|
'primary key' => array(),
|
||||||
|
'unique keys' => array(),
|
||||||
|
'indexes' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.table_info(' . $info['table'] . ')');
|
||||||
|
foreach ($result as $row) {
|
||||||
|
if (preg_match('/^([^(]+)\((.*)\)$/', $row->type, $matches)) {
|
||||||
|
$type = $matches[1];
|
||||||
|
$length = $matches[2];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$type = $row->type;
|
||||||
|
$length = NULL;
|
||||||
|
}
|
||||||
|
if (isset($mapped_fields[$type])) {
|
||||||
|
list($type, $size) = explode(':', $mapped_fields[$type]);
|
||||||
|
$schema['fields'][$row->name] = array(
|
||||||
|
'type' => $type,
|
||||||
|
'size' => $size,
|
||||||
|
'not null' => !empty($row->notnull),
|
||||||
|
'default' => trim($row->dflt_value, "'"),
|
||||||
|
);
|
||||||
|
if ($length) {
|
||||||
|
$schema['fields'][$row->name]['length'] = $length;
|
||||||
|
}
|
||||||
|
if ($row->pk) {
|
||||||
|
$schema['primary key'][] = $row->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
new Exception("Unable to parse the column type " . $row->type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$indexes = array();
|
||||||
|
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_list(' . $info['table'] . ')');
|
||||||
|
foreach ($result as $row) {
|
||||||
|
if (strpos($row->name, 'sqlite_autoindex_') !== 0) {
|
||||||
|
$indexes[] = array(
|
||||||
|
'schema_key' => $row->unique ? 'unique keys' : 'indexes',
|
||||||
|
'name' => $row->name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($indexes as $index) {
|
||||||
|
$name = $index['name'];
|
||||||
|
// Get index name without prefix.
|
||||||
|
$index_name = substr($name, strlen($info['table']) + 1);
|
||||||
|
$result = $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $name . ')');
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$schema[$index['schema_key']][$index_name][] = $row->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropField($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
unset($new_schema['fields'][$field]);
|
||||||
|
foreach ($new_schema['indexes'] as $index => $fields) {
|
||||||
|
foreach ($fields as $key => $field_name) {
|
||||||
|
if ($field_name == $field) {
|
||||||
|
unset($new_schema['indexes'][$index][$key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If this index has no more fields then remove it.
|
||||||
|
if (empty($new_schema['indexes'][$index])) {
|
||||||
|
unset($new_schema['indexes'][$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changeField($table, $field, $field_new, $spec, $keys_new = array()) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot change the definition of field @table.@name: field doesn't exist.", array('@table' => $table, '@name' => $field)));
|
||||||
|
}
|
||||||
|
if (($field != $field_new) && $this->fieldExists($table, $field_new)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot rename field @table.@name to @name_new: target field already exists.", array('@table' => $table, '@name' => $field, '@name_new' => $field_new)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
// Map the old field to the new field.
|
||||||
|
if ($field != $field_new) {
|
||||||
|
$mapping[$field_new] = $field;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$mapping = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the previous definition and swap in the new one.
|
||||||
|
unset($new_schema['fields'][$field]);
|
||||||
|
$new_schema['fields'][$field_new] = $spec;
|
||||||
|
|
||||||
|
// Map the former indexes to the new column name.
|
||||||
|
$new_schema['primary key'] = $this->mapKeyDefinition($new_schema['primary key'], $mapping);
|
||||||
|
foreach (array('unique keys', 'indexes') as $k) {
|
||||||
|
foreach ($new_schema[$k] as &$key_definition) {
|
||||||
|
$key_definition = $this->mapKeyDefinition($key_definition, $mapping);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in the keys from $keys_new.
|
||||||
|
if (isset($keys_new['primary key'])) {
|
||||||
|
$new_schema['primary key'] = $keys_new['primary key'];
|
||||||
|
}
|
||||||
|
foreach (array('unique keys', 'indexes') as $k) {
|
||||||
|
if (!empty($keys_new[$k])) {
|
||||||
|
$new_schema[$k] = $keys_new[$k] + $new_schema[$k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema, $mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method: rename columns in an index definition according to a new mapping.
|
||||||
|
*
|
||||||
|
* @param $key_definition
|
||||||
|
* The key definition.
|
||||||
|
* @param $mapping
|
||||||
|
* The new mapping.
|
||||||
|
*/
|
||||||
|
protected function mapKeyDefinition(array $key_definition, array $mapping) {
|
||||||
|
foreach ($key_definition as &$field) {
|
||||||
|
// The key definition can be an array($field, $length).
|
||||||
|
if (is_array($field)) {
|
||||||
|
$field = &$field[0];
|
||||||
|
}
|
||||||
|
if (isset($mapping[$field])) {
|
||||||
|
$field = $mapping[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $key_definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addIndex($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add index @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, $name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add index @name to table @table: index already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema['indexes'][$name] = $fields;
|
||||||
|
$statements = $this->createIndexSql($table, $schema);
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
$this->connection->query($statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function indexExists($table, $name) {
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
|
||||||
|
return $this->connection->query('PRAGMA ' . $info['schema'] . '.index_info(' . $info['table'] . '_' . $name . ')')->fetchField() != '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropIndex($table, $name) {
|
||||||
|
if (!$this->indexExists($table, $name)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
|
||||||
|
$this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUniqueKey($table, $name, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add unique key @name to table @table: table doesn't exist.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
if ($this->indexExists($table, $name)) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add unique key @name to table @table: unique key already exists.", array('@table' => $table, '@name' => $name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$schema['unique keys'][$name] = $fields;
|
||||||
|
$statements = $this->createIndexSql($table, $schema);
|
||||||
|
foreach ($statements as $statement) {
|
||||||
|
$this->connection->query($statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropUniqueKey($table, $name) {
|
||||||
|
if (!$this->indexExists($table, $name)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = $this->getPrefixInfo($table);
|
||||||
|
|
||||||
|
$this->connection->query('DROP INDEX ' . $info['schema'] . '.' . $info['table'] . '_' . $name);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPrimaryKey($table, $fields) {
|
||||||
|
if (!$this->tableExists($table)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot add primary key to table @table: table doesn't exist.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
if (!empty($new_schema['primary key'])) {
|
||||||
|
throw new DatabaseSchemaObjectExistsException(t("Cannot add primary key to table @table: primary key already exists.", array('@table' => $table)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_schema['primary key'] = $fields;
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dropPrimaryKey($table) {
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
if (empty($new_schema['primary key'])) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
unset($new_schema['primary key']);
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema);
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetDefault($table, $field, $default) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot set default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
$new_schema['fields'][$field]['default'] = $default;
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fieldSetNoDefault($table, $field) {
|
||||||
|
if (!$this->fieldExists($table, $field)) {
|
||||||
|
throw new DatabaseSchemaObjectDoesNotExistException(t("Cannot remove default value of field @table.@field: field doesn't exist.", array('@table' => $table, '@field' => $field)));
|
||||||
|
}
|
||||||
|
|
||||||
|
$old_schema = $this->introspectSchema($table);
|
||||||
|
$new_schema = $old_schema;
|
||||||
|
|
||||||
|
unset($new_schema['fields'][$field]['default']);
|
||||||
|
$this->alterTable($table, $old_schema, $new_schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findTables($table_expression) {
|
||||||
|
// Don't add the prefix, $table_expression already includes the prefix.
|
||||||
|
$info = $this->getPrefixInfo($table_expression, FALSE);
|
||||||
|
|
||||||
|
// Can't use query placeholders for the schema because the query would have
|
||||||
|
// to be :prefixsqlite_master, which does not work.
|
||||||
|
$result = db_query("SELECT name FROM " . $info['schema'] . ".sqlite_master WHERE type = :type AND name LIKE :table_name", array(
|
||||||
|
':type' => 'table',
|
||||||
|
':table_name' => $info['table'],
|
||||||
|
));
|
||||||
|
return $result->fetchAllKeyed(0, 0);
|
||||||
|
}
|
||||||
|
}
|
27
includes/database/sqlite/select.inc
Normal file
27
includes/database/sqlite/select.inc
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Select builder for SQLite embedded database engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @addtogroup database
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SQLite specific query builder for SELECT statements.
|
||||||
|
*/
|
||||||
|
class SelectQuery_sqlite extends SelectQuery {
|
||||||
|
public function forUpdate($set = TRUE) {
|
||||||
|
// SQLite does not support FOR UPDATE so nothing to do.
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "addtogroup database".
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
196
includes/date.inc
Normal file
196
includes/date.inc
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Initializes the list of date formats and their locales.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default system list of date formats for system_date_formats().
|
||||||
|
*/
|
||||||
|
function system_default_date_formats() {
|
||||||
|
$formats = array();
|
||||||
|
|
||||||
|
// Short date formats.
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'm/d/Y - H:i',
|
||||||
|
'locales' => array('en-us'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'd/m/Y - H:i',
|
||||||
|
'locales' => array('en-gb', 'en-hk', 'en-ie', 'el-gr', 'es-es', 'fr-be', 'fr-fr', 'fr-lu', 'it-it', 'nl-be', 'pt-pt'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'Y/m/d - H:i',
|
||||||
|
'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'd.m.Y - H:i',
|
||||||
|
'locales' => array('de-ch', 'de-de', 'de-lu', 'fi-fi', 'fr-ch', 'is-is', 'pl-pl', 'ro-ro', 'ru-ru'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'Y-m-d H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'm/d/Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'd/m/Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'Y/m/d - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'M j Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'j M Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'Y M j - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'M j Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'j M Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'short',
|
||||||
|
'format' => 'Y M j - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Medium date formats.
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, m/d/Y - H:i',
|
||||||
|
'locales' => array('en-us'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, d/m/Y - H:i',
|
||||||
|
'locales' => array('en-gb', 'en-hk', 'en-ie', 'el-gr', 'es-es', 'fr-be', 'fr-fr', 'fr-lu', 'it-it', 'nl-be', 'pt-pt'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, Y/m/d - H:i',
|
||||||
|
'locales' => array('en-ca', 'fr-ca', 'no-no', 'sv-se'),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, Y-m-d H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'F j, Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'j F, Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'Y, F j - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, m/d/Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, d/m/Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'D, Y/m/d - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'F j, Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'j F Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'Y, F j - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'medium',
|
||||||
|
'format' => 'j. F Y - G:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Long date formats.
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, F j, Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, j F, Y - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, Y, F j - H:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, F j, Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, j F Y - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, Y, F j - g:ia',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
$formats[] = array(
|
||||||
|
'type' => 'long',
|
||||||
|
'format' => 'l, j. F Y - G:i',
|
||||||
|
'locales' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $formats;
|
||||||
|
}
|
1436
includes/entity.inc
Normal file
1436
includes/entity.inc
Normal file
File diff suppressed because it is too large
Load diff
295
includes/errors.inc
Normal file
295
includes/errors.inc
Normal file
|
@ -0,0 +1,295 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Functions for error handling.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps PHP error constants to watchdog severity levels.
|
||||||
|
*
|
||||||
|
* The error constants are documented at
|
||||||
|
* http://php.net/manual/errorfunc.constants.php
|
||||||
|
*
|
||||||
|
* @ingroup logging_severity_levels
|
||||||
|
*/
|
||||||
|
function drupal_error_levels() {
|
||||||
|
$types = array(
|
||||||
|
E_ERROR => array('Error', WATCHDOG_ERROR),
|
||||||
|
E_WARNING => array('Warning', WATCHDOG_WARNING),
|
||||||
|
E_PARSE => array('Parse error', WATCHDOG_ERROR),
|
||||||
|
E_NOTICE => array('Notice', WATCHDOG_NOTICE),
|
||||||
|
E_CORE_ERROR => array('Core error', WATCHDOG_ERROR),
|
||||||
|
E_CORE_WARNING => array('Core warning', WATCHDOG_WARNING),
|
||||||
|
E_COMPILE_ERROR => array('Compile error', WATCHDOG_ERROR),
|
||||||
|
E_COMPILE_WARNING => array('Compile warning', WATCHDOG_WARNING),
|
||||||
|
E_USER_ERROR => array('User error', WATCHDOG_ERROR),
|
||||||
|
E_USER_WARNING => array('User warning', WATCHDOG_WARNING),
|
||||||
|
E_USER_NOTICE => array('User notice', WATCHDOG_NOTICE),
|
||||||
|
E_STRICT => array('Strict warning', WATCHDOG_DEBUG),
|
||||||
|
E_RECOVERABLE_ERROR => array('Recoverable fatal error', WATCHDOG_ERROR),
|
||||||
|
);
|
||||||
|
// E_DEPRECATED and E_USER_DEPRECATED were added in PHP 5.3.0.
|
||||||
|
if (defined('E_DEPRECATED')) {
|
||||||
|
$types[E_DEPRECATED] = array('Deprecated function', WATCHDOG_DEBUG);
|
||||||
|
$types[E_USER_DEPRECATED] = array('User deprecated function', WATCHDOG_DEBUG);
|
||||||
|
}
|
||||||
|
return $types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides custom PHP error handling.
|
||||||
|
*
|
||||||
|
* @param $error_level
|
||||||
|
* The level of the error raised.
|
||||||
|
* @param $message
|
||||||
|
* The error message.
|
||||||
|
* @param $filename
|
||||||
|
* The filename that the error was raised in.
|
||||||
|
* @param $line
|
||||||
|
* The line number the error was raised at.
|
||||||
|
* @param $context
|
||||||
|
* An array that points to the active symbol table at the point the error
|
||||||
|
* occurred.
|
||||||
|
*/
|
||||||
|
function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) {
|
||||||
|
if ($error_level & error_reporting()) {
|
||||||
|
$types = drupal_error_levels();
|
||||||
|
list($severity_msg, $severity_level) = $types[$error_level];
|
||||||
|
$caller = _drupal_get_last_caller(debug_backtrace());
|
||||||
|
|
||||||
|
if (!function_exists('filter_xss_admin')) {
|
||||||
|
require_once DRUPAL_ROOT . '/includes/common.inc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// We treat recoverable errors as fatal.
|
||||||
|
_drupal_log_error(array(
|
||||||
|
'%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
|
||||||
|
// The standard PHP error handler considers that the error messages
|
||||||
|
// are HTML. We mimic this behavior here.
|
||||||
|
'!message' => filter_xss_admin($message),
|
||||||
|
'%function' => $caller['function'],
|
||||||
|
'%file' => $caller['file'],
|
||||||
|
'%line' => $caller['line'],
|
||||||
|
'severity_level' => $severity_level,
|
||||||
|
), $error_level == E_RECOVERABLE_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes an exception and retrieves the correct caller.
|
||||||
|
*
|
||||||
|
* @param $exception
|
||||||
|
* The exception object that was thrown.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An error in the format expected by _drupal_log_error().
|
||||||
|
*/
|
||||||
|
function _drupal_decode_exception($exception) {
|
||||||
|
$message = $exception->getMessage();
|
||||||
|
|
||||||
|
$backtrace = $exception->getTrace();
|
||||||
|
// Add the line throwing the exception to the backtrace.
|
||||||
|
array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile()));
|
||||||
|
|
||||||
|
// For PDOException errors, we try to return the initial caller,
|
||||||
|
// skipping internal functions of the database layer.
|
||||||
|
if ($exception instanceof PDOException) {
|
||||||
|
// The first element in the stack is the call, the second element gives us the caller.
|
||||||
|
// We skip calls that occurred in one of the classes of the database layer
|
||||||
|
// or in one of its global functions.
|
||||||
|
$db_functions = array('db_query', 'db_query_range');
|
||||||
|
while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
|
||||||
|
((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
|
||||||
|
in_array($caller['function'], $db_functions))) {
|
||||||
|
// We remove that call.
|
||||||
|
array_shift($backtrace);
|
||||||
|
}
|
||||||
|
if (isset($exception->query_string, $exception->args)) {
|
||||||
|
$message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$caller = _drupal_get_last_caller($backtrace);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'%type' => get_class($exception),
|
||||||
|
// The standard PHP exception handler considers that the exception message
|
||||||
|
// is plain-text. We mimic this behavior here.
|
||||||
|
'!message' => check_plain($message),
|
||||||
|
'%function' => $caller['function'],
|
||||||
|
'%file' => $caller['file'],
|
||||||
|
'%line' => $caller['line'],
|
||||||
|
'severity_level' => WATCHDOG_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an exception error message without further exceptions.
|
||||||
|
*
|
||||||
|
* @param $exception
|
||||||
|
* The exception object that was thrown.
|
||||||
|
* @return
|
||||||
|
* An error message.
|
||||||
|
*/
|
||||||
|
function _drupal_render_exception_safe($exception) {
|
||||||
|
return check_plain(strtr('%type: !message in %function (line %line of %file).', _drupal_decode_exception($exception)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether an error should be displayed.
|
||||||
|
*
|
||||||
|
* When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
|
||||||
|
* all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
|
||||||
|
* will be examined to determine if it should be displayed.
|
||||||
|
*
|
||||||
|
* @param $error
|
||||||
|
* Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if an error should be displayed.
|
||||||
|
*/
|
||||||
|
function error_displayable($error = NULL) {
|
||||||
|
$error_level = variable_get('error_level', ERROR_REPORTING_DISPLAY_ALL);
|
||||||
|
$updating = (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE == 'update');
|
||||||
|
$all_errors_displayed = ($error_level == ERROR_REPORTING_DISPLAY_ALL);
|
||||||
|
$error_needs_display = ($error_level == ERROR_REPORTING_DISPLAY_SOME &&
|
||||||
|
isset($error) && $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning');
|
||||||
|
|
||||||
|
return ($updating || $all_errors_displayed || $error_needs_display);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs a PHP error or exception and displays an error page in fatal cases.
|
||||||
|
*
|
||||||
|
* @param $error
|
||||||
|
* An array with the following keys: %type, !message, %function, %file, %line
|
||||||
|
* and severity_level. All the parameters are plain-text, with the exception
|
||||||
|
* of !message, which needs to be a safe HTML string.
|
||||||
|
* @param $fatal
|
||||||
|
* TRUE if the error is fatal.
|
||||||
|
*/
|
||||||
|
function _drupal_log_error($error, $fatal = FALSE) {
|
||||||
|
// Initialize a maintenance theme if the bootstrap was not complete.
|
||||||
|
// Do it early because drupal_set_message() triggers a drupal_theme_initialize().
|
||||||
|
if ($fatal && (drupal_get_bootstrap_phase() != DRUPAL_BOOTSTRAP_FULL)) {
|
||||||
|
unset($GLOBALS['theme']);
|
||||||
|
if (!defined('MAINTENANCE_MODE')) {
|
||||||
|
define('MAINTENANCE_MODE', 'error');
|
||||||
|
}
|
||||||
|
drupal_maintenance_theme();
|
||||||
|
}
|
||||||
|
|
||||||
|
// When running inside the testing framework, we relay the errors
|
||||||
|
// to the tested site by the way of HTTP headers.
|
||||||
|
$test_info = &$GLOBALS['drupal_test_info'];
|
||||||
|
if (!empty($test_info['in_child_site']) && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
|
||||||
|
// $number does not use drupal_static as it should not be reset
|
||||||
|
// as it uniquely identifies each PHP error.
|
||||||
|
static $number = 0;
|
||||||
|
$assertion = array(
|
||||||
|
$error['!message'],
|
||||||
|
$error['%type'],
|
||||||
|
array(
|
||||||
|
'function' => $error['%function'],
|
||||||
|
'file' => $error['%file'],
|
||||||
|
'line' => $error['%line'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
|
||||||
|
$number++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the error immediately, unless this is a non-fatal error which has been
|
||||||
|
// triggered via drupal_trigger_error_with_delayed_logging(); in that case
|
||||||
|
// trigger it in a shutdown function. Fatal errors are always triggered
|
||||||
|
// immediately since for a fatal error the page request will end here anyway.
|
||||||
|
if (!$fatal && drupal_static('_drupal_trigger_error_with_delayed_logging')) {
|
||||||
|
drupal_register_shutdown_function('watchdog', 'php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
watchdog('php', '%type: !message in %function (line %line of %file).', $error, $error['severity_level']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fatal) {
|
||||||
|
drupal_add_http_header('Status', '500 Service unavailable (with message)');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drupal_is_cli()) {
|
||||||
|
if ($fatal) {
|
||||||
|
// When called from CLI, simply output a plain text message.
|
||||||
|
print html_entity_decode(strip_tags(t('%type: !message in %function (line %line of %file).', $error))). "\n";
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest') {
|
||||||
|
if ($fatal) {
|
||||||
|
if (error_displayable($error)) {
|
||||||
|
// When called from JavaScript, simply output the error message.
|
||||||
|
print t('%type: !message in %function (line %line of %file).', $error);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Display the message if the current error reporting level allows this type
|
||||||
|
// of message to be displayed, and unconditionally in update.php.
|
||||||
|
if (error_displayable($error)) {
|
||||||
|
$class = 'error';
|
||||||
|
|
||||||
|
// If error type is 'User notice' then treat it as debug information
|
||||||
|
// instead of an error message, see dd().
|
||||||
|
if ($error['%type'] == 'User notice') {
|
||||||
|
$error['%type'] = 'Debug';
|
||||||
|
$class = 'status';
|
||||||
|
}
|
||||||
|
|
||||||
|
drupal_set_message(t('%type: !message in %function (line %line of %file).', $error), $class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($fatal) {
|
||||||
|
drupal_set_title(t('Error'));
|
||||||
|
// We fallback to a maintenance page at this point, because the page generation
|
||||||
|
// itself can generate errors.
|
||||||
|
print theme('maintenance_page', array('content' => t('The website encountered an unexpected error. Please try again later.')));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last caller from a backtrace.
|
||||||
|
*
|
||||||
|
* @param $backtrace
|
||||||
|
* A standard PHP backtrace.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array with keys 'file', 'line' and 'function'.
|
||||||
|
*/
|
||||||
|
function _drupal_get_last_caller($backtrace) {
|
||||||
|
// Errors that occur inside PHP internal functions do not generate
|
||||||
|
// information about file and line. Ignore black listed functions.
|
||||||
|
$blacklist = array('debug', '_drupal_error_handler', '_drupal_exception_handler');
|
||||||
|
while (($backtrace && !isset($backtrace[0]['line'])) ||
|
||||||
|
(isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], $blacklist))) {
|
||||||
|
array_shift($backtrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first trace is the call itself.
|
||||||
|
// It gives us the line and the file of the last call.
|
||||||
|
$call = $backtrace[0];
|
||||||
|
|
||||||
|
// The second call give us the function where the call originated.
|
||||||
|
if (isset($backtrace[1])) {
|
||||||
|
if (isset($backtrace[1]['class'])) {
|
||||||
|
$call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$call['function'] = $backtrace[1]['function'] . '()';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$call['function'] = 'main()';
|
||||||
|
}
|
||||||
|
return $call;
|
||||||
|
}
|
2591
includes/file.inc
Normal file
2591
includes/file.inc
Normal file
File diff suppressed because it is too large
Load diff
879
includes/file.mimetypes.inc
Normal file
879
includes/file.mimetypes.inc
Normal file
|
@ -0,0 +1,879 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides mimetype mappings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of MIME extension mappings.
|
||||||
|
*
|
||||||
|
* Returns the mapping after modules have altered the default mapping.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Array of mimetypes correlated to the extensions that relate to them.
|
||||||
|
*
|
||||||
|
* @see file_get_mimetype()
|
||||||
|
*/
|
||||||
|
function file_mimetype_mapping() {
|
||||||
|
$mapping = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($mapping)) {
|
||||||
|
$mapping = file_default_mimetype_mapping();
|
||||||
|
// Allow modules to alter the default mapping.
|
||||||
|
drupal_alter('file_mimetype_mapping', $mapping);
|
||||||
|
}
|
||||||
|
return $mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default MIME extension mapping.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Array of mimetypes correlated to the extensions that relate to them.
|
||||||
|
*
|
||||||
|
* @see file_get_mimetype()
|
||||||
|
*/
|
||||||
|
function file_default_mimetype_mapping() {
|
||||||
|
return array(
|
||||||
|
'mimetypes' => array(
|
||||||
|
0 => 'application/andrew-inset',
|
||||||
|
1 => 'application/atom',
|
||||||
|
2 => 'application/atomcat+xml',
|
||||||
|
3 => 'application/atomserv+xml',
|
||||||
|
4 => 'application/cap',
|
||||||
|
5 => 'application/cu-seeme',
|
||||||
|
6 => 'application/dsptype',
|
||||||
|
350 => 'application/epub+zip',
|
||||||
|
7 => 'application/hta',
|
||||||
|
8 => 'application/java-archive',
|
||||||
|
9 => 'application/java-serialized-object',
|
||||||
|
10 => 'application/java-vm',
|
||||||
|
11 => 'application/mac-binhex40',
|
||||||
|
12 => 'application/mathematica',
|
||||||
|
13 => 'application/msaccess',
|
||||||
|
14 => 'application/msword',
|
||||||
|
15 => 'application/octet-stream',
|
||||||
|
16 => 'application/oda',
|
||||||
|
17 => 'application/ogg',
|
||||||
|
18 => 'application/pdf',
|
||||||
|
19 => 'application/pgp-keys',
|
||||||
|
20 => 'application/pgp-signature',
|
||||||
|
21 => 'application/pics-rules',
|
||||||
|
22 => 'application/postscript',
|
||||||
|
23 => 'application/rar',
|
||||||
|
24 => 'application/rdf+xml',
|
||||||
|
25 => 'application/rss+xml',
|
||||||
|
26 => 'application/rtf',
|
||||||
|
27 => 'application/smil',
|
||||||
|
349 => 'application/vnd.amazon.ebook',
|
||||||
|
28 => 'application/vnd.cinderella',
|
||||||
|
29 => 'application/vnd.google-earth.kml+xml',
|
||||||
|
30 => 'application/vnd.google-earth.kmz',
|
||||||
|
31 => 'application/vnd.mozilla.xul+xml',
|
||||||
|
32 => 'application/vnd.ms-excel',
|
||||||
|
33 => 'application/vnd.ms-excel.addin.macroEnabled.12',
|
||||||
|
34 => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||||
|
35 => 'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||||
|
36 => 'application/vnd.ms-excel.template.macroEnabled.12',
|
||||||
|
37 => 'application/vnd.ms-pki.seccat',
|
||||||
|
38 => 'application/vnd.ms-pki.stl',
|
||||||
|
39 => 'application/vnd.ms-powerpoint',
|
||||||
|
40 => 'application/vnd.ms-powerpoint.addin.macroEnabled.12',
|
||||||
|
41 => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||||
|
42 => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12',
|
||||||
|
43 => 'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||||
|
44 => 'application/vnd.ms-word.document.macroEnabled.12',
|
||||||
|
45 => 'application/vnd.ms-word.template.macroEnabled.12',
|
||||||
|
46 => 'application/vnd.ms-xpsdocument',
|
||||||
|
47 => 'application/vnd.oasis.opendocument.chart',
|
||||||
|
48 => 'application/vnd.oasis.opendocument.database',
|
||||||
|
49 => 'application/vnd.oasis.opendocument.formula',
|
||||||
|
50 => 'application/vnd.oasis.opendocument.graphics',
|
||||||
|
51 => 'application/vnd.oasis.opendocument.graphics-template',
|
||||||
|
52 => 'application/vnd.oasis.opendocument.image',
|
||||||
|
53 => 'application/vnd.oasis.opendocument.presentation',
|
||||||
|
54 => 'application/vnd.oasis.opendocument.presentation-template',
|
||||||
|
55 => 'application/vnd.oasis.opendocument.spreadsheet',
|
||||||
|
56 => 'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||||
|
57 => 'application/vnd.oasis.opendocument.text',
|
||||||
|
58 => 'application/vnd.oasis.opendocument.text-master',
|
||||||
|
59 => 'application/vnd.oasis.opendocument.text-template',
|
||||||
|
60 => 'application/vnd.oasis.opendocument.text-web',
|
||||||
|
61 => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||||
|
62 => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||||
|
63 => 'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||||
|
64 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
65 => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||||
|
66 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
67 => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||||
|
68 => 'application/vnd.rim.cod',
|
||||||
|
69 => 'application/vnd.smaf',
|
||||||
|
70 => 'application/vnd.stardivision.calc',
|
||||||
|
71 => 'application/vnd.stardivision.chart',
|
||||||
|
72 => 'application/vnd.stardivision.draw',
|
||||||
|
73 => 'application/vnd.stardivision.impress',
|
||||||
|
74 => 'application/vnd.stardivision.math',
|
||||||
|
75 => 'application/vnd.stardivision.writer',
|
||||||
|
76 => 'application/vnd.stardivision.writer-global',
|
||||||
|
77 => 'application/vnd.sun.xml.calc',
|
||||||
|
78 => 'application/vnd.sun.xml.calc.template',
|
||||||
|
79 => 'application/vnd.sun.xml.draw',
|
||||||
|
80 => 'application/vnd.sun.xml.draw.template',
|
||||||
|
81 => 'application/vnd.sun.xml.impress',
|
||||||
|
82 => 'application/vnd.sun.xml.impress.template',
|
||||||
|
83 => 'application/vnd.sun.xml.math',
|
||||||
|
84 => 'application/vnd.sun.xml.writer',
|
||||||
|
85 => 'application/vnd.sun.xml.writer.global',
|
||||||
|
86 => 'application/vnd.sun.xml.writer.template',
|
||||||
|
87 => 'application/vnd.symbian.install',
|
||||||
|
88 => 'application/vnd.visio',
|
||||||
|
89 => 'application/vnd.wap.wbxml',
|
||||||
|
90 => 'application/vnd.wap.wmlc',
|
||||||
|
91 => 'application/vnd.wap.wmlscriptc',
|
||||||
|
92 => 'application/wordperfect',
|
||||||
|
93 => 'application/wordperfect5.1',
|
||||||
|
94 => 'application/x-123',
|
||||||
|
95 => 'application/x-7z-compressed',
|
||||||
|
96 => 'application/x-abiword',
|
||||||
|
97 => 'application/x-apple-diskimage',
|
||||||
|
98 => 'application/x-bcpio',
|
||||||
|
99 => 'application/x-bittorrent',
|
||||||
|
100 => 'application/x-cab',
|
||||||
|
101 => 'application/x-cbr',
|
||||||
|
102 => 'application/x-cbz',
|
||||||
|
103 => 'application/x-cdf',
|
||||||
|
104 => 'application/x-cdlink',
|
||||||
|
105 => 'application/x-chess-pgn',
|
||||||
|
106 => 'application/x-cpio',
|
||||||
|
107 => 'application/x-debian-package',
|
||||||
|
108 => 'application/x-director',
|
||||||
|
109 => 'application/x-dms',
|
||||||
|
110 => 'application/x-doom',
|
||||||
|
111 => 'application/x-dvi',
|
||||||
|
112 => 'application/x-flac',
|
||||||
|
113 => 'application/x-font',
|
||||||
|
114 => 'application/x-freemind',
|
||||||
|
115 => 'application/x-futuresplash',
|
||||||
|
116 => 'application/x-gnumeric',
|
||||||
|
117 => 'application/x-go-sgf',
|
||||||
|
118 => 'application/x-graphing-calculator',
|
||||||
|
119 => 'application/x-gtar',
|
||||||
|
120 => 'application/x-hdf',
|
||||||
|
121 => 'application/x-httpd-eruby',
|
||||||
|
122 => 'application/x-httpd-php',
|
||||||
|
123 => 'application/x-httpd-php-source',
|
||||||
|
124 => 'application/x-httpd-php3',
|
||||||
|
125 => 'application/x-httpd-php3-preprocessed',
|
||||||
|
126 => 'application/x-httpd-php4',
|
||||||
|
127 => 'application/x-ica',
|
||||||
|
128 => 'application/x-internet-signup',
|
||||||
|
129 => 'application/x-iphone',
|
||||||
|
130 => 'application/x-iso9660-image',
|
||||||
|
131 => 'application/x-java-jnlp-file',
|
||||||
|
132 => 'application/x-javascript',
|
||||||
|
133 => 'application/x-jmol',
|
||||||
|
134 => 'application/x-kchart',
|
||||||
|
135 => 'application/x-killustrator',
|
||||||
|
136 => 'application/x-koan',
|
||||||
|
137 => 'application/x-kpresenter',
|
||||||
|
138 => 'application/x-kspread',
|
||||||
|
139 => 'application/x-kword',
|
||||||
|
140 => 'application/x-latex',
|
||||||
|
141 => 'application/x-lha',
|
||||||
|
142 => 'application/x-lyx',
|
||||||
|
143 => 'application/x-lzh',
|
||||||
|
144 => 'application/x-lzx',
|
||||||
|
145 => 'application/x-maker',
|
||||||
|
146 => 'application/x-mif',
|
||||||
|
351 => 'application/x-mobipocket-ebook',
|
||||||
|
352 => 'application/x-mobipocket-ebook',
|
||||||
|
147 => 'application/x-ms-wmd',
|
||||||
|
148 => 'application/x-ms-wmz',
|
||||||
|
149 => 'application/x-msdos-program',
|
||||||
|
150 => 'application/x-msi',
|
||||||
|
151 => 'application/x-netcdf',
|
||||||
|
152 => 'application/x-ns-proxy-autoconfig',
|
||||||
|
153 => 'application/x-nwc',
|
||||||
|
154 => 'application/x-object',
|
||||||
|
155 => 'application/x-oz-application',
|
||||||
|
156 => 'application/x-pkcs7-certreqresp',
|
||||||
|
157 => 'application/x-pkcs7-crl',
|
||||||
|
158 => 'application/x-python-code',
|
||||||
|
159 => 'application/x-quicktimeplayer',
|
||||||
|
160 => 'application/x-redhat-package-manager',
|
||||||
|
161 => 'application/x-shar',
|
||||||
|
162 => 'application/x-shockwave-flash',
|
||||||
|
163 => 'application/x-stuffit',
|
||||||
|
164 => 'application/x-sv4cpio',
|
||||||
|
165 => 'application/x-sv4crc',
|
||||||
|
166 => 'application/x-tar',
|
||||||
|
167 => 'application/x-tcl',
|
||||||
|
168 => 'application/x-tex-gf',
|
||||||
|
169 => 'application/x-tex-pk',
|
||||||
|
170 => 'application/x-texinfo',
|
||||||
|
171 => 'application/x-trash',
|
||||||
|
172 => 'application/x-troff',
|
||||||
|
173 => 'application/x-troff-man',
|
||||||
|
174 => 'application/x-troff-me',
|
||||||
|
175 => 'application/x-troff-ms',
|
||||||
|
176 => 'application/x-ustar',
|
||||||
|
177 => 'application/x-wais-source',
|
||||||
|
178 => 'application/x-wingz',
|
||||||
|
179 => 'application/x-x509-ca-cert',
|
||||||
|
180 => 'application/x-xcf',
|
||||||
|
181 => 'application/x-xfig',
|
||||||
|
182 => 'application/x-xpinstall',
|
||||||
|
183 => 'application/xhtml+xml',
|
||||||
|
184 => 'application/xml',
|
||||||
|
185 => 'application/zip',
|
||||||
|
186 => 'audio/basic',
|
||||||
|
187 => 'audio/midi',
|
||||||
|
346 => 'audio/mp4',
|
||||||
|
188 => 'audio/mpeg',
|
||||||
|
189 => 'audio/ogg',
|
||||||
|
190 => 'audio/prs.sid',
|
||||||
|
356 => 'audio/webm',
|
||||||
|
191 => 'audio/x-aiff',
|
||||||
|
192 => 'audio/x-gsm',
|
||||||
|
354 => 'audio/x-matroska',
|
||||||
|
193 => 'audio/x-mpegurl',
|
||||||
|
194 => 'audio/x-ms-wax',
|
||||||
|
195 => 'audio/x-ms-wma',
|
||||||
|
196 => 'audio/x-pn-realaudio',
|
||||||
|
197 => 'audio/x-realaudio',
|
||||||
|
198 => 'audio/x-scpls',
|
||||||
|
199 => 'audio/x-sd2',
|
||||||
|
200 => 'audio/x-wav',
|
||||||
|
201 => 'chemical/x-alchemy',
|
||||||
|
202 => 'chemical/x-cache',
|
||||||
|
203 => 'chemical/x-cache-csf',
|
||||||
|
204 => 'chemical/x-cactvs-binary',
|
||||||
|
205 => 'chemical/x-cdx',
|
||||||
|
206 => 'chemical/x-cerius',
|
||||||
|
207 => 'chemical/x-chem3d',
|
||||||
|
208 => 'chemical/x-chemdraw',
|
||||||
|
209 => 'chemical/x-cif',
|
||||||
|
210 => 'chemical/x-cmdf',
|
||||||
|
211 => 'chemical/x-cml',
|
||||||
|
212 => 'chemical/x-compass',
|
||||||
|
213 => 'chemical/x-crossfire',
|
||||||
|
214 => 'chemical/x-csml',
|
||||||
|
215 => 'chemical/x-ctx',
|
||||||
|
216 => 'chemical/x-cxf',
|
||||||
|
217 => 'chemical/x-embl-dl-nucleotide',
|
||||||
|
218 => 'chemical/x-galactic-spc',
|
||||||
|
219 => 'chemical/x-gamess-input',
|
||||||
|
220 => 'chemical/x-gaussian-checkpoint',
|
||||||
|
221 => 'chemical/x-gaussian-cube',
|
||||||
|
222 => 'chemical/x-gaussian-input',
|
||||||
|
223 => 'chemical/x-gaussian-log',
|
||||||
|
224 => 'chemical/x-gcg8-sequence',
|
||||||
|
225 => 'chemical/x-genbank',
|
||||||
|
226 => 'chemical/x-hin',
|
||||||
|
227 => 'chemical/x-isostar',
|
||||||
|
228 => 'chemical/x-jcamp-dx',
|
||||||
|
229 => 'chemical/x-kinemage',
|
||||||
|
230 => 'chemical/x-macmolecule',
|
||||||
|
231 => 'chemical/x-macromodel-input',
|
||||||
|
232 => 'chemical/x-mdl-molfile',
|
||||||
|
233 => 'chemical/x-mdl-rdfile',
|
||||||
|
234 => 'chemical/x-mdl-rxnfile',
|
||||||
|
235 => 'chemical/x-mdl-sdfile',
|
||||||
|
236 => 'chemical/x-mdl-tgf',
|
||||||
|
237 => 'chemical/x-mmcif',
|
||||||
|
238 => 'chemical/x-mol2',
|
||||||
|
239 => 'chemical/x-molconn-Z',
|
||||||
|
240 => 'chemical/x-mopac-graph',
|
||||||
|
241 => 'chemical/x-mopac-input',
|
||||||
|
242 => 'chemical/x-mopac-out',
|
||||||
|
243 => 'chemical/x-mopac-vib',
|
||||||
|
244 => 'chemical/x-ncbi-asn1-ascii',
|
||||||
|
245 => 'chemical/x-ncbi-asn1-binary',
|
||||||
|
246 => 'chemical/x-ncbi-asn1-spec',
|
||||||
|
247 => 'chemical/x-pdb',
|
||||||
|
248 => 'chemical/x-rosdal',
|
||||||
|
249 => 'chemical/x-swissprot',
|
||||||
|
250 => 'chemical/x-vamas-iso14976',
|
||||||
|
251 => 'chemical/x-vmd',
|
||||||
|
252 => 'chemical/x-xtel',
|
||||||
|
253 => 'chemical/x-xyz',
|
||||||
|
254 => 'image/gif',
|
||||||
|
255 => 'image/ief',
|
||||||
|
256 => 'image/jpeg',
|
||||||
|
257 => 'image/pcx',
|
||||||
|
258 => 'image/png',
|
||||||
|
259 => 'image/svg+xml',
|
||||||
|
260 => 'image/tiff',
|
||||||
|
261 => 'image/vnd.djvu',
|
||||||
|
262 => 'image/vnd.microsoft.icon',
|
||||||
|
263 => 'image/vnd.wap.wbmp',
|
||||||
|
355 => 'image/webp',
|
||||||
|
264 => 'image/x-cmu-raster',
|
||||||
|
265 => 'image/x-coreldraw',
|
||||||
|
266 => 'image/x-coreldrawpattern',
|
||||||
|
267 => 'image/x-coreldrawtemplate',
|
||||||
|
268 => 'image/x-corelphotopaint',
|
||||||
|
269 => 'image/x-jg',
|
||||||
|
270 => 'image/x-jng',
|
||||||
|
271 => 'image/x-ms-bmp',
|
||||||
|
272 => 'image/x-photoshop',
|
||||||
|
273 => 'image/x-portable-anymap',
|
||||||
|
274 => 'image/x-portable-bitmap',
|
||||||
|
275 => 'image/x-portable-graymap',
|
||||||
|
276 => 'image/x-portable-pixmap',
|
||||||
|
277 => 'image/x-rgb',
|
||||||
|
278 => 'image/x-xbitmap',
|
||||||
|
279 => 'image/x-xpixmap',
|
||||||
|
280 => 'image/x-xwindowdump',
|
||||||
|
281 => 'message/rfc822',
|
||||||
|
282 => 'model/iges',
|
||||||
|
283 => 'model/mesh',
|
||||||
|
284 => 'model/vrml',
|
||||||
|
285 => 'text/calendar',
|
||||||
|
286 => 'text/css',
|
||||||
|
287 => 'text/csv',
|
||||||
|
288 => 'text/h323',
|
||||||
|
289 => 'text/html',
|
||||||
|
290 => 'text/iuls',
|
||||||
|
291 => 'text/mathml',
|
||||||
|
292 => 'text/plain',
|
||||||
|
293 => 'text/richtext',
|
||||||
|
294 => 'text/scriptlet',
|
||||||
|
295 => 'text/tab-separated-values',
|
||||||
|
296 => 'text/texmacs',
|
||||||
|
297 => 'text/vnd.sun.j2me.app-descriptor',
|
||||||
|
298 => 'text/vnd.wap.wml',
|
||||||
|
299 => 'text/vnd.wap.wmlscript',
|
||||||
|
358 => 'text/vtt',
|
||||||
|
300 => 'text/x-bibtex',
|
||||||
|
301 => 'text/x-boo',
|
||||||
|
302 => 'text/x-c++hdr',
|
||||||
|
303 => 'text/x-c++src',
|
||||||
|
304 => 'text/x-chdr',
|
||||||
|
305 => 'text/x-component',
|
||||||
|
306 => 'text/x-csh',
|
||||||
|
307 => 'text/x-csrc',
|
||||||
|
308 => 'text/x-diff',
|
||||||
|
309 => 'text/x-dsrc',
|
||||||
|
310 => 'text/x-haskell',
|
||||||
|
311 => 'text/x-java',
|
||||||
|
312 => 'text/x-literate-haskell',
|
||||||
|
313 => 'text/x-moc',
|
||||||
|
314 => 'text/x-pascal',
|
||||||
|
315 => 'text/x-pcs-gcd',
|
||||||
|
316 => 'text/x-perl',
|
||||||
|
317 => 'text/x-python',
|
||||||
|
318 => 'text/x-setext',
|
||||||
|
319 => 'text/x-sh',
|
||||||
|
320 => 'text/x-tcl',
|
||||||
|
321 => 'text/x-tex',
|
||||||
|
322 => 'text/x-vcalendar',
|
||||||
|
323 => 'text/x-vcard',
|
||||||
|
324 => 'video/3gpp',
|
||||||
|
325 => 'video/dl',
|
||||||
|
326 => 'video/dv',
|
||||||
|
327 => 'video/fli',
|
||||||
|
328 => 'video/gl',
|
||||||
|
329 => 'video/mp4',
|
||||||
|
330 => 'video/mpeg',
|
||||||
|
331 => 'video/ogg',
|
||||||
|
332 => 'video/quicktime',
|
||||||
|
333 => 'video/vnd.mpegurl',
|
||||||
|
357 => 'video/webm',
|
||||||
|
347 => 'video/x-flv',
|
||||||
|
334 => 'video/x-la-asf',
|
||||||
|
348 => 'video/x-m4v',
|
||||||
|
353 => 'video/x-matroska',
|
||||||
|
335 => 'video/x-mng',
|
||||||
|
336 => 'video/x-ms-asf',
|
||||||
|
337 => 'video/x-ms-wm',
|
||||||
|
338 => 'video/x-ms-wmv',
|
||||||
|
339 => 'video/x-ms-wmx',
|
||||||
|
340 => 'video/x-ms-wvx',
|
||||||
|
341 => 'video/x-msvideo',
|
||||||
|
342 => 'video/x-sgi-movie',
|
||||||
|
343 => 'x-conference/x-cooltalk',
|
||||||
|
344 => 'x-epoc/x-sisx-app',
|
||||||
|
345 => 'x-world/x-vrml',
|
||||||
|
),
|
||||||
|
|
||||||
|
// Extensions added to this list MUST be lower-case.
|
||||||
|
'extensions' => array(
|
||||||
|
'ez' => 0,
|
||||||
|
'atom' => 1,
|
||||||
|
'atomcat' => 2,
|
||||||
|
'atomsrv' => 3,
|
||||||
|
'cap' => 4,
|
||||||
|
'pcap' => 4,
|
||||||
|
'cu' => 5,
|
||||||
|
'tsp' => 6,
|
||||||
|
'hta' => 7,
|
||||||
|
'jar' => 8,
|
||||||
|
'ser' => 9,
|
||||||
|
'class' => 10,
|
||||||
|
'hqx' => 11,
|
||||||
|
'nb' => 12,
|
||||||
|
'mdb' => 13,
|
||||||
|
'dot' => 14,
|
||||||
|
'doc' => 14,
|
||||||
|
'bin' => 15,
|
||||||
|
'oda' => 16,
|
||||||
|
'ogx' => 17,
|
||||||
|
'pdf' => 18,
|
||||||
|
'key' => 19,
|
||||||
|
'pgp' => 20,
|
||||||
|
'prf' => 21,
|
||||||
|
'eps' => 22,
|
||||||
|
'ai' => 22,
|
||||||
|
'ps' => 22,
|
||||||
|
'rar' => 23,
|
||||||
|
'rdf' => 24,
|
||||||
|
'rss' => 25,
|
||||||
|
'rtf' => 26,
|
||||||
|
'smi' => 27,
|
||||||
|
'smil' => 27,
|
||||||
|
'cdy' => 28,
|
||||||
|
'kml' => 29,
|
||||||
|
'kmz' => 30,
|
||||||
|
'xul' => 31,
|
||||||
|
'xlb' => 32,
|
||||||
|
'xlt' => 32,
|
||||||
|
'xls' => 32,
|
||||||
|
'xlam' => 33,
|
||||||
|
'xlsb' => 34,
|
||||||
|
'xlsm' => 35,
|
||||||
|
'xltm' => 36,
|
||||||
|
'cat' => 37,
|
||||||
|
'stl' => 38,
|
||||||
|
'pps' => 39,
|
||||||
|
'ppt' => 39,
|
||||||
|
'ppam' => 40,
|
||||||
|
'pptm' => 41,
|
||||||
|
'ppsm' => 42,
|
||||||
|
'potm' => 43,
|
||||||
|
'docm' => 44,
|
||||||
|
'dotm' => 45,
|
||||||
|
'xps' => 46,
|
||||||
|
'odc' => 47,
|
||||||
|
'odb' => 48,
|
||||||
|
'odf' => 49,
|
||||||
|
'odg' => 50,
|
||||||
|
'otg' => 51,
|
||||||
|
'odi' => 52,
|
||||||
|
'odp' => 53,
|
||||||
|
'otp' => 54,
|
||||||
|
'ods' => 55,
|
||||||
|
'ots' => 56,
|
||||||
|
'odt' => 57,
|
||||||
|
'odm' => 58,
|
||||||
|
'ott' => 59,
|
||||||
|
'oth' => 60,
|
||||||
|
'pptx' => 61,
|
||||||
|
'ppsx' => 62,
|
||||||
|
'potx' => 63,
|
||||||
|
'xlsx' => 64,
|
||||||
|
'xltx' => 65,
|
||||||
|
'docx' => 66,
|
||||||
|
'dotx' => 67,
|
||||||
|
'cod' => 68,
|
||||||
|
'mmf' => 69,
|
||||||
|
'sdc' => 70,
|
||||||
|
'sds' => 71,
|
||||||
|
'sda' => 72,
|
||||||
|
'sdd' => 73,
|
||||||
|
'sdw' => 75,
|
||||||
|
'sgl' => 76,
|
||||||
|
'sxc' => 77,
|
||||||
|
'stc' => 78,
|
||||||
|
'sxd' => 79,
|
||||||
|
'std' => 80,
|
||||||
|
'sxi' => 81,
|
||||||
|
'sti' => 82,
|
||||||
|
'sxm' => 83,
|
||||||
|
'sxw' => 84,
|
||||||
|
'sxg' => 85,
|
||||||
|
'stw' => 86,
|
||||||
|
'sis' => 87,
|
||||||
|
'vsd' => 88,
|
||||||
|
'wbxml' => 89,
|
||||||
|
'wmlc' => 90,
|
||||||
|
'wmlsc' => 91,
|
||||||
|
'wpd' => 92,
|
||||||
|
'wp5' => 93,
|
||||||
|
'wk' => 94,
|
||||||
|
'7z' => 95,
|
||||||
|
'abw' => 96,
|
||||||
|
'dmg' => 97,
|
||||||
|
'bcpio' => 98,
|
||||||
|
'torrent' => 99,
|
||||||
|
'cab' => 100,
|
||||||
|
'cbr' => 101,
|
||||||
|
'cbz' => 102,
|
||||||
|
'cdf' => 103,
|
||||||
|
'vcd' => 104,
|
||||||
|
'pgn' => 105,
|
||||||
|
'cpio' => 106,
|
||||||
|
'udeb' => 107,
|
||||||
|
'deb' => 107,
|
||||||
|
'dir' => 108,
|
||||||
|
'dxr' => 108,
|
||||||
|
'dcr' => 108,
|
||||||
|
'dms' => 109,
|
||||||
|
'wad' => 110,
|
||||||
|
'dvi' => 111,
|
||||||
|
'flac' => 112,
|
||||||
|
'pfa' => 113,
|
||||||
|
'pfb' => 113,
|
||||||
|
'pcf' => 113,
|
||||||
|
'gsf' => 113,
|
||||||
|
'pcf.z' => 113,
|
||||||
|
'mm' => 114,
|
||||||
|
'spl' => 115,
|
||||||
|
'gnumeric' => 116,
|
||||||
|
'sgf' => 117,
|
||||||
|
'gcf' => 118,
|
||||||
|
'taz' => 119,
|
||||||
|
'gtar' => 119,
|
||||||
|
'tgz' => 119,
|
||||||
|
'hdf' => 120,
|
||||||
|
'rhtml' => 121,
|
||||||
|
'phtml' => 122,
|
||||||
|
'pht' => 122,
|
||||||
|
'php' => 122,
|
||||||
|
'phps' => 123,
|
||||||
|
'php3' => 124,
|
||||||
|
'php3p' => 125,
|
||||||
|
'php4' => 126,
|
||||||
|
'ica' => 127,
|
||||||
|
'ins' => 128,
|
||||||
|
'isp' => 128,
|
||||||
|
'iii' => 129,
|
||||||
|
'iso' => 130,
|
||||||
|
'jnlp' => 131,
|
||||||
|
'js' => 132,
|
||||||
|
'jmz' => 133,
|
||||||
|
'chrt' => 134,
|
||||||
|
'kil' => 135,
|
||||||
|
'skp' => 136,
|
||||||
|
'skd' => 136,
|
||||||
|
'skm' => 136,
|
||||||
|
'skt' => 136,
|
||||||
|
'kpr' => 137,
|
||||||
|
'kpt' => 137,
|
||||||
|
'ksp' => 138,
|
||||||
|
'kwd' => 139,
|
||||||
|
'kwt' => 139,
|
||||||
|
'latex' => 140,
|
||||||
|
'lha' => 141,
|
||||||
|
'lyx' => 142,
|
||||||
|
'lzh' => 143,
|
||||||
|
'lzx' => 144,
|
||||||
|
'maker' => 145,
|
||||||
|
'frm' => 145,
|
||||||
|
'frame' => 145,
|
||||||
|
'fm' => 145,
|
||||||
|
'book' => 145,
|
||||||
|
'fb' => 145,
|
||||||
|
'fbdoc' => 145,
|
||||||
|
'mif' => 146,
|
||||||
|
'wmd' => 147,
|
||||||
|
'wmz' => 148,
|
||||||
|
'dll' => 149,
|
||||||
|
'bat' => 149,
|
||||||
|
'exe' => 149,
|
||||||
|
'com' => 149,
|
||||||
|
'msi' => 150,
|
||||||
|
'nc' => 151,
|
||||||
|
'pac' => 152,
|
||||||
|
'nwc' => 153,
|
||||||
|
'o' => 154,
|
||||||
|
'oza' => 155,
|
||||||
|
'p7r' => 156,
|
||||||
|
'crl' => 157,
|
||||||
|
'pyo' => 158,
|
||||||
|
'pyc' => 158,
|
||||||
|
'qtl' => 159,
|
||||||
|
'rpm' => 160,
|
||||||
|
'shar' => 161,
|
||||||
|
'swf' => 162,
|
||||||
|
'swfl' => 162,
|
||||||
|
'sitx' => 163,
|
||||||
|
'sit' => 163,
|
||||||
|
'sv4cpio' => 164,
|
||||||
|
'sv4crc' => 165,
|
||||||
|
'tar' => 166,
|
||||||
|
'gf' => 168,
|
||||||
|
'pk' => 169,
|
||||||
|
'texi' => 170,
|
||||||
|
'texinfo' => 170,
|
||||||
|
'sik' => 171,
|
||||||
|
'~' => 171,
|
||||||
|
'bak' => 171,
|
||||||
|
'%' => 171,
|
||||||
|
'old' => 171,
|
||||||
|
't' => 172,
|
||||||
|
'roff' => 172,
|
||||||
|
'tr' => 172,
|
||||||
|
'man' => 173,
|
||||||
|
'me' => 174,
|
||||||
|
'ms' => 175,
|
||||||
|
'ustar' => 176,
|
||||||
|
'src' => 177,
|
||||||
|
'wz' => 178,
|
||||||
|
'crt' => 179,
|
||||||
|
'xcf' => 180,
|
||||||
|
'fig' => 181,
|
||||||
|
'xpi' => 182,
|
||||||
|
'xht' => 183,
|
||||||
|
'xhtml' => 183,
|
||||||
|
'xml' => 184,
|
||||||
|
'xsl' => 184,
|
||||||
|
'zip' => 185,
|
||||||
|
'au' => 186,
|
||||||
|
'snd' => 186,
|
||||||
|
'mid' => 187,
|
||||||
|
'midi' => 187,
|
||||||
|
'kar' => 187,
|
||||||
|
'mpega' => 188,
|
||||||
|
'mpga' => 188,
|
||||||
|
'm4a' => 188,
|
||||||
|
'mp3' => 188,
|
||||||
|
'mp2' => 188,
|
||||||
|
'ogg' => 189,
|
||||||
|
'oga' => 189,
|
||||||
|
'spx' => 189,
|
||||||
|
'sid' => 190,
|
||||||
|
'aif' => 191,
|
||||||
|
'aiff' => 191,
|
||||||
|
'aifc' => 191,
|
||||||
|
'gsm' => 192,
|
||||||
|
'm3u' => 193,
|
||||||
|
'wax' => 194,
|
||||||
|
'wma' => 195,
|
||||||
|
'rm' => 196,
|
||||||
|
'ram' => 196,
|
||||||
|
'ra' => 197,
|
||||||
|
'pls' => 198,
|
||||||
|
'sd2' => 199,
|
||||||
|
'wav' => 200,
|
||||||
|
'alc' => 201,
|
||||||
|
'cac' => 202,
|
||||||
|
'cache' => 202,
|
||||||
|
'csf' => 203,
|
||||||
|
'cascii' => 204,
|
||||||
|
'cbin' => 204,
|
||||||
|
'ctab' => 204,
|
||||||
|
'cdx' => 205,
|
||||||
|
'cer' => 206,
|
||||||
|
'c3d' => 207,
|
||||||
|
'chm' => 208,
|
||||||
|
'cif' => 209,
|
||||||
|
'cmdf' => 210,
|
||||||
|
'cml' => 211,
|
||||||
|
'cpa' => 212,
|
||||||
|
'bsd' => 213,
|
||||||
|
'csml' => 214,
|
||||||
|
'csm' => 214,
|
||||||
|
'ctx' => 215,
|
||||||
|
'cxf' => 216,
|
||||||
|
'cef' => 216,
|
||||||
|
'emb' => 217,
|
||||||
|
'embl' => 217,
|
||||||
|
'spc' => 218,
|
||||||
|
'gam' => 219,
|
||||||
|
'inp' => 219,
|
||||||
|
'gamin' => 219,
|
||||||
|
'fchk' => 220,
|
||||||
|
'fch' => 220,
|
||||||
|
'cub' => 221,
|
||||||
|
'gau' => 222,
|
||||||
|
'gjf' => 222,
|
||||||
|
'gjc' => 222,
|
||||||
|
'gal' => 223,
|
||||||
|
'gcg' => 224,
|
||||||
|
'gen' => 225,
|
||||||
|
'hin' => 226,
|
||||||
|
'istr' => 227,
|
||||||
|
'ist' => 227,
|
||||||
|
'dx' => 228,
|
||||||
|
'jdx' => 228,
|
||||||
|
'kin' => 229,
|
||||||
|
'mcm' => 230,
|
||||||
|
'mmd' => 231,
|
||||||
|
'mmod' => 231,
|
||||||
|
'mol' => 232,
|
||||||
|
'rd' => 233,
|
||||||
|
'rxn' => 234,
|
||||||
|
'sdf' => 235,
|
||||||
|
'sd' => 235,
|
||||||
|
'tgf' => 236,
|
||||||
|
'mcif' => 237,
|
||||||
|
'mol2' => 238,
|
||||||
|
'b' => 239,
|
||||||
|
'gpt' => 240,
|
||||||
|
'mopcrt' => 241,
|
||||||
|
'zmt' => 241,
|
||||||
|
'mpc' => 241,
|
||||||
|
'dat' => 241,
|
||||||
|
'mop' => 241,
|
||||||
|
'moo' => 242,
|
||||||
|
'mvb' => 243,
|
||||||
|
'prt' => 244,
|
||||||
|
'aso' => 245,
|
||||||
|
'val' => 245,
|
||||||
|
'asn' => 246,
|
||||||
|
'ent' => 247,
|
||||||
|
'pdb' => 247,
|
||||||
|
'ros' => 248,
|
||||||
|
'sw' => 249,
|
||||||
|
'vms' => 250,
|
||||||
|
'vmd' => 251,
|
||||||
|
'xtel' => 252,
|
||||||
|
'xyz' => 253,
|
||||||
|
'gif' => 254,
|
||||||
|
'ief' => 255,
|
||||||
|
'jpeg' => 256,
|
||||||
|
'jpe' => 256,
|
||||||
|
'jpg' => 256,
|
||||||
|
'pcx' => 257,
|
||||||
|
'png' => 258,
|
||||||
|
'svgz' => 259,
|
||||||
|
'svg' => 259,
|
||||||
|
'tif' => 260,
|
||||||
|
'tiff' => 260,
|
||||||
|
'djvu' => 261,
|
||||||
|
'djv' => 261,
|
||||||
|
'ico' => 262,
|
||||||
|
'wbmp' => 263,
|
||||||
|
'ras' => 264,
|
||||||
|
'cdr' => 265,
|
||||||
|
'pat' => 266,
|
||||||
|
'cdt' => 267,
|
||||||
|
'cpt' => 268,
|
||||||
|
'art' => 269,
|
||||||
|
'jng' => 270,
|
||||||
|
'bmp' => 271,
|
||||||
|
'psd' => 272,
|
||||||
|
'pnm' => 273,
|
||||||
|
'pbm' => 274,
|
||||||
|
'pgm' => 275,
|
||||||
|
'ppm' => 276,
|
||||||
|
'rgb' => 277,
|
||||||
|
'xbm' => 278,
|
||||||
|
'xpm' => 279,
|
||||||
|
'xwd' => 280,
|
||||||
|
'eml' => 281,
|
||||||
|
'igs' => 282,
|
||||||
|
'iges' => 282,
|
||||||
|
'silo' => 283,
|
||||||
|
'msh' => 283,
|
||||||
|
'mesh' => 283,
|
||||||
|
'icz' => 285,
|
||||||
|
'ics' => 285,
|
||||||
|
'css' => 286,
|
||||||
|
'csv' => 287,
|
||||||
|
'323' => 288,
|
||||||
|
'html' => 289,
|
||||||
|
'htm' => 289,
|
||||||
|
'shtml' => 289,
|
||||||
|
'uls' => 290,
|
||||||
|
'mml' => 291,
|
||||||
|
'txt' => 292,
|
||||||
|
'pot' => 292,
|
||||||
|
'text' => 292,
|
||||||
|
'asc' => 292,
|
||||||
|
'rtx' => 293,
|
||||||
|
'wsc' => 294,
|
||||||
|
'sct' => 294,
|
||||||
|
'tsv' => 295,
|
||||||
|
'ts' => 296,
|
||||||
|
'tm' => 296,
|
||||||
|
'jad' => 297,
|
||||||
|
'wml' => 298,
|
||||||
|
'wmls' => 299,
|
||||||
|
'bib' => 300,
|
||||||
|
'boo' => 301,
|
||||||
|
'hpp' => 302,
|
||||||
|
'hh' => 302,
|
||||||
|
'h++' => 302,
|
||||||
|
'hxx' => 302,
|
||||||
|
'cxx' => 303,
|
||||||
|
'cc' => 303,
|
||||||
|
'cpp' => 303,
|
||||||
|
'c++' => 303,
|
||||||
|
'h' => 304,
|
||||||
|
'htc' => 305,
|
||||||
|
'csh' => 306,
|
||||||
|
'c' => 307,
|
||||||
|
'patch' => 308,
|
||||||
|
'diff' => 308,
|
||||||
|
'd' => 309,
|
||||||
|
'hs' => 310,
|
||||||
|
'java' => 311,
|
||||||
|
'lhs' => 312,
|
||||||
|
'moc' => 313,
|
||||||
|
'pas' => 314,
|
||||||
|
'p' => 314,
|
||||||
|
'gcd' => 315,
|
||||||
|
'pm' => 316,
|
||||||
|
'pl' => 316,
|
||||||
|
'py' => 317,
|
||||||
|
'etx' => 318,
|
||||||
|
'sh' => 319,
|
||||||
|
'tk' => 320,
|
||||||
|
'tcl' => 320,
|
||||||
|
'cls' => 321,
|
||||||
|
'ltx' => 321,
|
||||||
|
'sty' => 321,
|
||||||
|
'tex' => 321,
|
||||||
|
'vcs' => 322,
|
||||||
|
'vcf' => 323,
|
||||||
|
'3gp' => 324,
|
||||||
|
'dl' => 325,
|
||||||
|
'dif' => 326,
|
||||||
|
'dv' => 326,
|
||||||
|
'fli' => 327,
|
||||||
|
'gl' => 328,
|
||||||
|
'mp4' => 329,
|
||||||
|
'f4v' => 329,
|
||||||
|
'f4p' => 329,
|
||||||
|
'mpe' => 330,
|
||||||
|
'mpeg' => 330,
|
||||||
|
'mpg' => 330,
|
||||||
|
'ogv' => 331,
|
||||||
|
'qt' => 332,
|
||||||
|
'mov' => 332,
|
||||||
|
'mxu' => 333,
|
||||||
|
'lsf' => 334,
|
||||||
|
'lsx' => 334,
|
||||||
|
'mng' => 335,
|
||||||
|
'asx' => 336,
|
||||||
|
'asf' => 336,
|
||||||
|
'wm' => 337,
|
||||||
|
'wmv' => 338,
|
||||||
|
'wmx' => 339,
|
||||||
|
'wvx' => 340,
|
||||||
|
'avi' => 341,
|
||||||
|
'movie' => 342,
|
||||||
|
'ice' => 343,
|
||||||
|
'sisx' => 344,
|
||||||
|
'wrl' => 345,
|
||||||
|
'vrm' => 345,
|
||||||
|
'vrml' => 345,
|
||||||
|
'f4a' => 346,
|
||||||
|
'f4b' => 346,
|
||||||
|
'flv' => 347,
|
||||||
|
'm4v' => 348,
|
||||||
|
'azw' => 349,
|
||||||
|
'epub' => 350,
|
||||||
|
'mobi' => 351,
|
||||||
|
'prc' => 352,
|
||||||
|
'mkv' => 353,
|
||||||
|
'mka' => 354,
|
||||||
|
'webp' => 355,
|
||||||
|
'weba' => 356,
|
||||||
|
'webm' => 357,
|
||||||
|
'vtt' => 358,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
427
includes/filetransfer/filetransfer.inc
Normal file
427
includes/filetransfer/filetransfer.inc
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Base FileTransfer class.
|
||||||
|
*
|
||||||
|
* Classes extending this class perform file operations on directories not
|
||||||
|
* writable by the webserver. To achieve this, the class should connect back
|
||||||
|
* to the server using some backend (for example FTP or SSH). To keep security,
|
||||||
|
* the password should always be asked from the user and never stored. For
|
||||||
|
* safety, all methods operate only inside a "jail", by default the Drupal root.
|
||||||
|
*/
|
||||||
|
abstract class FileTransfer {
|
||||||
|
protected $username;
|
||||||
|
protected $password;
|
||||||
|
protected $hostname = 'localhost';
|
||||||
|
protected $port;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The constructor for the UpdateConnection class. This method is also called
|
||||||
|
* from the classes that extend this class and override this method.
|
||||||
|
*/
|
||||||
|
function __construct($jail) {
|
||||||
|
$this->jail = $jail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classes that extend this class must override the factory() static method.
|
||||||
|
*
|
||||||
|
* @param string $jail
|
||||||
|
* The full path where all file operations performed by this object will
|
||||||
|
* be restricted to. This prevents the FileTransfer classes from being
|
||||||
|
* able to touch other parts of the filesystem.
|
||||||
|
* @param array $settings
|
||||||
|
* An array of connection settings for the FileTransfer subclass. If the
|
||||||
|
* getSettingsForm() method uses any nested settings, the same structure
|
||||||
|
* will be assumed here.
|
||||||
|
* @return object
|
||||||
|
* New instance of the appropriate FileTransfer subclass.
|
||||||
|
*/
|
||||||
|
static function factory($jail, $settings) {
|
||||||
|
throw new FileTransferException('FileTransfer::factory() static method not overridden by FileTransfer subclass.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of the magic __get() method.
|
||||||
|
*
|
||||||
|
* If the connection isn't set to anything, this will call the connect() method
|
||||||
|
* and set it to and return the result; afterwards, the connection will be
|
||||||
|
* returned directly without using this method.
|
||||||
|
*/
|
||||||
|
function __get($name) {
|
||||||
|
if ($name == 'connection') {
|
||||||
|
$this->connect();
|
||||||
|
return $this->connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($name == 'chroot') {
|
||||||
|
$this->setChroot();
|
||||||
|
return $this->chroot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connect to the server.
|
||||||
|
*/
|
||||||
|
abstract protected function connect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a directory.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* The source path.
|
||||||
|
* @param $destination
|
||||||
|
* The destination path.
|
||||||
|
*/
|
||||||
|
public final function copyDirectory($source, $destination) {
|
||||||
|
$source = $this->sanitizePath($source);
|
||||||
|
$destination = $this->fixRemotePath($destination);
|
||||||
|
$this->checkPath($destination);
|
||||||
|
$this->copyDirectoryJailed($source, $destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see http://php.net/chmod
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @param long $mode
|
||||||
|
* @param bool $recursive
|
||||||
|
*/
|
||||||
|
public final function chmod($path, $mode, $recursive = FALSE) {
|
||||||
|
if (!in_array('FileTransferChmodInterface', class_implements(get_class($this)))) {
|
||||||
|
throw new FileTransferException('Unable to change file permissions');
|
||||||
|
}
|
||||||
|
$path = $this->sanitizePath($path);
|
||||||
|
$path = $this->fixRemotePath($path);
|
||||||
|
$this->checkPath($path);
|
||||||
|
$this->chmodJailed($path, $mode, $recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory.
|
||||||
|
*
|
||||||
|
* @param $directory
|
||||||
|
* The directory to be created.
|
||||||
|
*/
|
||||||
|
public final function createDirectory($directory) {
|
||||||
|
$directory = $this->fixRemotePath($directory);
|
||||||
|
$this->checkPath($directory);
|
||||||
|
$this->createDirectoryJailed($directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a directory.
|
||||||
|
*
|
||||||
|
* @param $directory
|
||||||
|
* The directory to be removed.
|
||||||
|
*/
|
||||||
|
public final function removeDirectory($directory) {
|
||||||
|
$directory = $this->fixRemotePath($directory);
|
||||||
|
$this->checkPath($directory);
|
||||||
|
$this->removeDirectoryJailed($directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* The source file.
|
||||||
|
* @param $destination
|
||||||
|
* The destination file.
|
||||||
|
*/
|
||||||
|
public final function copyFile($source, $destination) {
|
||||||
|
$source = $this->sanitizePath($source);
|
||||||
|
$destination = $this->fixRemotePath($destination);
|
||||||
|
$this->checkPath($destination);
|
||||||
|
$this->copyFileJailed($source, $destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a file.
|
||||||
|
*
|
||||||
|
* @param $destination
|
||||||
|
* The destination file to be removed.
|
||||||
|
*/
|
||||||
|
public final function removeFile($destination) {
|
||||||
|
$destination = $this->fixRemotePath($destination);
|
||||||
|
$this->checkPath($destination);
|
||||||
|
$this->removeFileJailed($destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks that the path is inside the jail and throws an exception if not.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* A path to check against the jail.
|
||||||
|
*/
|
||||||
|
protected final function checkPath($path) {
|
||||||
|
$full_jail = $this->chroot . $this->jail;
|
||||||
|
$full_path = drupal_realpath(substr($this->chroot . $path, 0, strlen($full_jail)));
|
||||||
|
$full_path = $this->fixRemotePath($full_path, FALSE);
|
||||||
|
if ($full_jail !== $full_path) {
|
||||||
|
throw new FileTransferException('@directory is outside of the @jail', NULL, array('@directory' => $path, '@jail' => $this->jail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a modified path suitable for passing to the server.
|
||||||
|
* If a path is a windows path, makes it POSIX compliant by removing the drive letter.
|
||||||
|
* If $this->chroot has a value, it is stripped from the path to allow for
|
||||||
|
* chroot'd filetransfer systems.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* @param $strip_chroot
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected final function fixRemotePath($path, $strip_chroot = TRUE) {
|
||||||
|
$path = $this->sanitizePath($path);
|
||||||
|
$path = preg_replace('|^([a-z]{1}):|i', '', $path); // Strip out windows driveletter if its there.
|
||||||
|
if ($strip_chroot) {
|
||||||
|
if ($this->chroot && strpos($path, $this->chroot) === 0) {
|
||||||
|
$path = ($path == $this->chroot) ? '' : substr($path, strlen($this->chroot));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes backslashes to slashes, also removes a trailing slash.
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function sanitizePath($path) {
|
||||||
|
$path = str_replace('\\', '/', $path); // Windows path sanitization.
|
||||||
|
if (substr($path, -1) == '/') {
|
||||||
|
$path = substr($path, 0, -1);
|
||||||
|
}
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a directory.
|
||||||
|
*
|
||||||
|
* We need a separate method to make the $destination is in the jail.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* The source path.
|
||||||
|
* @param $destination
|
||||||
|
* The destination path.
|
||||||
|
*/
|
||||||
|
protected function copyDirectoryJailed($source, $destination) {
|
||||||
|
if ($this->isDirectory($destination)) {
|
||||||
|
$destination = $destination . '/' . drupal_basename($source);
|
||||||
|
}
|
||||||
|
$this->createDirectory($destination);
|
||||||
|
foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($source), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
|
||||||
|
$relative_path = substr($filename, strlen($source));
|
||||||
|
if ($file->isDir()) {
|
||||||
|
$this->createDirectory($destination . $relative_path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->copyFile($file->getPathName(), $destination . $relative_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory.
|
||||||
|
*
|
||||||
|
* @param $directory
|
||||||
|
* The directory to be created.
|
||||||
|
*/
|
||||||
|
abstract protected function createDirectoryJailed($directory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a directory.
|
||||||
|
*
|
||||||
|
* @param $directory
|
||||||
|
* The directory to be removed.
|
||||||
|
*/
|
||||||
|
abstract protected function removeDirectoryJailed($directory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* The source file.
|
||||||
|
* @param $destination
|
||||||
|
* The destination file.
|
||||||
|
*/
|
||||||
|
abstract protected function copyFileJailed($source, $destination);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a file.
|
||||||
|
*
|
||||||
|
* @param $destination
|
||||||
|
* The destination file to be removed.
|
||||||
|
*/
|
||||||
|
abstract protected function removeFileJailed($destination);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a particular path is a directory
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path to check
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
abstract public function isDirectory($path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a particular path is a file (not a directory).
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path to check
|
||||||
|
*
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
abstract public function isFile($path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the chroot property for this connection.
|
||||||
|
*
|
||||||
|
* It does this by moving up the tree until it finds itself. If successful,
|
||||||
|
* it will return the chroot, otherwise FALSE.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The chroot path for this connection or FALSE.
|
||||||
|
*/
|
||||||
|
function findChroot() {
|
||||||
|
// If the file exists as is, there is no chroot.
|
||||||
|
$path = __FILE__;
|
||||||
|
$path = $this->fixRemotePath($path, FALSE);
|
||||||
|
if ($this->isFile($path)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = dirname(__FILE__);
|
||||||
|
$path = $this->fixRemotePath($path, FALSE);
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
$chroot = '';
|
||||||
|
while (count($parts)) {
|
||||||
|
$check = implode($parts, '/');
|
||||||
|
if ($this->isFile($check . '/' . drupal_basename(__FILE__))) {
|
||||||
|
// Remove the trailing slash.
|
||||||
|
return substr($chroot, 0, -1);
|
||||||
|
}
|
||||||
|
$chroot .= array_shift($parts) . '/';
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the chroot and changes the jail to match the correct path scheme
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function setChroot() {
|
||||||
|
$this->chroot = $this->findChroot();
|
||||||
|
$this->jail = $this->fixRemotePath($this->jail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a form to collect connection settings credentials.
|
||||||
|
*
|
||||||
|
* Implementing classes can either extend this form with fields collecting the
|
||||||
|
* specific information they need, or override it entirely.
|
||||||
|
*/
|
||||||
|
public function getSettingsForm() {
|
||||||
|
$form['username'] = array(
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => t('Username'),
|
||||||
|
);
|
||||||
|
$form['password'] = array(
|
||||||
|
'#type' => 'password',
|
||||||
|
'#title' => t('Password'),
|
||||||
|
'#description' => t('Your password is not saved in the database and is only used to establish a connection.'),
|
||||||
|
);
|
||||||
|
$form['advanced'] = array(
|
||||||
|
'#type' => 'fieldset',
|
||||||
|
'#title' => t('Advanced settings'),
|
||||||
|
'#collapsible' => TRUE,
|
||||||
|
'#collapsed' => TRUE,
|
||||||
|
);
|
||||||
|
$form['advanced']['hostname'] = array(
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => t('Host'),
|
||||||
|
'#default_value' => 'localhost',
|
||||||
|
'#description' => t('The connection will be created between your web server and the machine hosting the web server files. In the vast majority of cases, this will be the same machine, and "localhost" is correct.'),
|
||||||
|
);
|
||||||
|
$form['advanced']['port'] = array(
|
||||||
|
'#type' => 'textfield',
|
||||||
|
'#title' => t('Port'),
|
||||||
|
'#default_value' => NULL,
|
||||||
|
);
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FileTransferException class.
|
||||||
|
*/
|
||||||
|
class FileTransferException extends Exception {
|
||||||
|
public $arguments;
|
||||||
|
|
||||||
|
function __construct($message, $code = 0, $arguments = array()) {
|
||||||
|
parent::__construct($message, $code);
|
||||||
|
$this->arguments = $arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A FileTransfer Class implementing this interface can be used to chmod files.
|
||||||
|
*/
|
||||||
|
interface FileTransferChmodInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the permissions of the file / directory specified in $path
|
||||||
|
*
|
||||||
|
* @param string $path
|
||||||
|
* Path to change permissions of.
|
||||||
|
* @param long $mode
|
||||||
|
* The new file permission mode to be passed to chmod().
|
||||||
|
* @param boolean $recursive
|
||||||
|
* Pass TRUE to recursively chmod the entire directory specified in $path.
|
||||||
|
*/
|
||||||
|
function chmodJailed($path, $mode, $recursive);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an interface for iterating recursively over filesystem directories.
|
||||||
|
*
|
||||||
|
* Manually skips '.' and '..' directories, since no existing method is
|
||||||
|
* available in PHP 5.2.
|
||||||
|
*
|
||||||
|
* @todo Depreciate in favor of RecursiveDirectoryIterator::SKIP_DOTS once PHP
|
||||||
|
* 5.3 or later is required.
|
||||||
|
*/
|
||||||
|
class SkipDotsRecursiveDirectoryIterator extends RecursiveDirectoryIterator {
|
||||||
|
/**
|
||||||
|
* Constructs a SkipDotsRecursiveDirectoryIterator
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path of the directory to be iterated over.
|
||||||
|
*/
|
||||||
|
function __construct($path) {
|
||||||
|
parent::__construct($path);
|
||||||
|
$this->skipdots();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewind() {
|
||||||
|
parent::rewind();
|
||||||
|
$this->skipdots();
|
||||||
|
}
|
||||||
|
|
||||||
|
function next() {
|
||||||
|
parent::next();
|
||||||
|
$this->skipdots();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function skipdots() {
|
||||||
|
while ($this->isDot()) {
|
||||||
|
parent::next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
144
includes/filetransfer/ftp.inc
Normal file
144
includes/filetransfer/ftp.inc
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for FTP implementations.
|
||||||
|
*/
|
||||||
|
abstract class FileTransferFTP extends FileTransfer {
|
||||||
|
|
||||||
|
public function __construct($jail, $username, $password, $hostname, $port) {
|
||||||
|
$this->username = $username;
|
||||||
|
$this->password = $password;
|
||||||
|
$this->hostname = $hostname;
|
||||||
|
$this->port = $port;
|
||||||
|
parent::__construct($jail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an object which can implement the FTP protocol.
|
||||||
|
*
|
||||||
|
* @param string $jail
|
||||||
|
* @param array $settings
|
||||||
|
* @return FileTransferFTP
|
||||||
|
* The appropriate FileTransferFTP subclass based on the available
|
||||||
|
* options. If the FTP PHP extension is available, use it.
|
||||||
|
*/
|
||||||
|
static function factory($jail, $settings) {
|
||||||
|
$username = empty($settings['username']) ? '' : $settings['username'];
|
||||||
|
$password = empty($settings['password']) ? '' : $settings['password'];
|
||||||
|
$hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
|
||||||
|
$port = empty($settings['advanced']['port']) ? 21 : $settings['advanced']['port'];
|
||||||
|
|
||||||
|
if (function_exists('ftp_connect')) {
|
||||||
|
$class = 'FileTransferFTPExtension';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new FileTransferException('No FTP backend available.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new $class($jail, $username, $password, $hostname, $port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the form to configure the FileTransfer class for FTP.
|
||||||
|
*/
|
||||||
|
public function getSettingsForm() {
|
||||||
|
$form = parent::getSettingsForm();
|
||||||
|
$form['advanced']['port']['#default_value'] = 21;
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileTransferFTPExtension extends FileTransferFTP implements FileTransferChmodInterface {
|
||||||
|
|
||||||
|
public function connect() {
|
||||||
|
$this->connection = ftp_connect($this->hostname, $this->port);
|
||||||
|
|
||||||
|
if (!$this->connection) {
|
||||||
|
throw new FileTransferException("Cannot connect to FTP Server, check settings");
|
||||||
|
}
|
||||||
|
if (!ftp_login($this->connection, $this->username, $this->password)) {
|
||||||
|
throw new FileTransferException("Cannot log in to FTP server. Check username and password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function copyFileJailed($source, $destination) {
|
||||||
|
if (!@ftp_put($this->connection, $destination, $source, FTP_BINARY)) {
|
||||||
|
throw new FileTransferException("Cannot move @source to @destination", NULL, array("@source" => $source, "@destination" => $destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDirectoryJailed($directory) {
|
||||||
|
if (!ftp_mkdir($this->connection, $directory)) {
|
||||||
|
throw new FileTransferException("Cannot create directory @directory", NULL, array("@directory" => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeDirectoryJailed($directory) {
|
||||||
|
$pwd = ftp_pwd($this->connection);
|
||||||
|
if (!ftp_chdir($this->connection, $directory)) {
|
||||||
|
throw new FileTransferException("Unable to change to directory @directory", NULL, array('@directory' => $directory));
|
||||||
|
}
|
||||||
|
$list = @ftp_nlist($this->connection, '.');
|
||||||
|
if (!$list) {
|
||||||
|
$list = array();
|
||||||
|
}
|
||||||
|
foreach ($list as $item) {
|
||||||
|
if ($item == '.' || $item == '..') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (@ftp_chdir($this->connection, $item)) {
|
||||||
|
ftp_cdup($this->connection);
|
||||||
|
$this->removeDirectory(ftp_pwd($this->connection) . '/' . $item);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$this->removeFile(ftp_pwd($this->connection) . '/' . $item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ftp_chdir($this->connection, $pwd);
|
||||||
|
if (!ftp_rmdir($this->connection, $directory)) {
|
||||||
|
throw new FileTransferException("Unable to remove to directory @directory", NULL, array('@directory' => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeFileJailed($destination) {
|
||||||
|
if (!ftp_delete($this->connection, $destination)) {
|
||||||
|
throw new FileTransferException("Unable to remove to file @file", NULL, array('@file' => $destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirectory($path) {
|
||||||
|
$result = FALSE;
|
||||||
|
$curr = ftp_pwd($this->connection);
|
||||||
|
if (@ftp_chdir($this->connection, $path)) {
|
||||||
|
$result = TRUE;
|
||||||
|
}
|
||||||
|
ftp_chdir($this->connection, $curr);
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFile($path) {
|
||||||
|
return ftp_size($this->connection, $path) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chmodJailed($path, $mode, $recursive) {
|
||||||
|
if (!ftp_chmod($this->connection, $mode, $path)) {
|
||||||
|
throw new FileTransferException("Unable to set permissions on %file", NULL, array('%file' => $path));
|
||||||
|
}
|
||||||
|
if ($this->isDirectory($path) && $recursive) {
|
||||||
|
$filelist = @ftp_nlist($this->connection, $path);
|
||||||
|
if (!$filelist) {
|
||||||
|
//empty directory - returns false
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach ($filelist as $file) {
|
||||||
|
$this->chmodJailed($file, $mode, $recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('ftp_chmod')) {
|
||||||
|
function ftp_chmod($ftp_stream, $mode, $filename) {
|
||||||
|
return ftp_site($ftp_stream, sprintf('CHMOD %o %s', $mode, $filename));
|
||||||
|
}
|
||||||
|
}
|
76
includes/filetransfer/local.inc
Normal file
76
includes/filetransfer/local.inc
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The local connection class for copying files as the httpd user.
|
||||||
|
*/
|
||||||
|
class FileTransferLocal extends FileTransfer implements FileTransferChmodInterface {
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
static function factory($jail, $settings) {
|
||||||
|
return new FileTransferLocal($jail);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function copyFileJailed($source, $destination) {
|
||||||
|
if (@!copy($source, $destination)) {
|
||||||
|
throw new FileTransferException('Cannot copy %source to %destination.', NULL, array('%source' => $source, '%destination' => $destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDirectoryJailed($directory) {
|
||||||
|
if (!is_dir($directory) && @!mkdir($directory, 0777, TRUE)) {
|
||||||
|
throw new FileTransferException('Cannot create directory %directory.', NULL, array('%directory' => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeDirectoryJailed($directory) {
|
||||||
|
if (!is_dir($directory)) {
|
||||||
|
// Programmer error assertion, not something we expect users to see.
|
||||||
|
throw new FileTransferException('removeDirectoryJailed() called with a path (%directory) that is not a directory.', NULL, array('%directory' => $directory));
|
||||||
|
}
|
||||||
|
foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($directory), RecursiveIteratorIterator::CHILD_FIRST) as $filename => $file) {
|
||||||
|
if ($file->isDir()) {
|
||||||
|
if (@!drupal_rmdir($filename)) {
|
||||||
|
throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($file->isFile()) {
|
||||||
|
if (@!drupal_unlink($filename)) {
|
||||||
|
throw new FileTransferException('Cannot remove file %file.', NULL, array('%file' => $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (@!drupal_rmdir($directory)) {
|
||||||
|
throw new FileTransferException('Cannot remove directory %directory.', NULL, array('%directory' => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeFileJailed($file) {
|
||||||
|
if (@!drupal_unlink($file)) {
|
||||||
|
throw new FileTransferException('Cannot remove file %file.', NULL, array('%file' => $file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDirectory($path) {
|
||||||
|
return is_dir($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFile($path) {
|
||||||
|
return is_file($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function chmodJailed($path, $mode, $recursive) {
|
||||||
|
if ($recursive && is_dir($path)) {
|
||||||
|
foreach (new RecursiveIteratorIterator(new SkipDotsRecursiveDirectoryIterator($path), RecursiveIteratorIterator::SELF_FIRST) as $filename => $file) {
|
||||||
|
if (@!chmod($filename, $mode)) {
|
||||||
|
throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $filename));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (@!chmod($path, $mode)) {
|
||||||
|
throw new FileTransferException('Cannot chmod %path.', NULL, array('%path' => $path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
includes/filetransfer/ssh.inc
Normal file
110
includes/filetransfer/ssh.inc
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The SSH connection class for the update module.
|
||||||
|
*/
|
||||||
|
class FileTransferSSH extends FileTransfer implements FileTransferChmodInterface {
|
||||||
|
|
||||||
|
function __construct($jail, $username, $password, $hostname = "localhost", $port = 22) {
|
||||||
|
$this->username = $username;
|
||||||
|
$this->password = $password;
|
||||||
|
$this->hostname = $hostname;
|
||||||
|
$this->port = $port;
|
||||||
|
parent::__construct($jail);
|
||||||
|
}
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
$this->connection = @ssh2_connect($this->hostname, $this->port);
|
||||||
|
if (!$this->connection) {
|
||||||
|
throw new FileTransferException('SSH Connection failed to @host:@port', NULL, array('@host' => $this->hostname, '@port' => $this->port));
|
||||||
|
}
|
||||||
|
if (!@ssh2_auth_password($this->connection, $this->username, $this->password)) {
|
||||||
|
throw new FileTransferException('The supplied username/password combination was not accepted.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static function factory($jail, $settings) {
|
||||||
|
$username = empty($settings['username']) ? '' : $settings['username'];
|
||||||
|
$password = empty($settings['password']) ? '' : $settings['password'];
|
||||||
|
$hostname = empty($settings['advanced']['hostname']) ? 'localhost' : $settings['advanced']['hostname'];
|
||||||
|
$port = empty($settings['advanced']['port']) ? 22 : $settings['advanced']['port'];
|
||||||
|
return new FileTransferSSH($jail, $username, $password, $hostname, $port);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function copyFileJailed($source, $destination) {
|
||||||
|
if (!@ssh2_scp_send($this->connection, $source, $destination)) {
|
||||||
|
throw new FileTransferException('Cannot copy @source_file to @destination_file.', NULL, array('@source' => $source, '@destination' => $destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function copyDirectoryJailed($source, $destination) {
|
||||||
|
if (@!ssh2_exec($this->connection, 'cp -Rp ' . escapeshellarg($source) . ' ' . escapeshellarg($destination))) {
|
||||||
|
throw new FileTransferException('Cannot copy directory @directory.', NULL, array('@directory' => $source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDirectoryJailed($directory) {
|
||||||
|
if (@!ssh2_exec($this->connection, 'mkdir ' . escapeshellarg($directory))) {
|
||||||
|
throw new FileTransferException('Cannot create directory @directory.', NULL, array('@directory' => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeDirectoryJailed($directory) {
|
||||||
|
if (@!ssh2_exec($this->connection, 'rm -Rf ' . escapeshellarg($directory))) {
|
||||||
|
throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $directory));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function removeFileJailed($destination) {
|
||||||
|
if (!@ssh2_exec($this->connection, 'rm ' . escapeshellarg($destination))) {
|
||||||
|
throw new FileTransferException('Cannot remove @directory.', NULL, array('@directory' => $destination));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This is untested. It is not currently used, but should do the trick.
|
||||||
|
*/
|
||||||
|
public function isDirectory($path) {
|
||||||
|
$directory = escapeshellarg($path);
|
||||||
|
$cmd = "[ -d {$directory} ] && echo 'yes'";
|
||||||
|
if ($output = @ssh2_exec($this->connection, $cmd)) {
|
||||||
|
if ($output == 'yes') {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isFile($path) {
|
||||||
|
$file = escapeshellarg($path);
|
||||||
|
$cmd = "[ -f {$file} ] && echo 'yes'";
|
||||||
|
if ($output = @ssh2_exec($this->connection, $cmd)) {
|
||||||
|
if ($output == 'yes') {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new FileTransferException('Cannot check @path.', NULL, array('@path' => $path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function chmodJailed($path, $mode, $recursive) {
|
||||||
|
$cmd = sprintf("chmod %s%o %s", $recursive ? '-R ' : '', $mode, escapeshellarg($path));
|
||||||
|
if (@!ssh2_exec($this->connection, $cmd)) {
|
||||||
|
throw new FileTransferException('Cannot change permissions of @path.', NULL, array('@path' => $path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the form to configure the FileTransfer class for SSH.
|
||||||
|
*/
|
||||||
|
public function getSettingsForm() {
|
||||||
|
$form = parent::getSettingsForm();
|
||||||
|
$form['advanced']['port']['#default_value'] = 22;
|
||||||
|
return $form;
|
||||||
|
}
|
||||||
|
}
|
4801
includes/form.inc
Normal file
4801
includes/form.inc
Normal file
File diff suppressed because it is too large
Load diff
145
includes/graph.inc
Normal file
145
includes/graph.inc
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Directed acyclic graph manipulation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a depth-first search and sort on a directed acyclic graph.
|
||||||
|
*
|
||||||
|
* @param $graph
|
||||||
|
* A three dimensional associated array, with the first keys being the names
|
||||||
|
* of the vertices, these can be strings or numbers. The second key is
|
||||||
|
* 'edges' and the third one are again vertices, each such key representing
|
||||||
|
* an edge. Values of array elements are copied over.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* @code
|
||||||
|
* $graph[1]['edges'][2] = 1;
|
||||||
|
* $graph[2]['edges'][3] = 1;
|
||||||
|
* $graph[2]['edges'][4] = 1;
|
||||||
|
* $graph[3]['edges'][4] = 1;
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* On return you will also have:
|
||||||
|
* @code
|
||||||
|
* $graph[1]['paths'][2] = 1;
|
||||||
|
* $graph[1]['paths'][3] = 1;
|
||||||
|
* $graph[2]['reverse_paths'][1] = 1;
|
||||||
|
* $graph[3]['reverse_paths'][1] = 1;
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The passed-in $graph with more secondary keys filled in:
|
||||||
|
* - 'paths': Contains a list of vertices than can be reached on a path from
|
||||||
|
* this vertex.
|
||||||
|
* - 'reverse_paths': Contains a list of vertices that has a path from them
|
||||||
|
* to this vertex.
|
||||||
|
* - 'weight': If there is a path from a vertex to another then the weight of
|
||||||
|
* the latter is higher.
|
||||||
|
* - 'component': Vertices in the same component have the same component
|
||||||
|
* identifier.
|
||||||
|
*
|
||||||
|
* @see _drupal_depth_first_search()
|
||||||
|
*/
|
||||||
|
function drupal_depth_first_search(&$graph) {
|
||||||
|
$state = array(
|
||||||
|
// The order of last visit of the depth first search. This is the reverse
|
||||||
|
// of the topological order if the graph is acyclic.
|
||||||
|
'last_visit_order' => array(),
|
||||||
|
// The components of the graph.
|
||||||
|
'components' => array(),
|
||||||
|
);
|
||||||
|
// Perform the actual search.
|
||||||
|
foreach ($graph as $start => $data) {
|
||||||
|
_drupal_depth_first_search($graph, $state, $start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We do such a numbering that every component starts with 0. This is useful
|
||||||
|
// for module installs as we can install every 0 weighted module in one
|
||||||
|
// request, and then every 1 weighted etc.
|
||||||
|
$component_weights = array();
|
||||||
|
|
||||||
|
foreach ($state['last_visit_order'] as $vertex) {
|
||||||
|
$component = $graph[$vertex]['component'];
|
||||||
|
if (!isset($component_weights[$component])) {
|
||||||
|
$component_weights[$component] = 0;
|
||||||
|
}
|
||||||
|
$graph[$vertex]['weight'] = $component_weights[$component]--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a depth-first search on a graph.
|
||||||
|
*
|
||||||
|
* @param $graph
|
||||||
|
* A three dimensional associated graph array.
|
||||||
|
* @param $state
|
||||||
|
* An associative array. The key 'last_visit_order' stores a list of the
|
||||||
|
* vertices visited. The key components stores list of vertices belonging
|
||||||
|
* to the same the component.
|
||||||
|
* @param $start
|
||||||
|
* An arbitrary vertex where we started traversing the graph.
|
||||||
|
* @param $component
|
||||||
|
* The component of the last vertex.
|
||||||
|
*
|
||||||
|
* @see drupal_depth_first_search()
|
||||||
|
*/
|
||||||
|
function _drupal_depth_first_search(&$graph, &$state, $start, &$component = NULL) {
|
||||||
|
// Assign new component for each new vertex, i.e. when not called recursively.
|
||||||
|
if (!isset($component)) {
|
||||||
|
$component = $start;
|
||||||
|
}
|
||||||
|
// Nothing to do, if we already visited this vertex.
|
||||||
|
if (isset($graph[$start]['paths'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Mark $start as visited.
|
||||||
|
$graph[$start]['paths'] = array();
|
||||||
|
|
||||||
|
// Assign $start to the current component.
|
||||||
|
$graph[$start]['component'] = $component;
|
||||||
|
$state['components'][$component][] = $start;
|
||||||
|
|
||||||
|
// Visit edges of $start.
|
||||||
|
if (isset($graph[$start]['edges'])) {
|
||||||
|
foreach ($graph[$start]['edges'] as $end => $v) {
|
||||||
|
// Mark that $start can reach $end.
|
||||||
|
$graph[$start]['paths'][$end] = $v;
|
||||||
|
|
||||||
|
if (isset($graph[$end]['component']) && $component != $graph[$end]['component']) {
|
||||||
|
// This vertex already has a component, use that from now on and
|
||||||
|
// reassign all the previously explored vertices.
|
||||||
|
$new_component = $graph[$end]['component'];
|
||||||
|
foreach ($state['components'][$component] as $vertex) {
|
||||||
|
$graph[$vertex]['component'] = $new_component;
|
||||||
|
$state['components'][$new_component][] = $vertex;
|
||||||
|
}
|
||||||
|
unset($state['components'][$component]);
|
||||||
|
$component = $new_component;
|
||||||
|
}
|
||||||
|
// Only visit existing vertices.
|
||||||
|
if (isset($graph[$end])) {
|
||||||
|
// Visit the connected vertex.
|
||||||
|
_drupal_depth_first_search($graph, $state, $end, $component);
|
||||||
|
|
||||||
|
// All vertices reachable by $end are also reachable by $start.
|
||||||
|
$graph[$start]['paths'] += $graph[$end]['paths'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that any other subgraph has been explored, add $start to all reverse
|
||||||
|
// paths.
|
||||||
|
foreach ($graph[$start]['paths'] as $end => $v) {
|
||||||
|
if (isset($graph[$end])) {
|
||||||
|
$graph[$end]['reverse_paths'][$start] = $v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the order of the last visit. This is the reverse of the
|
||||||
|
// topological order if the graph is acyclic.
|
||||||
|
$state['last_visit_order'][] = $start;
|
||||||
|
}
|
435
includes/image.inc
Normal file
435
includes/image.inc
Normal file
|
@ -0,0 +1,435 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* API for manipulating images.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup image Image toolkits
|
||||||
|
* @{
|
||||||
|
* Functions for image file manipulations.
|
||||||
|
*
|
||||||
|
* Drupal's image toolkits provide an abstraction layer for common image file
|
||||||
|
* manipulations like scaling, cropping, and rotating. The abstraction frees
|
||||||
|
* module authors from the need to support multiple image libraries, and it
|
||||||
|
* allows site administrators to choose the library that's best for them.
|
||||||
|
*
|
||||||
|
* PHP includes the GD library by default so a GD toolkit is installed with
|
||||||
|
* Drupal. Other toolkits like ImageMagick are available from contrib modules.
|
||||||
|
* GD works well for small images, but using it with larger files may cause PHP
|
||||||
|
* to run out of memory. In contrast the ImageMagick library does not suffer
|
||||||
|
* from this problem, but it requires the ISP to have installed additional
|
||||||
|
* software.
|
||||||
|
*
|
||||||
|
* Image toolkits are discovered based on the associated module's
|
||||||
|
* hook_image_toolkits. Additionally the image toolkit include file
|
||||||
|
* must be identified in the files array in the module.info file. The
|
||||||
|
* toolkit must then be enabled using the admin/config/media/image-toolkit
|
||||||
|
* form.
|
||||||
|
*
|
||||||
|
* Only one toolkit may be selected at a time. If a module author wishes to call
|
||||||
|
* a specific toolkit they can check that it is installed by calling
|
||||||
|
* image_get_available_toolkits(), and then calling its functions directly.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of available toolkits.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array with the toolkit names as keys and the descriptions as values.
|
||||||
|
*/
|
||||||
|
function image_get_available_toolkits() {
|
||||||
|
// hook_image_toolkits returns an array of toolkit names.
|
||||||
|
$toolkits = module_invoke_all('image_toolkits');
|
||||||
|
|
||||||
|
$output = array();
|
||||||
|
foreach ($toolkits as $name => $info) {
|
||||||
|
// Only allow modules that aren't marked as unavailable.
|
||||||
|
if ($info['available']) {
|
||||||
|
$output[$name] = $info['title'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the currently used toolkit.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* String containing the name of the selected toolkit, or FALSE on error.
|
||||||
|
*/
|
||||||
|
function image_get_toolkit() {
|
||||||
|
static $toolkit;
|
||||||
|
|
||||||
|
if (!isset($toolkit)) {
|
||||||
|
$toolkits = image_get_available_toolkits();
|
||||||
|
$toolkit = variable_get('image_toolkit', 'gd');
|
||||||
|
if (!isset($toolkits[$toolkit]) || !function_exists('image_' . $toolkit . '_load')) {
|
||||||
|
// The selected toolkit isn't available so return the first one found. If
|
||||||
|
// none are available this will return FALSE.
|
||||||
|
reset($toolkits);
|
||||||
|
$toolkit = key($toolkits);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $toolkit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes the given method using the currently selected toolkit.
|
||||||
|
*
|
||||||
|
* @param $method
|
||||||
|
* A string containing the method to invoke.
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $params
|
||||||
|
* An optional array of parameters to pass to the toolkit method.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Mixed values (typically Boolean indicating successful operation).
|
||||||
|
*/
|
||||||
|
function image_toolkit_invoke($method, stdClass $image, array $params = array()) {
|
||||||
|
$function = 'image_' . $image->toolkit . '_' . $method;
|
||||||
|
if (function_exists($function)) {
|
||||||
|
array_unshift($params, $image);
|
||||||
|
return call_user_func_array($function, $params);
|
||||||
|
}
|
||||||
|
watchdog('image', 'The selected image handling toolkit %toolkit can not correctly process %function.', array('%toolkit' => $image->toolkit, '%function' => $function), WATCHDOG_ERROR);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets details about an image.
|
||||||
|
*
|
||||||
|
* Drupal supports GIF, JPG and PNG file formats when used with the GD
|
||||||
|
* toolkit, and may support others, depending on which toolkits are
|
||||||
|
* installed.
|
||||||
|
*
|
||||||
|
* @param $filepath
|
||||||
|
* String specifying the path of the image file.
|
||||||
|
* @param $toolkit
|
||||||
|
* An optional image toolkit name to override the default.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* FALSE, if the file could not be found or is not an image. Otherwise, a
|
||||||
|
* keyed array containing information about the image:
|
||||||
|
* - "width": Width, in pixels.
|
||||||
|
* - "height": Height, in pixels.
|
||||||
|
* - "extension": Commonly used file extension for the image.
|
||||||
|
* - "mime_type": MIME type ('image/jpeg', 'image/gif', 'image/png').
|
||||||
|
* - "file_size": File size in bytes.
|
||||||
|
*/
|
||||||
|
function image_get_info($filepath, $toolkit = FALSE) {
|
||||||
|
$details = FALSE;
|
||||||
|
if (!is_file($filepath) && !is_uploaded_file($filepath)) {
|
||||||
|
return $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$toolkit) {
|
||||||
|
$toolkit = image_get_toolkit();
|
||||||
|
}
|
||||||
|
if ($toolkit) {
|
||||||
|
$image = new stdClass();
|
||||||
|
$image->source = $filepath;
|
||||||
|
$image->toolkit = $toolkit;
|
||||||
|
$details = image_toolkit_invoke('get_info', $image);
|
||||||
|
if (isset($details) && is_array($details)) {
|
||||||
|
$details['file_size'] = filesize($filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales an image to the exact width and height given.
|
||||||
|
*
|
||||||
|
* This function achieves the target aspect ratio by cropping the original image
|
||||||
|
* equally on both sides, or equally on the top and bottom. This function is
|
||||||
|
* useful to create uniform sized avatars from larger images.
|
||||||
|
*
|
||||||
|
* The resulting image always has the exact target dimensions.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $width
|
||||||
|
* The target width, in pixels.
|
||||||
|
* @param $height
|
||||||
|
* The target height, in pixels.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_resize()
|
||||||
|
* @see image_crop()
|
||||||
|
*/
|
||||||
|
function image_scale_and_crop(stdClass $image, $width, $height) {
|
||||||
|
$scale = max($width / $image->info['width'], $height / $image->info['height']);
|
||||||
|
$x = ($image->info['width'] * $scale - $width) / 2;
|
||||||
|
$y = ($image->info['height'] * $scale - $height) / 2;
|
||||||
|
|
||||||
|
if (image_resize($image, $image->info['width'] * $scale, $image->info['height'] * $scale)) {
|
||||||
|
return image_crop($image, $x, $y, $width, $height);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales image dimensions while maintaining aspect ratio.
|
||||||
|
*
|
||||||
|
* The resulting dimensions can be smaller for one or both target dimensions.
|
||||||
|
*
|
||||||
|
* @param $dimensions
|
||||||
|
* Dimensions to be modified - an array with components width and height, in
|
||||||
|
* pixels.
|
||||||
|
* @param $width
|
||||||
|
* The target width, in pixels. If this value is NULL then the scaling will be
|
||||||
|
* based only on the height value.
|
||||||
|
* @param $height
|
||||||
|
* The target height, in pixels. If this value is NULL then the scaling will
|
||||||
|
* be based only on the width value.
|
||||||
|
* @param $upscale
|
||||||
|
* Boolean indicating that images smaller than the target dimensions will be
|
||||||
|
* scaled up. This generally results in a low quality image.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if $dimensions was modified, FALSE otherwise.
|
||||||
|
*
|
||||||
|
* @see image_scale()
|
||||||
|
*/
|
||||||
|
function image_dimensions_scale(array &$dimensions, $width = NULL, $height = NULL, $upscale = FALSE) {
|
||||||
|
$aspect = $dimensions['height'] / $dimensions['width'];
|
||||||
|
|
||||||
|
// Calculate one of the dimensions from the other target dimension,
|
||||||
|
// ensuring the same aspect ratio as the source dimensions. If one of the
|
||||||
|
// target dimensions is missing, that is the one that is calculated. If both
|
||||||
|
// are specified then the dimension calculated is the one that would not be
|
||||||
|
// calculated to be bigger than its target.
|
||||||
|
if (($width && !$height) || ($width && $height && $aspect < $height / $width)) {
|
||||||
|
$height = (int) round($width * $aspect);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$width = (int) round($height / $aspect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't upscale if the option isn't enabled.
|
||||||
|
if (!$upscale && ($width >= $dimensions['width'] || $height >= $dimensions['height'])) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dimensions['width'] = $width;
|
||||||
|
$dimensions['height'] = $height;
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scales an image while maintaining aspect ratio.
|
||||||
|
*
|
||||||
|
* The resulting image can be smaller for one or both target dimensions.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $width
|
||||||
|
* The target width, in pixels. If this value is NULL then the scaling will
|
||||||
|
* be based only on the height value.
|
||||||
|
* @param $height
|
||||||
|
* The target height, in pixels. If this value is NULL then the scaling will
|
||||||
|
* be based only on the width value.
|
||||||
|
* @param $upscale
|
||||||
|
* Boolean indicating that files smaller than the dimensions will be scaled
|
||||||
|
* up. This generally results in a low quality image.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_dimensions_scale()
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_scale_and_crop()
|
||||||
|
*/
|
||||||
|
function image_scale(stdClass $image, $width = NULL, $height = NULL, $upscale = FALSE) {
|
||||||
|
$dimensions = $image->info;
|
||||||
|
|
||||||
|
// Scale the dimensions - if they don't change then just return success.
|
||||||
|
if (!image_dimensions_scale($dimensions, $width, $height, $upscale)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return image_resize($image, $dimensions['width'], $dimensions['height']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resizes an image to the given dimensions (ignoring aspect ratio).
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $width
|
||||||
|
* The target width, in pixels.
|
||||||
|
* @param $height
|
||||||
|
* The target height, in pixels.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_gd_resize()
|
||||||
|
*/
|
||||||
|
function image_resize(stdClass $image, $width, $height) {
|
||||||
|
$width = (int) round($width);
|
||||||
|
$height = (int) round($height);
|
||||||
|
|
||||||
|
return image_toolkit_invoke('resize', $image, array($width, $height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates an image by the given number of degrees.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $degrees
|
||||||
|
* The number of (clockwise) degrees to rotate the image.
|
||||||
|
* @param $background
|
||||||
|
* An hexadecimal integer specifying the background color to use for the
|
||||||
|
* uncovered area of the image after the rotation. E.g. 0x000000 for black,
|
||||||
|
* 0xff00ff for magenta, and 0xffffff for white. For images that support
|
||||||
|
* transparency, this will default to transparent. Otherwise it will
|
||||||
|
* be white.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_gd_rotate()
|
||||||
|
*/
|
||||||
|
function image_rotate(stdClass $image, $degrees, $background = NULL) {
|
||||||
|
return image_toolkit_invoke('rotate', $image, array($degrees, $background));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crops an image to a rectangle specified by the given dimensions.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
* @param $x
|
||||||
|
* The top left coordinate, in pixels, of the crop area (x axis value).
|
||||||
|
* @param $y
|
||||||
|
* The top left coordinate, in pixels, of the crop area (y axis value).
|
||||||
|
* @param $width
|
||||||
|
* The target width, in pixels.
|
||||||
|
* @param $height
|
||||||
|
* The target height, in pixels.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_scale_and_crop()
|
||||||
|
* @see image_gd_crop()
|
||||||
|
*/
|
||||||
|
function image_crop(stdClass $image, $x, $y, $width, $height) {
|
||||||
|
$aspect = $image->info['height'] / $image->info['width'];
|
||||||
|
if (empty($height)) $height = $width / $aspect;
|
||||||
|
if (empty($width)) $width = $height * $aspect;
|
||||||
|
|
||||||
|
$width = (int) round($width);
|
||||||
|
$height = (int) round($height);
|
||||||
|
|
||||||
|
return image_toolkit_invoke('crop', $image, array($x, $y, $width, $height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an image to grayscale.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_gd_desaturate()
|
||||||
|
*/
|
||||||
|
function image_desaturate(stdClass $image) {
|
||||||
|
return image_toolkit_invoke('desaturate', $image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an image file and returns an image object.
|
||||||
|
*
|
||||||
|
* Any changes to the file are not saved until image_save() is called.
|
||||||
|
*
|
||||||
|
* @param $file
|
||||||
|
* Path to an image file.
|
||||||
|
* @param $toolkit
|
||||||
|
* An optional, image toolkit name to override the default.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An image object or FALSE if there was a problem loading the file. The
|
||||||
|
* image object has the following properties:
|
||||||
|
* - 'source' - The original file path.
|
||||||
|
* - 'info' - The array of information returned by image_get_info()
|
||||||
|
* - 'toolkit' - The name of the image toolkit requested when the image was
|
||||||
|
* loaded.
|
||||||
|
* Image toolkits may add additional properties. The caller is advised not to
|
||||||
|
* monkey about with them.
|
||||||
|
*
|
||||||
|
* @see image_save()
|
||||||
|
* @see image_get_info()
|
||||||
|
* @see image_get_available_toolkits()
|
||||||
|
* @see image_gd_load()
|
||||||
|
*/
|
||||||
|
function image_load($file, $toolkit = FALSE) {
|
||||||
|
if (!$toolkit) {
|
||||||
|
$toolkit = image_get_toolkit();
|
||||||
|
}
|
||||||
|
if ($toolkit) {
|
||||||
|
$image = new stdClass();
|
||||||
|
$image->source = $file;
|
||||||
|
$image->info = image_get_info($file, $toolkit);
|
||||||
|
if (isset($image->info) && is_array($image->info)) {
|
||||||
|
$image->toolkit = $toolkit;
|
||||||
|
if (image_toolkit_invoke('load', $image)) {
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the image and saves the changes to a file.
|
||||||
|
*
|
||||||
|
* @param $image
|
||||||
|
* An image object returned by image_load(). The object's 'info' property
|
||||||
|
* will be updated if the file is saved successfully.
|
||||||
|
* @param $destination
|
||||||
|
* Destination path where the image should be saved. If it is empty the
|
||||||
|
* original image file will be overwritten.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success, FALSE on failure.
|
||||||
|
*
|
||||||
|
* @see image_load()
|
||||||
|
* @see image_gd_save()
|
||||||
|
*/
|
||||||
|
function image_save(stdClass $image, $destination = NULL) {
|
||||||
|
if (empty($destination)) {
|
||||||
|
$destination = $image->source;
|
||||||
|
}
|
||||||
|
if ($return = image_toolkit_invoke('save', $image, array($destination))) {
|
||||||
|
// Clear the cached file size and refresh the image information.
|
||||||
|
clearstatcache();
|
||||||
|
$image->info = image_get_info($destination, $image->toolkit);
|
||||||
|
|
||||||
|
if (drupal_chmod($destination)) {
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "defgroup image".
|
||||||
|
*/
|
1925
includes/install.core.inc
Normal file
1925
includes/install.core.inc
Normal file
File diff suppressed because it is too large
Load diff
1340
includes/install.inc
Normal file
1340
includes/install.inc
Normal file
File diff suppressed because it is too large
Load diff
486
includes/iso.inc
Normal file
486
includes/iso.inc
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides a list of countries and languages based on ISO standards.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an array of all country code => country name pairs.
|
||||||
|
*
|
||||||
|
* Get an array of all country code => country name pairs as laid out
|
||||||
|
* in ISO 3166-1 alpha-2.
|
||||||
|
* Grabbed from location project (http://drupal.org/project/location).
|
||||||
|
* @return
|
||||||
|
* An array of all country code => country name pairs.
|
||||||
|
*/
|
||||||
|
function _country_get_predefined_list() {
|
||||||
|
static $countries;
|
||||||
|
|
||||||
|
if (isset($countries)) {
|
||||||
|
return $countries;
|
||||||
|
}
|
||||||
|
$t = get_t();
|
||||||
|
|
||||||
|
$countries = array(
|
||||||
|
'AD' => $t('Andorra'),
|
||||||
|
'AE' => $t('United Arab Emirates'),
|
||||||
|
'AF' => $t('Afghanistan'),
|
||||||
|
'AG' => $t('Antigua and Barbuda'),
|
||||||
|
'AI' => $t('Anguilla'),
|
||||||
|
'AL' => $t('Albania'),
|
||||||
|
'AM' => $t('Armenia'),
|
||||||
|
'AN' => $t('Netherlands Antilles'),
|
||||||
|
'AO' => $t('Angola'),
|
||||||
|
'AQ' => $t('Antarctica'),
|
||||||
|
'AR' => $t('Argentina'),
|
||||||
|
'AS' => $t('American Samoa'),
|
||||||
|
'AT' => $t('Austria'),
|
||||||
|
'AU' => $t('Australia'),
|
||||||
|
'AW' => $t('Aruba'),
|
||||||
|
'AX' => $t('Aland Islands'),
|
||||||
|
'AZ' => $t('Azerbaijan'),
|
||||||
|
'BA' => $t('Bosnia and Herzegovina'),
|
||||||
|
'BB' => $t('Barbados'),
|
||||||
|
'BD' => $t('Bangladesh'),
|
||||||
|
'BE' => $t('Belgium'),
|
||||||
|
'BF' => $t('Burkina Faso'),
|
||||||
|
'BG' => $t('Bulgaria'),
|
||||||
|
'BH' => $t('Bahrain'),
|
||||||
|
'BI' => $t('Burundi'),
|
||||||
|
'BJ' => $t('Benin'),
|
||||||
|
'BL' => $t('Saint Barthélemy'),
|
||||||
|
'BM' => $t('Bermuda'),
|
||||||
|
'BN' => $t('Brunei'),
|
||||||
|
'BO' => $t('Bolivia'),
|
||||||
|
'BQ' => $t('Caribbean Netherlands'),
|
||||||
|
'BR' => $t('Brazil'),
|
||||||
|
'BS' => $t('Bahamas'),
|
||||||
|
'BT' => $t('Bhutan'),
|
||||||
|
'BV' => $t('Bouvet Island'),
|
||||||
|
'BW' => $t('Botswana'),
|
||||||
|
'BY' => $t('Belarus'),
|
||||||
|
'BZ' => $t('Belize'),
|
||||||
|
'CA' => $t('Canada'),
|
||||||
|
'CC' => $t('Cocos (Keeling) Islands'),
|
||||||
|
'CD' => $t('Congo (Kinshasa)'),
|
||||||
|
'CF' => $t('Central African Republic'),
|
||||||
|
'CG' => $t('Congo (Brazzaville)'),
|
||||||
|
'CH' => $t('Switzerland'),
|
||||||
|
'CI' => $t('Ivory Coast'),
|
||||||
|
'CK' => $t('Cook Islands'),
|
||||||
|
'CL' => $t('Chile'),
|
||||||
|
'CM' => $t('Cameroon'),
|
||||||
|
'CN' => $t('China'),
|
||||||
|
'CO' => $t('Colombia'),
|
||||||
|
'CR' => $t('Costa Rica'),
|
||||||
|
'CU' => $t('Cuba'),
|
||||||
|
'CV' => $t('Cape Verde'),
|
||||||
|
'CW' => $t('Curaçao'),
|
||||||
|
'CX' => $t('Christmas Island'),
|
||||||
|
'CY' => $t('Cyprus'),
|
||||||
|
'CZ' => $t('Czech Republic'),
|
||||||
|
'DE' => $t('Germany'),
|
||||||
|
'DJ' => $t('Djibouti'),
|
||||||
|
'DK' => $t('Denmark'),
|
||||||
|
'DM' => $t('Dominica'),
|
||||||
|
'DO' => $t('Dominican Republic'),
|
||||||
|
'DZ' => $t('Algeria'),
|
||||||
|
'EC' => $t('Ecuador'),
|
||||||
|
'EE' => $t('Estonia'),
|
||||||
|
'EG' => $t('Egypt'),
|
||||||
|
'EH' => $t('Western Sahara'),
|
||||||
|
'ER' => $t('Eritrea'),
|
||||||
|
'ES' => $t('Spain'),
|
||||||
|
'ET' => $t('Ethiopia'),
|
||||||
|
'FI' => $t('Finland'),
|
||||||
|
'FJ' => $t('Fiji'),
|
||||||
|
'FK' => $t('Falkland Islands'),
|
||||||
|
'FM' => $t('Micronesia'),
|
||||||
|
'FO' => $t('Faroe Islands'),
|
||||||
|
'FR' => $t('France'),
|
||||||
|
'GA' => $t('Gabon'),
|
||||||
|
'GB' => $t('United Kingdom'),
|
||||||
|
'GD' => $t('Grenada'),
|
||||||
|
'GE' => $t('Georgia'),
|
||||||
|
'GF' => $t('French Guiana'),
|
||||||
|
'GG' => $t('Guernsey'),
|
||||||
|
'GH' => $t('Ghana'),
|
||||||
|
'GI' => $t('Gibraltar'),
|
||||||
|
'GL' => $t('Greenland'),
|
||||||
|
'GM' => $t('Gambia'),
|
||||||
|
'GN' => $t('Guinea'),
|
||||||
|
'GP' => $t('Guadeloupe'),
|
||||||
|
'GQ' => $t('Equatorial Guinea'),
|
||||||
|
'GR' => $t('Greece'),
|
||||||
|
'GS' => $t('South Georgia and the South Sandwich Islands'),
|
||||||
|
'GT' => $t('Guatemala'),
|
||||||
|
'GU' => $t('Guam'),
|
||||||
|
'GW' => $t('Guinea-Bissau'),
|
||||||
|
'GY' => $t('Guyana'),
|
||||||
|
'HK' => $t('Hong Kong S.A.R., China'),
|
||||||
|
'HM' => $t('Heard Island and McDonald Islands'),
|
||||||
|
'HN' => $t('Honduras'),
|
||||||
|
'HR' => $t('Croatia'),
|
||||||
|
'HT' => $t('Haiti'),
|
||||||
|
'HU' => $t('Hungary'),
|
||||||
|
'ID' => $t('Indonesia'),
|
||||||
|
'IE' => $t('Ireland'),
|
||||||
|
'IL' => $t('Israel'),
|
||||||
|
'IM' => $t('Isle of Man'),
|
||||||
|
'IN' => $t('India'),
|
||||||
|
'IO' => $t('British Indian Ocean Territory'),
|
||||||
|
'IQ' => $t('Iraq'),
|
||||||
|
'IR' => $t('Iran'),
|
||||||
|
'IS' => $t('Iceland'),
|
||||||
|
'IT' => $t('Italy'),
|
||||||
|
'JE' => $t('Jersey'),
|
||||||
|
'JM' => $t('Jamaica'),
|
||||||
|
'JO' => $t('Jordan'),
|
||||||
|
'JP' => $t('Japan'),
|
||||||
|
'KE' => $t('Kenya'),
|
||||||
|
'KG' => $t('Kyrgyzstan'),
|
||||||
|
'KH' => $t('Cambodia'),
|
||||||
|
'KI' => $t('Kiribati'),
|
||||||
|
'KM' => $t('Comoros'),
|
||||||
|
'KN' => $t('Saint Kitts and Nevis'),
|
||||||
|
'KP' => $t('North Korea'),
|
||||||
|
'KR' => $t('South Korea'),
|
||||||
|
'KW' => $t('Kuwait'),
|
||||||
|
'KY' => $t('Cayman Islands'),
|
||||||
|
'KZ' => $t('Kazakhstan'),
|
||||||
|
'LA' => $t('Laos'),
|
||||||
|
'LB' => $t('Lebanon'),
|
||||||
|
'LC' => $t('Saint Lucia'),
|
||||||
|
'LI' => $t('Liechtenstein'),
|
||||||
|
'LK' => $t('Sri Lanka'),
|
||||||
|
'LR' => $t('Liberia'),
|
||||||
|
'LS' => $t('Lesotho'),
|
||||||
|
'LT' => $t('Lithuania'),
|
||||||
|
'LU' => $t('Luxembourg'),
|
||||||
|
'LV' => $t('Latvia'),
|
||||||
|
'LY' => $t('Libya'),
|
||||||
|
'MA' => $t('Morocco'),
|
||||||
|
'MC' => $t('Monaco'),
|
||||||
|
'MD' => $t('Moldova'),
|
||||||
|
'ME' => $t('Montenegro'),
|
||||||
|
'MF' => $t('Saint Martin (French part)'),
|
||||||
|
'MG' => $t('Madagascar'),
|
||||||
|
'MH' => $t('Marshall Islands'),
|
||||||
|
'MK' => $t('Macedonia'),
|
||||||
|
'ML' => $t('Mali'),
|
||||||
|
'MM' => $t('Myanmar'),
|
||||||
|
'MN' => $t('Mongolia'),
|
||||||
|
'MO' => $t('Macao S.A.R., China'),
|
||||||
|
'MP' => $t('Northern Mariana Islands'),
|
||||||
|
'MQ' => $t('Martinique'),
|
||||||
|
'MR' => $t('Mauritania'),
|
||||||
|
'MS' => $t('Montserrat'),
|
||||||
|
'MT' => $t('Malta'),
|
||||||
|
'MU' => $t('Mauritius'),
|
||||||
|
'MV' => $t('Maldives'),
|
||||||
|
'MW' => $t('Malawi'),
|
||||||
|
'MX' => $t('Mexico'),
|
||||||
|
'MY' => $t('Malaysia'),
|
||||||
|
'MZ' => $t('Mozambique'),
|
||||||
|
'NA' => $t('Namibia'),
|
||||||
|
'NC' => $t('New Caledonia'),
|
||||||
|
'NE' => $t('Niger'),
|
||||||
|
'NF' => $t('Norfolk Island'),
|
||||||
|
'NG' => $t('Nigeria'),
|
||||||
|
'NI' => $t('Nicaragua'),
|
||||||
|
'NL' => $t('Netherlands'),
|
||||||
|
'NO' => $t('Norway'),
|
||||||
|
'NP' => $t('Nepal'),
|
||||||
|
'NR' => $t('Nauru'),
|
||||||
|
'NU' => $t('Niue'),
|
||||||
|
'NZ' => $t('New Zealand'),
|
||||||
|
'OM' => $t('Oman'),
|
||||||
|
'PA' => $t('Panama'),
|
||||||
|
'PE' => $t('Peru'),
|
||||||
|
'PF' => $t('French Polynesia'),
|
||||||
|
'PG' => $t('Papua New Guinea'),
|
||||||
|
'PH' => $t('Philippines'),
|
||||||
|
'PK' => $t('Pakistan'),
|
||||||
|
'PL' => $t('Poland'),
|
||||||
|
'PM' => $t('Saint Pierre and Miquelon'),
|
||||||
|
'PN' => $t('Pitcairn'),
|
||||||
|
'PR' => $t('Puerto Rico'),
|
||||||
|
'PS' => $t('Palestinian Territory'),
|
||||||
|
'PT' => $t('Portugal'),
|
||||||
|
'PW' => $t('Palau'),
|
||||||
|
'PY' => $t('Paraguay'),
|
||||||
|
'QA' => $t('Qatar'),
|
||||||
|
'RE' => $t('Reunion'),
|
||||||
|
'RO' => $t('Romania'),
|
||||||
|
'RS' => $t('Serbia'),
|
||||||
|
'RU' => $t('Russia'),
|
||||||
|
'RW' => $t('Rwanda'),
|
||||||
|
'SA' => $t('Saudi Arabia'),
|
||||||
|
'SB' => $t('Solomon Islands'),
|
||||||
|
'SC' => $t('Seychelles'),
|
||||||
|
'SD' => $t('Sudan'),
|
||||||
|
'SE' => $t('Sweden'),
|
||||||
|
'SG' => $t('Singapore'),
|
||||||
|
'SH' => $t('Saint Helena'),
|
||||||
|
'SI' => $t('Slovenia'),
|
||||||
|
'SJ' => $t('Svalbard and Jan Mayen'),
|
||||||
|
'SK' => $t('Slovakia'),
|
||||||
|
'SL' => $t('Sierra Leone'),
|
||||||
|
'SM' => $t('San Marino'),
|
||||||
|
'SN' => $t('Senegal'),
|
||||||
|
'SO' => $t('Somalia'),
|
||||||
|
'SR' => $t('Suriname'),
|
||||||
|
'SS' => $t('South Sudan'),
|
||||||
|
'ST' => $t('Sao Tome and Principe'),
|
||||||
|
'SV' => $t('El Salvador'),
|
||||||
|
'SX' => $t('Sint Maarten'),
|
||||||
|
'SY' => $t('Syria'),
|
||||||
|
'SZ' => $t('Swaziland'),
|
||||||
|
'TC' => $t('Turks and Caicos Islands'),
|
||||||
|
'TD' => $t('Chad'),
|
||||||
|
'TF' => $t('French Southern Territories'),
|
||||||
|
'TG' => $t('Togo'),
|
||||||
|
'TH' => $t('Thailand'),
|
||||||
|
'TJ' => $t('Tajikistan'),
|
||||||
|
'TK' => $t('Tokelau'),
|
||||||
|
'TL' => $t('Timor-Leste'),
|
||||||
|
'TM' => $t('Turkmenistan'),
|
||||||
|
'TN' => $t('Tunisia'),
|
||||||
|
'TO' => $t('Tonga'),
|
||||||
|
'TR' => $t('Turkey'),
|
||||||
|
'TT' => $t('Trinidad and Tobago'),
|
||||||
|
'TV' => $t('Tuvalu'),
|
||||||
|
'TW' => $t('Taiwan'),
|
||||||
|
'TZ' => $t('Tanzania'),
|
||||||
|
'UA' => $t('Ukraine'),
|
||||||
|
'UG' => $t('Uganda'),
|
||||||
|
'UM' => $t('United States Minor Outlying Islands'),
|
||||||
|
'US' => $t('United States'),
|
||||||
|
'UY' => $t('Uruguay'),
|
||||||
|
'UZ' => $t('Uzbekistan'),
|
||||||
|
'VA' => $t('Vatican'),
|
||||||
|
'VC' => $t('Saint Vincent and the Grenadines'),
|
||||||
|
'VE' => $t('Venezuela'),
|
||||||
|
'VG' => $t('British Virgin Islands'),
|
||||||
|
'VI' => $t('U.S. Virgin Islands'),
|
||||||
|
'VN' => $t('Vietnam'),
|
||||||
|
'VU' => $t('Vanuatu'),
|
||||||
|
'WF' => $t('Wallis and Futuna'),
|
||||||
|
'WS' => $t('Samoa'),
|
||||||
|
'YE' => $t('Yemen'),
|
||||||
|
'YT' => $t('Mayotte'),
|
||||||
|
'ZA' => $t('South Africa'),
|
||||||
|
'ZM' => $t('Zambia'),
|
||||||
|
'ZW' => $t('Zimbabwe'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort the list.
|
||||||
|
natcasesort($countries);
|
||||||
|
|
||||||
|
return $countries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ingroup locale-api-predefined List of predefined languages
|
||||||
|
* @{
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some of the common languages with their English and native names
|
||||||
|
*
|
||||||
|
* Based on ISO 639 and http://people.w3.org/rishida/names/languages.html
|
||||||
|
*/
|
||||||
|
function _locale_get_predefined_list() {
|
||||||
|
return array(
|
||||||
|
'aa' => array('Afar'),
|
||||||
|
'ab' => array('Abkhazian', 'аҧсуа бызшәа'),
|
||||||
|
'ae' => array('Avestan'),
|
||||||
|
'af' => array('Afrikaans'),
|
||||||
|
'ak' => array('Akan'),
|
||||||
|
'am' => array('Amharic', 'አማርኛ'),
|
||||||
|
'ar' => array('Arabic', /* Left-to-right marker "" */ 'العربية', LANGUAGE_RTL),
|
||||||
|
'as' => array('Assamese'),
|
||||||
|
'ast' => array('Asturian'),
|
||||||
|
'av' => array('Avar'),
|
||||||
|
'ay' => array('Aymara'),
|
||||||
|
'az' => array('Azerbaijani', 'azərbaycan'),
|
||||||
|
'ba' => array('Bashkir'),
|
||||||
|
'be' => array('Belarusian', 'Беларуская'),
|
||||||
|
'bg' => array('Bulgarian', 'Български'),
|
||||||
|
'bh' => array('Bihari'),
|
||||||
|
'bi' => array('Bislama'),
|
||||||
|
'bm' => array('Bambara', 'Bamanankan'),
|
||||||
|
'bn' => array('Bengali'),
|
||||||
|
'bo' => array('Tibetan'),
|
||||||
|
'br' => array('Breton'),
|
||||||
|
'bs' => array('Bosnian', 'Bosanski'),
|
||||||
|
'ca' => array('Catalan', 'Català'),
|
||||||
|
'ce' => array('Chechen'),
|
||||||
|
'ch' => array('Chamorro'),
|
||||||
|
'co' => array('Corsican'),
|
||||||
|
'cr' => array('Cree'),
|
||||||
|
'cs' => array('Czech', 'Čeština'),
|
||||||
|
'cu' => array('Old Slavonic'),
|
||||||
|
'cv' => array('Chuvash'),
|
||||||
|
'cy' => array('Welsh', 'Cymraeg'),
|
||||||
|
'da' => array('Danish', 'Dansk'),
|
||||||
|
'de' => array('German', 'Deutsch'),
|
||||||
|
'dv' => array('Maldivian'),
|
||||||
|
'dz' => array('Bhutani'),
|
||||||
|
'ee' => array('Ewe', 'Ɛʋɛ'),
|
||||||
|
'el' => array('Greek', 'Ελληνικά'),
|
||||||
|
'en' => array('English'),
|
||||||
|
'en-gb' => array('English, British'),
|
||||||
|
'eo' => array('Esperanto'),
|
||||||
|
'es' => array('Spanish', 'Español'),
|
||||||
|
'et' => array('Estonian', 'Eesti'),
|
||||||
|
'eu' => array('Basque', 'Euskera'),
|
||||||
|
'fa' => array('Persian', /* Left-to-right marker "" */ 'فارسی', LANGUAGE_RTL),
|
||||||
|
'ff' => array('Fulah', 'Fulfulde'),
|
||||||
|
'fi' => array('Finnish', 'Suomi'),
|
||||||
|
'fil' => array('Filipino'),
|
||||||
|
'fj' => array('Fiji'),
|
||||||
|
'fo' => array('Faeroese'),
|
||||||
|
'fr' => array('French', 'Français'),
|
||||||
|
'fy' => array('Frisian', 'Frysk'),
|
||||||
|
'ga' => array('Irish', 'Gaeilge'),
|
||||||
|
'gd' => array('Scots Gaelic'),
|
||||||
|
'gl' => array('Galician', 'Galego'),
|
||||||
|
'gn' => array('Guarani'),
|
||||||
|
'gsw-berne' => array('Swiss German'),
|
||||||
|
'gu' => array('Gujarati'),
|
||||||
|
'gv' => array('Manx'),
|
||||||
|
'ha' => array('Hausa'),
|
||||||
|
'he' => array('Hebrew', /* Left-to-right marker "" */ 'עברית', LANGUAGE_RTL),
|
||||||
|
'hi' => array('Hindi', 'हिन्दी'),
|
||||||
|
'ho' => array('Hiri Motu'),
|
||||||
|
'hr' => array('Croatian', 'Hrvatski'),
|
||||||
|
'ht' => array('Haitian Creole'),
|
||||||
|
'hu' => array('Hungarian', 'Magyar'),
|
||||||
|
'hy' => array('Armenian', 'Հայերեն'),
|
||||||
|
'hz' => array('Herero'),
|
||||||
|
'ia' => array('Interlingua'),
|
||||||
|
'id' => array('Indonesian', 'Bahasa Indonesia'),
|
||||||
|
'ie' => array('Interlingue'),
|
||||||
|
'ig' => array('Igbo'),
|
||||||
|
'ik' => array('Inupiak'),
|
||||||
|
'is' => array('Icelandic', 'Íslenska'),
|
||||||
|
'it' => array('Italian', 'Italiano'),
|
||||||
|
'iu' => array('Inuktitut'),
|
||||||
|
'ja' => array('Japanese', '日本語'),
|
||||||
|
'jv' => array('Javanese'),
|
||||||
|
'ka' => array('Georgian'),
|
||||||
|
'kg' => array('Kongo'),
|
||||||
|
'ki' => array('Kikuyu'),
|
||||||
|
'kj' => array('Kwanyama'),
|
||||||
|
'kk' => array('Kazakh', 'Қазақ'),
|
||||||
|
'kl' => array('Greenlandic'),
|
||||||
|
'km' => array('Cambodian'),
|
||||||
|
'kn' => array('Kannada', 'ಕನ್ನಡ'),
|
||||||
|
'ko' => array('Korean', '한국어'),
|
||||||
|
'kr' => array('Kanuri'),
|
||||||
|
'ks' => array('Kashmiri'),
|
||||||
|
'ku' => array('Kurdish', 'Kurdî'),
|
||||||
|
'kv' => array('Komi'),
|
||||||
|
'kw' => array('Cornish'),
|
||||||
|
'ky' => array('Kyrgyz', 'Кыргызча'),
|
||||||
|
'la' => array('Latin', 'Latina'),
|
||||||
|
'lb' => array('Luxembourgish'),
|
||||||
|
'lg' => array('Luganda'),
|
||||||
|
'ln' => array('Lingala'),
|
||||||
|
'lo' => array('Laothian'),
|
||||||
|
'lt' => array('Lithuanian', 'Lietuvių'),
|
||||||
|
'lv' => array('Latvian', 'Latviešu'),
|
||||||
|
'mg' => array('Malagasy'),
|
||||||
|
'mh' => array('Marshallese'),
|
||||||
|
'mi' => array('Māori'),
|
||||||
|
'mk' => array('Macedonian', 'Македонски'),
|
||||||
|
'ml' => array('Malayalam', 'മലയാളം'),
|
||||||
|
'mn' => array('Mongolian'),
|
||||||
|
'mo' => array('Moldavian'),
|
||||||
|
'mr' => array('Marathi'),
|
||||||
|
'ms' => array('Malay', 'Bahasa Melayu'),
|
||||||
|
'mt' => array('Maltese', 'Malti'),
|
||||||
|
'my' => array('Burmese'),
|
||||||
|
'na' => array('Nauru'),
|
||||||
|
'nd' => array('North Ndebele'),
|
||||||
|
'ne' => array('Nepali'),
|
||||||
|
'ng' => array('Ndonga'),
|
||||||
|
'nl' => array('Dutch', 'Nederlands'),
|
||||||
|
'nb' => array('Norwegian Bokmål', 'Bokmål'),
|
||||||
|
'nn' => array('Norwegian Nynorsk', 'Nynorsk'),
|
||||||
|
'nr' => array('South Ndebele'),
|
||||||
|
'nv' => array('Navajo'),
|
||||||
|
'ny' => array('Chichewa'),
|
||||||
|
'oc' => array('Occitan'),
|
||||||
|
'om' => array('Oromo'),
|
||||||
|
'or' => array('Oriya'),
|
||||||
|
'os' => array('Ossetian'),
|
||||||
|
'pa' => array('Punjabi'),
|
||||||
|
'pi' => array('Pali'),
|
||||||
|
'pl' => array('Polish', 'Polski'),
|
||||||
|
'ps' => array('Pashto', /* Left-to-right marker "" */ 'پښتو', LANGUAGE_RTL),
|
||||||
|
'pt' => array('Portuguese, International'),
|
||||||
|
'pt-pt' => array('Portuguese, Portugal', 'Português'),
|
||||||
|
'pt-br' => array('Portuguese, Brazil', 'Português'),
|
||||||
|
'qu' => array('Quechua'),
|
||||||
|
'rm' => array('Rhaeto-Romance'),
|
||||||
|
'rn' => array('Kirundi'),
|
||||||
|
'ro' => array('Romanian', 'Română'),
|
||||||
|
'ru' => array('Russian', 'Русский'),
|
||||||
|
'rw' => array('Kinyarwanda'),
|
||||||
|
'sa' => array('Sanskrit'),
|
||||||
|
'sc' => array('Sardinian'),
|
||||||
|
'sco' => array('Scots'),
|
||||||
|
'sd' => array('Sindhi'),
|
||||||
|
'se' => array('Northern Sami'),
|
||||||
|
'sg' => array('Sango'),
|
||||||
|
'sh' => array('Serbo-Croatian'),
|
||||||
|
'si' => array('Sinhala', 'සිංහල'),
|
||||||
|
'sk' => array('Slovak', 'Slovenčina'),
|
||||||
|
'sl' => array('Slovenian', 'Slovenščina'),
|
||||||
|
'sm' => array('Samoan'),
|
||||||
|
'sn' => array('Shona'),
|
||||||
|
'so' => array('Somali'),
|
||||||
|
'sq' => array('Albanian', 'Shqip'),
|
||||||
|
'sr' => array('Serbian', 'Српски'),
|
||||||
|
'ss' => array('Siswati'),
|
||||||
|
'st' => array('Sesotho'),
|
||||||
|
'su' => array('Sudanese'),
|
||||||
|
'sv' => array('Swedish', 'Svenska'),
|
||||||
|
'sw' => array('Swahili', 'Kiswahili'),
|
||||||
|
'ta' => array('Tamil', 'தமிழ்'),
|
||||||
|
'te' => array('Telugu', 'తెలుగు'),
|
||||||
|
'tg' => array('Tajik'),
|
||||||
|
'th' => array('Thai', 'ภาษาไทย'),
|
||||||
|
'ti' => array('Tigrinya'),
|
||||||
|
'tk' => array('Turkmen'),
|
||||||
|
'tl' => array('Tagalog'),
|
||||||
|
'tn' => array('Setswana'),
|
||||||
|
'to' => array('Tonga'),
|
||||||
|
'tr' => array('Turkish', 'Türkçe'),
|
||||||
|
'ts' => array('Tsonga'),
|
||||||
|
'tt' => array('Tatar', 'Tatarça'),
|
||||||
|
'tw' => array('Twi'),
|
||||||
|
'ty' => array('Tahitian'),
|
||||||
|
'ug' => array('Uyghur'),
|
||||||
|
'uk' => array('Ukrainian', 'Українська'),
|
||||||
|
'ur' => array('Urdu', /* Left-to-right marker "" */ 'اردو', LANGUAGE_RTL),
|
||||||
|
'uz' => array('Uzbek', "o'zbek"),
|
||||||
|
've' => array('Venda'),
|
||||||
|
'vi' => array('Vietnamese', 'Tiếng Việt'),
|
||||||
|
'wo' => array('Wolof'),
|
||||||
|
'xh' => array('Xhosa', 'isiXhosa'),
|
||||||
|
'xx-lolspeak' => array('Lolspeak'),
|
||||||
|
'yi' => array('Yiddish'),
|
||||||
|
'yo' => array('Yoruba', 'Yorùbá'),
|
||||||
|
'za' => array('Zhuang'),
|
||||||
|
'zh-hans' => array('Chinese, Simplified', '简体中文'),
|
||||||
|
'zh-hant' => array('Chinese, Traditional', '繁體中文'),
|
||||||
|
'zu' => array('Zulu', 'isiZulu'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @} End of "locale-api-languages-predefined"
|
||||||
|
*/
|
102
includes/json-encode.inc
Normal file
102
includes/json-encode.inc
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides a helper to properly encode HTML-safe JSON prior to PHP 5.3.0.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a PHP variable to HTML-safe JSON for PHP versions below 5.3.0.
|
||||||
|
*
|
||||||
|
* @see drupal_json_encode()
|
||||||
|
*/
|
||||||
|
function drupal_json_encode_helper($var) {
|
||||||
|
switch (gettype($var)) {
|
||||||
|
case 'boolean':
|
||||||
|
return $var ? 'true' : 'false'; // Lowercase necessary!
|
||||||
|
|
||||||
|
case 'integer':
|
||||||
|
case 'double':
|
||||||
|
return $var;
|
||||||
|
|
||||||
|
case 'resource':
|
||||||
|
case 'string':
|
||||||
|
// Always use Unicode escape sequences (\u0022) over JSON escape
|
||||||
|
// sequences (\") to prevent browsers interpreting these as
|
||||||
|
// special characters.
|
||||||
|
$replace_pairs = array(
|
||||||
|
// ", \ and U+0000 - U+001F must be escaped according to RFC 4627.
|
||||||
|
'\\' => '\u005C',
|
||||||
|
'"' => '\u0022',
|
||||||
|
"\x00" => '\u0000',
|
||||||
|
"\x01" => '\u0001',
|
||||||
|
"\x02" => '\u0002',
|
||||||
|
"\x03" => '\u0003',
|
||||||
|
"\x04" => '\u0004',
|
||||||
|
"\x05" => '\u0005',
|
||||||
|
"\x06" => '\u0006',
|
||||||
|
"\x07" => '\u0007',
|
||||||
|
"\x08" => '\u0008',
|
||||||
|
"\x09" => '\u0009',
|
||||||
|
"\x0a" => '\u000A',
|
||||||
|
"\x0b" => '\u000B',
|
||||||
|
"\x0c" => '\u000C',
|
||||||
|
"\x0d" => '\u000D',
|
||||||
|
"\x0e" => '\u000E',
|
||||||
|
"\x0f" => '\u000F',
|
||||||
|
"\x10" => '\u0010',
|
||||||
|
"\x11" => '\u0011',
|
||||||
|
"\x12" => '\u0012',
|
||||||
|
"\x13" => '\u0013',
|
||||||
|
"\x14" => '\u0014',
|
||||||
|
"\x15" => '\u0015',
|
||||||
|
"\x16" => '\u0016',
|
||||||
|
"\x17" => '\u0017',
|
||||||
|
"\x18" => '\u0018',
|
||||||
|
"\x19" => '\u0019',
|
||||||
|
"\x1a" => '\u001A',
|
||||||
|
"\x1b" => '\u001B',
|
||||||
|
"\x1c" => '\u001C',
|
||||||
|
"\x1d" => '\u001D',
|
||||||
|
"\x1e" => '\u001E',
|
||||||
|
"\x1f" => '\u001F',
|
||||||
|
// Prevent browsers from interpreting these as as special.
|
||||||
|
"'" => '\u0027',
|
||||||
|
'<' => '\u003C',
|
||||||
|
'>' => '\u003E',
|
||||||
|
'&' => '\u0026',
|
||||||
|
// Prevent browsers from interpreting the solidus as special and
|
||||||
|
// non-compliant JSON parsers from interpreting // as a comment.
|
||||||
|
'/' => '\u002F',
|
||||||
|
// While these are allowed unescaped according to ECMA-262, section
|
||||||
|
// 15.12.2, they cause problems in some JSON parsers.
|
||||||
|
"\xe2\x80\xa8" => '\u2028', // U+2028, Line Separator.
|
||||||
|
"\xe2\x80\xa9" => '\u2029', // U+2029, Paragraph Separator.
|
||||||
|
);
|
||||||
|
|
||||||
|
return '"' . strtr($var, $replace_pairs) . '"';
|
||||||
|
|
||||||
|
case 'array':
|
||||||
|
// Arrays in JSON can't be associative. If the array is empty or if it
|
||||||
|
// has sequential whole number keys starting with 0, it's not associative
|
||||||
|
// so we can go ahead and convert it as an array.
|
||||||
|
if (empty($var) || array_keys($var) === range(0, sizeof($var) - 1)) {
|
||||||
|
$output = array();
|
||||||
|
foreach ($var as $v) {
|
||||||
|
$output[] = drupal_json_encode_helper($v);
|
||||||
|
}
|
||||||
|
return '[ ' . implode(', ', $output) . ' ]';
|
||||||
|
}
|
||||||
|
// Otherwise, fall through to convert the array as an object.
|
||||||
|
|
||||||
|
case 'object':
|
||||||
|
$output = array();
|
||||||
|
foreach ($var as $k => $v) {
|
||||||
|
$output[] = drupal_json_encode_helper(strval($k)) . ':' . drupal_json_encode_helper($v);
|
||||||
|
}
|
||||||
|
return '{' . implode(', ', $output) . '}';
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
}
|
580
includes/language.inc
Normal file
580
includes/language.inc
Normal file
|
@ -0,0 +1,580 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Language Negotiation API.
|
||||||
|
*
|
||||||
|
* @see http://drupal.org/node/1497272
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No language negotiation. The default language is used.
|
||||||
|
*/
|
||||||
|
define('LANGUAGE_NEGOTIATION_DEFAULT', 'language-default');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup language_negotiation Language Negotiation API functionality
|
||||||
|
* @{
|
||||||
|
* Functions to customize the language types and the negotiation process.
|
||||||
|
*
|
||||||
|
* The language negotiation API is based on two major concepts:
|
||||||
|
* - Language types: types of translatable data (the types of data that a user
|
||||||
|
* can view or request).
|
||||||
|
* - Language negotiation providers: functions for determining which language to
|
||||||
|
* use to present a particular piece of data to the user.
|
||||||
|
* Both language types and language negotiation providers are customizable.
|
||||||
|
*
|
||||||
|
* Drupal defines three built-in language types:
|
||||||
|
* - Interface language: The page's main language, used to present translated
|
||||||
|
* user interface elements such as titles, labels, help text, and messages.
|
||||||
|
* - Content language: The language used to present content that is available
|
||||||
|
* in more than one language (see
|
||||||
|
* @link field_language Field Language API @endlink for details).
|
||||||
|
* - URL language: The language associated with URLs. When generating a URL,
|
||||||
|
* this value will be used by url() as a default if no explicit preference is
|
||||||
|
* provided.
|
||||||
|
* Modules can define additional language types through
|
||||||
|
* hook_language_types_info(), and alter existing language type definitions
|
||||||
|
* through hook_language_types_info_alter().
|
||||||
|
*
|
||||||
|
* Language types may be configurable or fixed. The language negotiation
|
||||||
|
* providers associated with a configurable language type can be explicitly
|
||||||
|
* set through the user interface. A fixed language type has predetermined
|
||||||
|
* (module-defined) language negotiation settings and, thus, does not appear in
|
||||||
|
* the configuration page. Here is a code snippet that makes the content
|
||||||
|
* language (which by default inherits the interface language's values)
|
||||||
|
* configurable:
|
||||||
|
* @code
|
||||||
|
* function mymodule_language_types_info_alter(&$language_types) {
|
||||||
|
* unset($language_types[LANGUAGE_TYPE_CONTENT]['fixed']);
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Every language type can have a different set of language negotiation
|
||||||
|
* providers assigned to it. Different language types often share the same
|
||||||
|
* language negotiation settings, but they can have independent settings if
|
||||||
|
* needed. If two language types are configured the same way, their language
|
||||||
|
* switcher configuration will be functionally identical and the same settings
|
||||||
|
* will act on both language types.
|
||||||
|
*
|
||||||
|
* Drupal defines the following built-in language negotiation providers:
|
||||||
|
* - URL: Determine the language from the URL (path prefix or domain).
|
||||||
|
* - Session: Determine the language from a request/session parameter.
|
||||||
|
* - User: Follow the user's language preference.
|
||||||
|
* - Browser: Determine the language from the browser's language settings.
|
||||||
|
* - Default language: Use the default site language.
|
||||||
|
* Language negotiation providers are simple callback functions that implement a
|
||||||
|
* particular logic to return a language code. For instance, the URL provider
|
||||||
|
* searches for a valid path prefix or domain name in the current request URL.
|
||||||
|
* If a language negotiation provider does not return a valid language code, the
|
||||||
|
* next provider associated to the language type (based on provider weight) is
|
||||||
|
* invoked.
|
||||||
|
*
|
||||||
|
* Modules can define additional language negotiation providers through
|
||||||
|
* hook_language_negotiation_info(), and alter existing providers through
|
||||||
|
* hook_language_negotiation_info_alter(). Here is an example snippet that lets
|
||||||
|
* path prefixes be ignored for administrative paths:
|
||||||
|
* @code
|
||||||
|
* function mymodule_language_negotiation_info_alter(&$negotiation_info) {
|
||||||
|
* // Replace the core function with our own function.
|
||||||
|
* module_load_include('language', 'inc', 'language.negotiation');
|
||||||
|
* $negotiation_info[LANGUAGE_NEGOTIATION_URL]['callbacks']['language'] = 'mymodule_from_url';
|
||||||
|
* $negotiation_info[LANGUAGE_NEGOTIATION_URL]['file'] = drupal_get_path('module', 'mymodule') . '/mymodule.module';
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* function mymodule_from_url($languages) {
|
||||||
|
* // Use the core URL language negotiation provider to get a valid language
|
||||||
|
* // code.
|
||||||
|
* module_load_include('language', 'inc', 'language.negotiation');
|
||||||
|
* $langcode = language_from_url($languages);
|
||||||
|
*
|
||||||
|
* // If we are on an administrative path, override with the default language.
|
||||||
|
* if (isset($_GET['q']) && strtok($_GET['q'], '/') == 'admin') {
|
||||||
|
* return language_default()->langcode;
|
||||||
|
* }
|
||||||
|
* return $langcode;
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* For more information, see
|
||||||
|
* @link http://drupal.org/node/1497272 Language Negotiation API @endlink
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the defined language types.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of language type names. The name will be used as the global
|
||||||
|
* variable name the language value will be stored in.
|
||||||
|
*/
|
||||||
|
function language_types_info() {
|
||||||
|
$language_types = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if (!isset($language_types)) {
|
||||||
|
$language_types = module_invoke_all('language_types_info');
|
||||||
|
// Let other modules alter the list of language types.
|
||||||
|
drupal_alter('language_types_info', $language_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $language_types;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the configurable language types.
|
||||||
|
*
|
||||||
|
* A language type maybe configurable or fixed. A fixed language type is a type
|
||||||
|
* whose language negotiation providers are module-defined and not altered
|
||||||
|
* through the user interface.
|
||||||
|
*
|
||||||
|
* @param $stored
|
||||||
|
* Optional. By default retrieves values from the 'language_types' variable to
|
||||||
|
* avoid unnecessary hook invocations.
|
||||||
|
* If set to FALSE retrieves values from the actual language type definitions.
|
||||||
|
* This allows to react to alterations performed on the definitions by modules
|
||||||
|
* installed after the 'language_types' variable is set.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of language type names.
|
||||||
|
*/
|
||||||
|
function language_types_configurable($stored = TRUE) {
|
||||||
|
$configurable = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if ($stored && !isset($configurable)) {
|
||||||
|
$types = variable_get('language_types', drupal_language_types());
|
||||||
|
$configurable = array_keys(array_filter($types));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$stored) {
|
||||||
|
$result = array();
|
||||||
|
foreach (language_types_info() as $type => $info) {
|
||||||
|
if (!isset($info['fixed'])) {
|
||||||
|
$result[] = $type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $configurable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables the given language types.
|
||||||
|
*
|
||||||
|
* @param $types
|
||||||
|
* An array of language types.
|
||||||
|
*/
|
||||||
|
function language_types_disable($types) {
|
||||||
|
$enabled_types = variable_get('language_types', drupal_language_types());
|
||||||
|
|
||||||
|
foreach ($types as $type) {
|
||||||
|
unset($enabled_types[$type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
variable_set('language_types', $enabled_types);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the language type configuration.
|
||||||
|
*/
|
||||||
|
function language_types_set() {
|
||||||
|
// Ensure that we are getting the defined language negotiation information. An
|
||||||
|
// invocation of module_enable() or module_disable() could outdate the cached
|
||||||
|
// information.
|
||||||
|
drupal_static_reset('language_types_info');
|
||||||
|
drupal_static_reset('language_negotiation_info');
|
||||||
|
|
||||||
|
// Determine which language types are configurable and which not by checking
|
||||||
|
// whether the 'fixed' key is defined. Non-configurable (fixed) language types
|
||||||
|
// have their language negotiation settings stored there.
|
||||||
|
$defined_providers = language_negotiation_info();
|
||||||
|
foreach (language_types_info() as $type => $info) {
|
||||||
|
if (isset($info['fixed'])) {
|
||||||
|
$language_types[$type] = FALSE;
|
||||||
|
$negotiation = array();
|
||||||
|
foreach ($info['fixed'] as $weight => $id) {
|
||||||
|
if (isset($defined_providers[$id])) {
|
||||||
|
$negotiation[$id] = $weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
language_negotiation_set($type, $negotiation);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$language_types[$type] = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save language types.
|
||||||
|
variable_set('language_types', $language_types);
|
||||||
|
|
||||||
|
// Ensure that subsequent calls of language_types_configurable() return the
|
||||||
|
// updated language type information.
|
||||||
|
drupal_static_reset('language_types_configurable');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a language negotiation provider is enabled for a language type.
|
||||||
|
*
|
||||||
|
* This has two possible behaviors:
|
||||||
|
* - If $provider_id is given return its ID if enabled, FALSE otherwise.
|
||||||
|
* - If no ID is passed the first enabled language negotiation provider is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* @param $type
|
||||||
|
* The language negotiation provider type.
|
||||||
|
* @param $provider_id
|
||||||
|
* The language negotiation provider ID.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The provider ID if it is enabled, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function language_negotiation_get($type, $provider_id = NULL) {
|
||||||
|
$negotiation = variable_get("language_negotiation_$type", array());
|
||||||
|
|
||||||
|
if (empty($negotiation)) {
|
||||||
|
return empty($provider_id) ? LANGUAGE_NEGOTIATION_DEFAULT : FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($provider_id)) {
|
||||||
|
return key($negotiation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($negotiation[$provider_id])) {
|
||||||
|
return $provider_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the language negotiation provider is enabled for any language type.
|
||||||
|
*
|
||||||
|
* @param $provider_id
|
||||||
|
* The language negotiation provider ID.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if there is at least one language type for which the given language
|
||||||
|
* provider is enabled, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function language_negotiation_get_any($provider_id) {
|
||||||
|
foreach (language_types_configurable() as $type) {
|
||||||
|
if (language_negotiation_get($type, $provider_id)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the language switch links for the given language.
|
||||||
|
*
|
||||||
|
* @param $type
|
||||||
|
* The language negotiation type.
|
||||||
|
* @param $path
|
||||||
|
* The internal path the switch links will be relative to.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A keyed array of links ready to be themed.
|
||||||
|
*/
|
||||||
|
function language_negotiation_get_switch_links($type, $path) {
|
||||||
|
$links = FALSE;
|
||||||
|
$negotiation = variable_get("language_negotiation_$type", array());
|
||||||
|
|
||||||
|
// Only get the languages if we have more than one.
|
||||||
|
if (count(language_list()) >= 2) {
|
||||||
|
$language = language_initialize($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($negotiation as $id => $provider) {
|
||||||
|
if (isset($provider['callbacks']['switcher'])) {
|
||||||
|
if (isset($provider['file'])) {
|
||||||
|
require_once DRUPAL_ROOT . '/' . $provider['file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$callback = $provider['callbacks']['switcher'];
|
||||||
|
$result = $callback($type, $path);
|
||||||
|
|
||||||
|
// Add support for WCAG 2.0's Language of Parts to add language identifiers.
|
||||||
|
// http://www.w3.org/TR/UNDERSTANDING-WCAG20/meaning-other-lang-id.html
|
||||||
|
foreach ($result as $langcode => $link) {
|
||||||
|
$result[$langcode]['attributes']['xml:lang'] = $langcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($result)) {
|
||||||
|
// Allow modules to provide translations for specific links.
|
||||||
|
drupal_alter('language_switch_links', $result, $type, $path);
|
||||||
|
$links = (object) array('links' => $result, 'provider' => $id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any unused language negotiation providers from the configuration.
|
||||||
|
*/
|
||||||
|
function language_negotiation_purge() {
|
||||||
|
// Ensure that we are getting the defined language negotiation information. An
|
||||||
|
// invocation of module_enable() or module_disable() could outdate the cached
|
||||||
|
// information.
|
||||||
|
drupal_static_reset('language_negotiation_info');
|
||||||
|
drupal_static_reset('language_types_info');
|
||||||
|
|
||||||
|
$defined_providers = language_negotiation_info();
|
||||||
|
foreach (language_types_info() as $type => $type_info) {
|
||||||
|
$weight = 0;
|
||||||
|
$negotiation = array();
|
||||||
|
foreach (variable_get("language_negotiation_$type", array()) as $id => $provider) {
|
||||||
|
if (isset($defined_providers[$id])) {
|
||||||
|
$negotiation[$id] = $weight++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
language_negotiation_set($type, $negotiation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a list of language negotiation providers.
|
||||||
|
*
|
||||||
|
* @param $type
|
||||||
|
* The language negotiation type.
|
||||||
|
* @param $language_providers
|
||||||
|
* An array of language negotiation provider weights keyed by provider ID.
|
||||||
|
* @see language_provider_weight()
|
||||||
|
*/
|
||||||
|
function language_negotiation_set($type, $language_providers) {
|
||||||
|
// Save only the necessary fields.
|
||||||
|
$provider_fields = array('callbacks', 'file', 'cache');
|
||||||
|
|
||||||
|
$negotiation = array();
|
||||||
|
$providers_weight = array();
|
||||||
|
$defined_providers = language_negotiation_info();
|
||||||
|
$default_types = language_types_configurable(FALSE);
|
||||||
|
|
||||||
|
// Initialize the providers weight list.
|
||||||
|
foreach ($language_providers as $id => $provider) {
|
||||||
|
$providers_weight[$id] = language_provider_weight($provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order providers list by weight.
|
||||||
|
asort($providers_weight);
|
||||||
|
|
||||||
|
foreach ($providers_weight as $id => $weight) {
|
||||||
|
if (isset($defined_providers[$id])) {
|
||||||
|
$provider = $defined_providers[$id];
|
||||||
|
// If the provider does not express any preference about types, make it
|
||||||
|
// available for any configurable type.
|
||||||
|
$types = array_flip(isset($provider['types']) ? $provider['types'] : $default_types);
|
||||||
|
// Check whether the provider is defined and has the right type.
|
||||||
|
if (isset($types[$type])) {
|
||||||
|
$provider_data = array();
|
||||||
|
foreach ($provider_fields as $field) {
|
||||||
|
if (isset($provider[$field])) {
|
||||||
|
$provider_data[$field] = $provider[$field];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$negotiation[$id] = $provider_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable_set("language_negotiation_$type", $negotiation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all the defined language negotiation providers.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of language negotiation providers.
|
||||||
|
*/
|
||||||
|
function language_negotiation_info() {
|
||||||
|
$language_providers = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if (!isset($language_providers)) {
|
||||||
|
// Collect all the module-defined language negotiation providers.
|
||||||
|
$language_providers = module_invoke_all('language_negotiation_info');
|
||||||
|
|
||||||
|
// Add the default language negotiation provider.
|
||||||
|
$language_providers[LANGUAGE_NEGOTIATION_DEFAULT] = array(
|
||||||
|
'callbacks' => array('language' => 'language_from_default'),
|
||||||
|
'weight' => 10,
|
||||||
|
'name' => t('Default'),
|
||||||
|
'description' => t('Use the default site language (@language_name).', array('@language_name' => language_default()->native)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Let other modules alter the list of language negotiation providers.
|
||||||
|
drupal_alter('language_negotiation_info', $language_providers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $language_providers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function used to cache the language negotiation providers results.
|
||||||
|
*
|
||||||
|
* @param $provider_id
|
||||||
|
* The language negotiation provider's identifier.
|
||||||
|
* @param $provider
|
||||||
|
* (optional) An associative array of information about the provider to be
|
||||||
|
* invoked (see hook_language_negotiation_info() for details). If not passed
|
||||||
|
* in, it will be loaded through language_negotiation_info().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A language object representing the language chosen by the provider.
|
||||||
|
*/
|
||||||
|
function language_provider_invoke($provider_id, $provider = NULL) {
|
||||||
|
$results = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if (!isset($results[$provider_id])) {
|
||||||
|
global $user;
|
||||||
|
|
||||||
|
// Get languages grouped by status and select only the enabled ones.
|
||||||
|
$languages = language_list('enabled');
|
||||||
|
$languages = $languages[1];
|
||||||
|
|
||||||
|
if (!isset($provider)) {
|
||||||
|
$providers = language_negotiation_info();
|
||||||
|
$provider = $providers[$provider_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($provider['file'])) {
|
||||||
|
require_once DRUPAL_ROOT . '/' . $provider['file'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the language negotiation provider has no cache preference or this is
|
||||||
|
// satisfied we can execute the callback.
|
||||||
|
$cache = !isset($provider['cache']) || $user->uid || $provider['cache'] == variable_get('cache', 0);
|
||||||
|
$callback = isset($provider['callbacks']['language']) ? $provider['callbacks']['language'] : FALSE;
|
||||||
|
$langcode = $cache && function_exists($callback) ? $callback($languages) : FALSE;
|
||||||
|
$results[$provider_id] = isset($languages[$langcode]) ? $languages[$langcode] : FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since objects are resources, we need to return a clone to prevent the
|
||||||
|
// language negotiation provider cache from being unintentionally altered. The
|
||||||
|
// same providers might be used with different language types based on
|
||||||
|
// configuration.
|
||||||
|
return !empty($results[$provider_id]) ? clone($results[$provider_id]) : $results[$provider_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the passed language negotiation provider weight or a default value.
|
||||||
|
*
|
||||||
|
* @param $provider
|
||||||
|
* A language negotiation provider data structure.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A numeric weight.
|
||||||
|
*/
|
||||||
|
function language_provider_weight($provider) {
|
||||||
|
$default = is_numeric($provider) ? $provider : 0;
|
||||||
|
return isset($provider['weight']) && is_numeric($provider['weight']) ? $provider['weight'] : $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chooses a language based on language negotiation provider settings.
|
||||||
|
*
|
||||||
|
* @param $type
|
||||||
|
* The language type key to find the language for.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The negotiated language object.
|
||||||
|
*/
|
||||||
|
function language_initialize($type) {
|
||||||
|
// Execute the language negotiation providers in the order they were set up and return the
|
||||||
|
// first valid language found.
|
||||||
|
$negotiation = variable_get("language_negotiation_$type", array());
|
||||||
|
|
||||||
|
foreach ($negotiation as $provider_id => $provider) {
|
||||||
|
$language = language_provider_invoke($provider_id, $provider);
|
||||||
|
if ($language) {
|
||||||
|
$language->provider = $provider_id;
|
||||||
|
return $language;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no other language was found use the default one.
|
||||||
|
$language = language_default();
|
||||||
|
$language->provider = LANGUAGE_NEGOTIATION_DEFAULT;
|
||||||
|
return $language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the default language negotiation provider.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The default language code.
|
||||||
|
*/
|
||||||
|
function language_from_default() {
|
||||||
|
return language_default()->language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits the given path into prefix and actual path.
|
||||||
|
*
|
||||||
|
* Parse the given path and return the language object identified by the prefix
|
||||||
|
* and the actual path.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path to split.
|
||||||
|
* @param $languages
|
||||||
|
* An array of valid languages.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array composed of:
|
||||||
|
* - A language object corresponding to the identified prefix on success,
|
||||||
|
* FALSE otherwise.
|
||||||
|
* - The path without the prefix on success, the given path otherwise.
|
||||||
|
*/
|
||||||
|
function language_url_split_prefix($path, $languages) {
|
||||||
|
$args = empty($path) ? array() : explode('/', $path);
|
||||||
|
$prefix = array_shift($args);
|
||||||
|
|
||||||
|
// Search prefix within enabled languages.
|
||||||
|
foreach ($languages as $language) {
|
||||||
|
if (!empty($language->prefix) && $language->prefix == $prefix) {
|
||||||
|
// Rebuild $path with the language removed.
|
||||||
|
return array($language, implode('/', $args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(FALSE, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the possible fallback languages ordered by language weight.
|
||||||
|
*
|
||||||
|
* @param
|
||||||
|
* (optional) The language type. Defaults to LANGUAGE_TYPE_CONTENT.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of language codes.
|
||||||
|
*/
|
||||||
|
function language_fallback_get_candidates($type = LANGUAGE_TYPE_CONTENT) {
|
||||||
|
$fallback_candidates = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if (!isset($fallback_candidates)) {
|
||||||
|
$fallback_candidates = array();
|
||||||
|
|
||||||
|
// Get languages ordered by weight.
|
||||||
|
// Use array keys to avoid duplicated entries.
|
||||||
|
foreach (language_list('weight') as $languages) {
|
||||||
|
foreach ($languages as $language) {
|
||||||
|
$fallback_candidates[$language->language] = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback_candidates = array_keys($fallback_candidates);
|
||||||
|
$fallback_candidates[] = LANGUAGE_NONE;
|
||||||
|
|
||||||
|
// Let other modules hook in and add/change candidates.
|
||||||
|
drupal_alter('language_fallback_candidates', $fallback_candidates);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fallback_candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "language_negotiation"
|
||||||
|
*/
|
2492
includes/locale.inc
Normal file
2492
includes/locale.inc
Normal file
File diff suppressed because it is too large
Load diff
274
includes/lock.inc
Normal file
274
includes/lock.inc
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* A database-mediated implementation of a locking mechanism.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup lock Locking mechanisms
|
||||||
|
* @{
|
||||||
|
* Functions to coordinate long-running operations across requests.
|
||||||
|
*
|
||||||
|
* In most environments, multiple Drupal page requests (a.k.a. threads or
|
||||||
|
* processes) will execute in parallel. This leads to potential conflicts or
|
||||||
|
* race conditions when two requests execute the same code at the same time. A
|
||||||
|
* common example of this is a rebuild like menu_rebuild() where we invoke many
|
||||||
|
* hook implementations to get and process data from all active modules, and
|
||||||
|
* then delete the current data in the database to insert the new afterwards.
|
||||||
|
*
|
||||||
|
* This is a cooperative, advisory lock system. Any long-running operation
|
||||||
|
* that could potentially be attempted in parallel by multiple requests should
|
||||||
|
* try to acquire a lock before proceeding. By obtaining a lock, one request
|
||||||
|
* notifies any other requests that a specific operation is in progress which
|
||||||
|
* must not be executed in parallel.
|
||||||
|
*
|
||||||
|
* To use this API, pick a unique name for the lock. A sensible choice is the
|
||||||
|
* name of the function performing the operation. A very simple example use of
|
||||||
|
* this API:
|
||||||
|
* @code
|
||||||
|
* function mymodule_long_operation() {
|
||||||
|
* if (lock_acquire('mymodule_long_operation')) {
|
||||||
|
* // Do the long operation here.
|
||||||
|
* // ...
|
||||||
|
* lock_release('mymodule_long_operation');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* If a function acquires a lock it should always release it when the
|
||||||
|
* operation is complete by calling lock_release(), as in the example.
|
||||||
|
*
|
||||||
|
* A function that has acquired a lock may attempt to renew a lock (extend the
|
||||||
|
* duration of the lock) by calling lock_acquire() again during the operation.
|
||||||
|
* Failure to renew a lock is indicative that another request has acquired
|
||||||
|
* the lock, and that the current operation may need to be aborted.
|
||||||
|
*
|
||||||
|
* If a function fails to acquire a lock it may either immediately return, or
|
||||||
|
* it may call lock_wait() if the rest of the current page request requires
|
||||||
|
* that the operation in question be complete. After lock_wait() returns,
|
||||||
|
* the function may again attempt to acquire the lock, or may simply allow the
|
||||||
|
* page request to proceed on the assumption that a parallel request completed
|
||||||
|
* the operation.
|
||||||
|
*
|
||||||
|
* lock_acquire() and lock_wait() will automatically break (delete) a lock
|
||||||
|
* whose duration has exceeded the timeout specified when it was acquired.
|
||||||
|
*
|
||||||
|
* Alternative implementations of this API (such as APC) may be substituted
|
||||||
|
* by setting the 'lock_inc' variable to an alternate include filepath. Since
|
||||||
|
* this is an API intended to support alternative implementations, code using
|
||||||
|
* this API should never rely upon specific implementation details (for example
|
||||||
|
* no code should look for or directly modify a lock in the {semaphore} table).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the locking system.
|
||||||
|
*/
|
||||||
|
function lock_initialize() {
|
||||||
|
global $locks;
|
||||||
|
|
||||||
|
$locks = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get this request's unique id.
|
||||||
|
*/
|
||||||
|
function _lock_id() {
|
||||||
|
// Do not use drupal_static(). This identifier refers to the current
|
||||||
|
// client request, and must not be changed under any circumstances
|
||||||
|
// else the shutdown handler may fail to release our locks.
|
||||||
|
static $lock_id;
|
||||||
|
|
||||||
|
if (!isset($lock_id)) {
|
||||||
|
// Assign a unique id.
|
||||||
|
$lock_id = uniqid(mt_rand(), TRUE);
|
||||||
|
// We only register a shutdown function if a lock is used.
|
||||||
|
drupal_register_shutdown_function('lock_release_all', $lock_id);
|
||||||
|
}
|
||||||
|
return $lock_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire (or renew) a lock, but do not block if it fails.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the lock. Limit of name's length is 255 characters.
|
||||||
|
* @param $timeout
|
||||||
|
* A number of seconds (float) before the lock expires (minimum of 0.001).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the lock was acquired, FALSE if it failed.
|
||||||
|
*/
|
||||||
|
function lock_acquire($name, $timeout = 30.0) {
|
||||||
|
global $locks;
|
||||||
|
|
||||||
|
// Insure that the timeout is at least 1 ms.
|
||||||
|
$timeout = max($timeout, 0.001);
|
||||||
|
$expire = microtime(TRUE) + $timeout;
|
||||||
|
if (isset($locks[$name])) {
|
||||||
|
// Try to extend the expiration of a lock we already acquired.
|
||||||
|
$success = (bool) db_update('semaphore')
|
||||||
|
->fields(array('expire' => $expire))
|
||||||
|
->condition('name', $name)
|
||||||
|
->condition('value', _lock_id())
|
||||||
|
->execute();
|
||||||
|
if (!$success) {
|
||||||
|
// The lock was broken.
|
||||||
|
unset($locks[$name]);
|
||||||
|
}
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Optimistically try to acquire the lock, then retry once if it fails.
|
||||||
|
// The first time through the loop cannot be a retry.
|
||||||
|
$retry = FALSE;
|
||||||
|
// We always want to do this code at least once.
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
db_insert('semaphore')
|
||||||
|
->fields(array(
|
||||||
|
'name' => $name,
|
||||||
|
'value' => _lock_id(),
|
||||||
|
'expire' => $expire,
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
// We track all acquired locks in the global variable.
|
||||||
|
$locks[$name] = TRUE;
|
||||||
|
// We never need to try again.
|
||||||
|
$retry = FALSE;
|
||||||
|
}
|
||||||
|
catch (PDOException $e) {
|
||||||
|
// Suppress the error. If this is our first pass through the loop,
|
||||||
|
// then $retry is FALSE. In this case, the insert must have failed
|
||||||
|
// meaning some other request acquired the lock but did not release it.
|
||||||
|
// We decide whether to retry by checking lock_may_be_available()
|
||||||
|
// Since this will break the lock in case it is expired.
|
||||||
|
$retry = $retry ? FALSE : lock_may_be_available($name);
|
||||||
|
}
|
||||||
|
// We only retry in case the first attempt failed, but we then broke
|
||||||
|
// an expired lock.
|
||||||
|
} while ($retry);
|
||||||
|
}
|
||||||
|
return isset($locks[$name]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if lock acquired by a different process may be available.
|
||||||
|
*
|
||||||
|
* If an existing lock has expired, it is removed.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the lock.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if there is no lock or it was removed, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function lock_may_be_available($name) {
|
||||||
|
$lock = db_query('SELECT expire, value FROM {semaphore} WHERE name = :name', array(':name' => $name))->fetchAssoc();
|
||||||
|
if (!$lock) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
$expire = (float) $lock['expire'];
|
||||||
|
$now = microtime(TRUE);
|
||||||
|
if ($now > $expire) {
|
||||||
|
// We check two conditions to prevent a race condition where another
|
||||||
|
// request acquired the lock and set a new expire time. We add a small
|
||||||
|
// number to $expire to avoid errors with float to string conversion.
|
||||||
|
return (bool) db_delete('semaphore')
|
||||||
|
->condition('name', $name)
|
||||||
|
->condition('value', $lock['value'])
|
||||||
|
->condition('expire', 0.0001 + $expire, '<=')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for a lock to be available.
|
||||||
|
*
|
||||||
|
* This function may be called in a request that fails to acquire a desired
|
||||||
|
* lock. This will block further execution until the lock is available or the
|
||||||
|
* specified delay in seconds is reached. This should not be used with locks
|
||||||
|
* that are acquired very frequently, since the lock is likely to be acquired
|
||||||
|
* again by a different request while waiting.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the lock.
|
||||||
|
* @param $delay
|
||||||
|
* The maximum number of seconds to wait, as an integer.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the lock holds, FALSE if it is available.
|
||||||
|
*/
|
||||||
|
function lock_wait($name, $delay = 30) {
|
||||||
|
// Pause the process for short periods between calling
|
||||||
|
// lock_may_be_available(). This prevents hitting the database with constant
|
||||||
|
// database queries while waiting, which could lead to performance issues.
|
||||||
|
// However, if the wait period is too long, there is the potential for a
|
||||||
|
// large number of processes to be blocked waiting for a lock, especially
|
||||||
|
// if the item being rebuilt is commonly requested. To address both of these
|
||||||
|
// concerns, begin waiting for 25ms, then add 25ms to the wait period each
|
||||||
|
// time until it reaches 500ms. After this point polling will continue every
|
||||||
|
// 500ms until $delay is reached.
|
||||||
|
|
||||||
|
// $delay is passed in seconds, but we will be using usleep(), which takes
|
||||||
|
// microseconds as a parameter. Multiply it by 1 million so that all
|
||||||
|
// further numbers are equivalent.
|
||||||
|
$delay = (int) $delay * 1000000;
|
||||||
|
|
||||||
|
// Begin sleeping at 25ms.
|
||||||
|
$sleep = 25000;
|
||||||
|
while ($delay > 0) {
|
||||||
|
// This function should only be called by a request that failed to get a
|
||||||
|
// lock, so we sleep first to give the parallel request a chance to finish
|
||||||
|
// and release the lock.
|
||||||
|
usleep($sleep);
|
||||||
|
// After each sleep, increase the value of $sleep until it reaches
|
||||||
|
// 500ms, to reduce the potential for a lock stampede.
|
||||||
|
$delay = $delay - $sleep;
|
||||||
|
$sleep = min(500000, $sleep + 25000, $delay);
|
||||||
|
if (lock_may_be_available($name)) {
|
||||||
|
// No longer need to wait.
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The caller must still wait longer to get the lock.
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release a lock previously acquired by lock_acquire().
|
||||||
|
*
|
||||||
|
* This will release the named lock if it is still held by the current request.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* The name of the lock.
|
||||||
|
*/
|
||||||
|
function lock_release($name) {
|
||||||
|
global $locks;
|
||||||
|
|
||||||
|
unset($locks[$name]);
|
||||||
|
db_delete('semaphore')
|
||||||
|
->condition('name', $name)
|
||||||
|
->condition('value', _lock_id())
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release all previously acquired locks.
|
||||||
|
*/
|
||||||
|
function lock_release_all($lock_id = NULL) {
|
||||||
|
global $locks;
|
||||||
|
|
||||||
|
$locks = array();
|
||||||
|
if (empty($lock_id)) {
|
||||||
|
$lock_id = _lock_id();
|
||||||
|
}
|
||||||
|
db_delete('semaphore')
|
||||||
|
->condition('value', $lock_id)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "defgroup lock".
|
||||||
|
*/
|
623
includes/mail.inc
Normal file
623
includes/mail.inc
Normal file
|
@ -0,0 +1,623 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* API functions for processing and sending e-mail.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-detect appropriate line endings for e-mails.
|
||||||
|
*
|
||||||
|
* $conf['mail_line_endings'] will override this setting.
|
||||||
|
*/
|
||||||
|
define('MAIL_LINE_ENDINGS', isset($_SERVER['WINDIR']) || (isset($_SERVER['SERVER_SOFTWARE']) && strpos($_SERVER['SERVER_SOFTWARE'], 'Win32') !== FALSE) ? "\r\n" : "\n");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes and optionally sends an e-mail message.
|
||||||
|
*
|
||||||
|
* Sending an e-mail works with defining an e-mail template (subject, text
|
||||||
|
* and possibly e-mail headers) and the replacement values to use in the
|
||||||
|
* appropriate places in the template. Processed e-mail templates are
|
||||||
|
* requested from hook_mail() from the module sending the e-mail. Any module
|
||||||
|
* can modify the composed e-mail message array using hook_mail_alter().
|
||||||
|
* Finally drupal_mail_system()->mail() sends the e-mail, which can
|
||||||
|
* be reused if the exact same composed e-mail is to be sent to multiple
|
||||||
|
* recipients.
|
||||||
|
*
|
||||||
|
* Finding out what language to send the e-mail with needs some consideration.
|
||||||
|
* If you send e-mail to a user, her preferred language should be fine, so
|
||||||
|
* use user_preferred_language(). If you send email based on form values
|
||||||
|
* filled on the page, there are two additional choices if you are not
|
||||||
|
* sending the e-mail to a user on the site. You can either use the language
|
||||||
|
* used to generate the page ($language global variable) or the site default
|
||||||
|
* language. See language_default(). The former is good if sending e-mail to
|
||||||
|
* the person filling the form, the later is good if you send e-mail to an
|
||||||
|
* address previously set up (like contact addresses in a contact form).
|
||||||
|
*
|
||||||
|
* Taking care of always using the proper language is even more important
|
||||||
|
* when sending e-mails in a row to multiple users. Hook_mail() abstracts
|
||||||
|
* whether the mail text comes from an administrator setting or is
|
||||||
|
* static in the source code. It should also deal with common mail tokens,
|
||||||
|
* only receiving $params which are unique to the actual e-mail at hand.
|
||||||
|
*
|
||||||
|
* An example:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* function example_notify($accounts) {
|
||||||
|
* foreach ($accounts as $account) {
|
||||||
|
* $params['account'] = $account;
|
||||||
|
* // example_mail() will be called based on the first drupal_mail() parameter.
|
||||||
|
* drupal_mail('example', 'notice', $account->mail, user_preferred_language($account), $params);
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* function example_mail($key, &$message, $params) {
|
||||||
|
* $data['user'] = $params['account'];
|
||||||
|
* $options['language'] = $message['language'];
|
||||||
|
* user_mail_tokens($variables, $data, $options);
|
||||||
|
* switch($key) {
|
||||||
|
* case 'notice':
|
||||||
|
* // If the recipient can receive such notices by instant-message, do
|
||||||
|
* // not send by email.
|
||||||
|
* if (example_im_send($key, $message, $params)) {
|
||||||
|
* $message['send'] = FALSE;
|
||||||
|
* break;
|
||||||
|
* }
|
||||||
|
* $langcode = $message['language']->language;
|
||||||
|
* $message['subject'] = t('Notification from !site', $variables, array('langcode' => $langcode));
|
||||||
|
* $message['body'][] = t("Dear !username\n\nThere is new content available on the site.", $variables, array('langcode' => $langcode));
|
||||||
|
* break;
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Another example, which uses drupal_mail() to format a message for sending
|
||||||
|
* later:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* $params = array('current_conditions' => $data);
|
||||||
|
* $to = 'user@example.com';
|
||||||
|
* $message = drupal_mail('example', 'notice', $to, $language, $params, FALSE);
|
||||||
|
* // Only add to the spool if sending was not canceled.
|
||||||
|
* if ($message['send']) {
|
||||||
|
* example_spool_message($message);
|
||||||
|
* }
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @param $module
|
||||||
|
* A module name to invoke hook_mail() on. The {$module}_mail() hook will be
|
||||||
|
* called to complete the $message structure which will already contain common
|
||||||
|
* defaults.
|
||||||
|
* @param $key
|
||||||
|
* A key to identify the e-mail sent. The final e-mail id for e-mail altering
|
||||||
|
* will be {$module}_{$key}.
|
||||||
|
* @param $to
|
||||||
|
* The e-mail address or addresses where the message will be sent to. The
|
||||||
|
* formatting of this string will be validated with the
|
||||||
|
* @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
|
||||||
|
* Some examples are:
|
||||||
|
* - user@example.com
|
||||||
|
* - user@example.com, anotheruser@example.com
|
||||||
|
* - User <user@example.com>
|
||||||
|
* - User <user@example.com>, Another User <anotheruser@example.com>
|
||||||
|
* @param $language
|
||||||
|
* Language object to use to compose the e-mail.
|
||||||
|
* @param $params
|
||||||
|
* Optional parameters to build the e-mail.
|
||||||
|
* @param $from
|
||||||
|
* Sets From to this value, if given.
|
||||||
|
* @param $send
|
||||||
|
* If TRUE, drupal_mail() will call drupal_mail_system()->mail() to deliver
|
||||||
|
* the message, and store the result in $message['result']. Modules
|
||||||
|
* implementing hook_mail_alter() may cancel sending by setting
|
||||||
|
* $message['send'] to FALSE.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The $message array structure containing all details of the
|
||||||
|
* message. If already sent ($send = TRUE), then the 'result' element
|
||||||
|
* will contain the success indicator of the e-mail, failure being already
|
||||||
|
* written to the watchdog. (Success means nothing more than the message being
|
||||||
|
* accepted at php-level, which still doesn't guarantee it to be delivered.)
|
||||||
|
*/
|
||||||
|
function drupal_mail($module, $key, $to, $language, $params = array(), $from = NULL, $send = TRUE) {
|
||||||
|
$default_from = variable_get('site_mail', ini_get('sendmail_from'));
|
||||||
|
|
||||||
|
// Bundle up the variables into a structured array for altering.
|
||||||
|
$message = array(
|
||||||
|
'id' => $module . '_' . $key,
|
||||||
|
'module' => $module,
|
||||||
|
'key' => $key,
|
||||||
|
'to' => $to,
|
||||||
|
'from' => isset($from) ? $from : $default_from,
|
||||||
|
'language' => $language,
|
||||||
|
'params' => $params,
|
||||||
|
'send' => TRUE,
|
||||||
|
'subject' => '',
|
||||||
|
'body' => array()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build the default headers
|
||||||
|
$headers = array(
|
||||||
|
'MIME-Version' => '1.0',
|
||||||
|
'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
|
||||||
|
'Content-Transfer-Encoding' => '8Bit',
|
||||||
|
'X-Mailer' => 'Drupal'
|
||||||
|
);
|
||||||
|
if ($default_from) {
|
||||||
|
// To prevent e-mail from looking like spam, the addresses in the Sender and
|
||||||
|
// Return-Path headers should have a domain authorized to use the originating
|
||||||
|
// SMTP server.
|
||||||
|
$headers['From'] = $headers['Sender'] = $headers['Return-Path'] = $default_from;
|
||||||
|
}
|
||||||
|
if ($from) {
|
||||||
|
$headers['From'] = $from;
|
||||||
|
}
|
||||||
|
$message['headers'] = $headers;
|
||||||
|
|
||||||
|
// Build the e-mail (get subject and body, allow additional headers) by
|
||||||
|
// invoking hook_mail() on this module. We cannot use module_invoke() as
|
||||||
|
// we need to have $message by reference in hook_mail().
|
||||||
|
if (function_exists($function = $module . '_mail')) {
|
||||||
|
$function($key, $message, $params);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke hook_mail_alter() to allow all modules to alter the resulting e-mail.
|
||||||
|
drupal_alter('mail', $message);
|
||||||
|
|
||||||
|
// Retrieve the responsible implementation for this message.
|
||||||
|
$system = drupal_mail_system($module, $key);
|
||||||
|
|
||||||
|
// Format the message body.
|
||||||
|
$message = $system->format($message);
|
||||||
|
|
||||||
|
// Optionally send e-mail.
|
||||||
|
if ($send) {
|
||||||
|
// The original caller requested sending. Sending was canceled by one or
|
||||||
|
// more hook_mail_alter() implementations. We set 'result' to NULL, because
|
||||||
|
// FALSE indicates an error in sending.
|
||||||
|
if (empty($message['send'])) {
|
||||||
|
$message['result'] = NULL;
|
||||||
|
}
|
||||||
|
// Sending was originally requested and was not canceled.
|
||||||
|
else {
|
||||||
|
$message['result'] = $system->mail($message);
|
||||||
|
// Log errors.
|
||||||
|
if (!$message['result']) {
|
||||||
|
watchdog('mail', 'Error sending e-mail (from %from to %to).', array('%from' => $message['from'], '%to' => $message['to']), WATCHDOG_ERROR);
|
||||||
|
drupal_set_message(t('Unable to send e-mail. Contact the site administrator if the problem persists.'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object that implements the MailSystemInterface interface.
|
||||||
|
*
|
||||||
|
* Allows for one or more custom mail backends to format and send mail messages
|
||||||
|
* composed using drupal_mail().
|
||||||
|
*
|
||||||
|
* An implementation needs to implement the following methods:
|
||||||
|
* - format: Allows to preprocess, format, and postprocess a mail
|
||||||
|
* message before it is passed to the sending system. By default, all messages
|
||||||
|
* may contain HTML and are converted to plain-text by the DefaultMailSystem
|
||||||
|
* implementation. For example, an alternative implementation could override
|
||||||
|
* the default implementation and additionally sanitize the HTML for usage in
|
||||||
|
* a MIME-encoded e-mail, but still invoking the DefaultMailSystem
|
||||||
|
* implementation to generate an alternate plain-text version for sending.
|
||||||
|
* - mail: Sends a message through a custom mail sending engine.
|
||||||
|
* By default, all messages are sent via PHP's mail() function by the
|
||||||
|
* DefaultMailSystem implementation.
|
||||||
|
*
|
||||||
|
* The selection of a particular implementation is controlled via the variable
|
||||||
|
* 'mail_system', which is a keyed array. The default implementation
|
||||||
|
* is the class whose name is the value of 'default-system' key. A more specific
|
||||||
|
* match first to key and then to module will be used in preference to the
|
||||||
|
* default. To specify a different class for all mail sent by one module, set
|
||||||
|
* the class name as the value for the key corresponding to the module name. To
|
||||||
|
* specify a class for a particular message sent by one module, set the class
|
||||||
|
* name as the value for the array key that is the message id, which is
|
||||||
|
* "${module}_${key}".
|
||||||
|
*
|
||||||
|
* For example to debug all mail sent by the user module by logging it to a
|
||||||
|
* file, you might set the variable as something like:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* array(
|
||||||
|
* 'default-system' => 'DefaultMailSystem',
|
||||||
|
* 'user' => 'DevelMailLog',
|
||||||
|
* );
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Finally, a different system can be specified for a specific e-mail ID (see
|
||||||
|
* the $key param), such as one of the keys used by the contact module:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* array(
|
||||||
|
* 'default-system' => 'DefaultMailSystem',
|
||||||
|
* 'user' => 'DevelMailLog',
|
||||||
|
* 'contact_page_autoreply' => 'DrupalDevNullMailSend',
|
||||||
|
* );
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Other possible uses for system include a mail-sending class that actually
|
||||||
|
* sends (or duplicates) each message to SMS, Twitter, instant message, etc, or
|
||||||
|
* a class that queues up a large number of messages for more efficient bulk
|
||||||
|
* sending or for sending via a remote gateway so as to reduce the load
|
||||||
|
* on the local server.
|
||||||
|
*
|
||||||
|
* @param $module
|
||||||
|
* The module name which was used by drupal_mail() to invoke hook_mail().
|
||||||
|
* @param $key
|
||||||
|
* A key to identify the e-mail sent. The final e-mail ID for the e-mail
|
||||||
|
* alter hook in drupal_mail() would have been {$module}_{$key}.
|
||||||
|
*
|
||||||
|
* @return MailSystemInterface
|
||||||
|
*/
|
||||||
|
function drupal_mail_system($module, $key) {
|
||||||
|
$instances = &drupal_static(__FUNCTION__, array());
|
||||||
|
|
||||||
|
$id = $module . '_' . $key;
|
||||||
|
|
||||||
|
$configuration = variable_get('mail_system', array('default-system' => 'DefaultMailSystem'));
|
||||||
|
|
||||||
|
// Look for overrides for the default class, starting from the most specific
|
||||||
|
// id, and falling back to the module name.
|
||||||
|
if (isset($configuration[$id])) {
|
||||||
|
$class = $configuration[$id];
|
||||||
|
}
|
||||||
|
elseif (isset($configuration[$module])) {
|
||||||
|
$class = $configuration[$module];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$class = $configuration['default-system'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($instances[$class])) {
|
||||||
|
$interfaces = class_implements($class);
|
||||||
|
if (isset($interfaces['MailSystemInterface'])) {
|
||||||
|
$instances[$class] = new $class();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new Exception(t('Class %class does not implement interface %interface', array('%class' => $class, '%interface' => 'MailSystemInterface')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $instances[$class];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface for pluggable mail back-ends.
|
||||||
|
*/
|
||||||
|
interface MailSystemInterface {
|
||||||
|
/**
|
||||||
|
* Format a message composed by drupal_mail() prior sending.
|
||||||
|
*
|
||||||
|
* @param $message
|
||||||
|
* A message array, as described in hook_mail_alter().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The formatted $message.
|
||||||
|
*/
|
||||||
|
public function format(array $message);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message composed by drupal_mail().
|
||||||
|
*
|
||||||
|
* @param $message
|
||||||
|
* Message array with at least the following elements:
|
||||||
|
* - id: A unique identifier of the e-mail type. Examples: 'contact_user_copy',
|
||||||
|
* 'user_password_reset'.
|
||||||
|
* - to: The mail address or addresses where the message will be sent to.
|
||||||
|
* The formatting of this string will be validated with the
|
||||||
|
* @link http://php.net/manual/filter.filters.validate.php PHP e-mail validation filter. @endlink
|
||||||
|
* Some examples are:
|
||||||
|
* - user@example.com
|
||||||
|
* - user@example.com, anotheruser@example.com
|
||||||
|
* - User <user@example.com>
|
||||||
|
* - User <user@example.com>, Another User <anotheruser@example.com>
|
||||||
|
* - subject: Subject of the e-mail to be sent. This must not contain any
|
||||||
|
* newline characters, or the mail may not be sent properly.
|
||||||
|
* - body: Message to be sent. Accepts both CRLF and LF line-endings.
|
||||||
|
* E-mail bodies must be wrapped. You can use drupal_wrap_mail() for
|
||||||
|
* smart plain text wrapping.
|
||||||
|
* - headers: Associative array containing all additional mail headers not
|
||||||
|
* defined by one of the other parameters. PHP's mail() looks for Cc and
|
||||||
|
* Bcc headers and sends the mail to addresses in these headers too.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the mail was successfully accepted for delivery, otherwise FALSE.
|
||||||
|
*/
|
||||||
|
public function mail(array $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs format=flowed soft wrapping for mail (RFC 3676).
|
||||||
|
*
|
||||||
|
* We use delsp=yes wrapping, but only break non-spaced languages when
|
||||||
|
* absolutely necessary to avoid compatibility issues.
|
||||||
|
*
|
||||||
|
* We deliberately use LF rather than CRLF, see drupal_mail().
|
||||||
|
*
|
||||||
|
* @param string $text
|
||||||
|
* The plain text to process.
|
||||||
|
* @param string $indent (optional)
|
||||||
|
* A string to indent the text with. Only '>' characters are repeated on
|
||||||
|
* subsequent wrapped lines. Others are replaced by spaces.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The content of the email as a string with formatting applied.
|
||||||
|
*/
|
||||||
|
function drupal_wrap_mail($text, $indent = '') {
|
||||||
|
// Convert CRLF into LF.
|
||||||
|
$text = str_replace("\r", '', $text);
|
||||||
|
// See if soft-wrapping is allowed.
|
||||||
|
$clean_indent = _drupal_html_to_text_clean($indent);
|
||||||
|
$soft = strpos($clean_indent, ' ') === FALSE;
|
||||||
|
// Check if the string has line breaks.
|
||||||
|
if (strpos($text, "\n") !== FALSE) {
|
||||||
|
// Remove trailing spaces to make existing breaks hard, but leave signature
|
||||||
|
// marker untouched (RFC 3676, Section 4.3).
|
||||||
|
$text = preg_replace('/(?(?<!^--) +\n| +\n)/m', "\n", $text);
|
||||||
|
// Wrap each line at the needed width.
|
||||||
|
$lines = explode("\n", $text);
|
||||||
|
array_walk($lines, '_drupal_wrap_mail_line', array('soft' => $soft, 'length' => strlen($indent)));
|
||||||
|
$text = implode("\n", $lines);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Wrap this line.
|
||||||
|
_drupal_wrap_mail_line($text, 0, array('soft' => $soft, 'length' => strlen($indent)));
|
||||||
|
}
|
||||||
|
// Empty lines with nothing but spaces.
|
||||||
|
$text = preg_replace('/^ +\n/m', "\n", $text);
|
||||||
|
// Space-stuff special lines.
|
||||||
|
$text = preg_replace('/^(>| |From)/m', ' $1', $text);
|
||||||
|
// Apply indentation. We only include non-'>' indentation on the first line.
|
||||||
|
$text = $indent . substr(preg_replace('/^/m', $clean_indent, $text), strlen($indent));
|
||||||
|
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms an HTML string into plain text, preserving its structure.
|
||||||
|
*
|
||||||
|
* The output will be suitable for use as 'format=flowed; delsp=yes' text
|
||||||
|
* (RFC 3676) and can be passed directly to drupal_mail() for sending.
|
||||||
|
*
|
||||||
|
* We deliberately use LF rather than CRLF, see drupal_mail().
|
||||||
|
*
|
||||||
|
* This function provides suitable alternatives for the following tags:
|
||||||
|
* <a> <em> <i> <strong> <b> <br> <p> <blockquote> <ul> <ol> <li> <dl> <dt>
|
||||||
|
* <dd> <h1> <h2> <h3> <h4> <h5> <h6> <hr>
|
||||||
|
*
|
||||||
|
* @param $string
|
||||||
|
* The string to be transformed.
|
||||||
|
* @param $allowed_tags (optional)
|
||||||
|
* If supplied, a list of tags that will be transformed. If omitted, all
|
||||||
|
* all supported tags are transformed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The transformed string.
|
||||||
|
*/
|
||||||
|
function drupal_html_to_text($string, $allowed_tags = NULL) {
|
||||||
|
// Cache list of supported tags.
|
||||||
|
static $supported_tags;
|
||||||
|
if (empty($supported_tags)) {
|
||||||
|
$supported_tags = array('a', 'em', 'i', 'strong', 'b', 'br', 'p', 'blockquote', 'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure only supported tags are kept.
|
||||||
|
$allowed_tags = isset($allowed_tags) ? array_intersect($supported_tags, $allowed_tags) : $supported_tags;
|
||||||
|
|
||||||
|
// Make sure tags, entities and attributes are well-formed and properly nested.
|
||||||
|
$string = _filter_htmlcorrector(filter_xss($string, $allowed_tags));
|
||||||
|
|
||||||
|
// Apply inline styles.
|
||||||
|
$string = preg_replace('!</?(em|i)((?> +)[^>]*)?>!i', '/', $string);
|
||||||
|
$string = preg_replace('!</?(strong|b)((?> +)[^>]*)?>!i', '*', $string);
|
||||||
|
|
||||||
|
// Replace inline <a> tags with the text of link and a footnote.
|
||||||
|
// 'See <a href="http://drupal.org">the Drupal site</a>' becomes
|
||||||
|
// 'See the Drupal site [1]' with the URL included as a footnote.
|
||||||
|
_drupal_html_to_mail_urls(NULL, TRUE);
|
||||||
|
$pattern = '@(<a[^>]+?href="([^"]*)"[^>]*?>(.+?)</a>)@i';
|
||||||
|
$string = preg_replace_callback($pattern, '_drupal_html_to_mail_urls', $string);
|
||||||
|
$urls = _drupal_html_to_mail_urls();
|
||||||
|
$footnotes = '';
|
||||||
|
if (count($urls)) {
|
||||||
|
$footnotes .= "\n";
|
||||||
|
for ($i = 0, $max = count($urls); $i < $max; $i++) {
|
||||||
|
$footnotes .= '[' . ($i + 1) . '] ' . $urls[$i] . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split tags from text.
|
||||||
|
$split = preg_split('/<([^>]+?)>/', $string, -1, PREG_SPLIT_DELIM_CAPTURE);
|
||||||
|
// Note: PHP ensures the array consists of alternating delimiters and literals
|
||||||
|
// and begins and ends with a literal (inserting $null as required).
|
||||||
|
|
||||||
|
$tag = FALSE; // Odd/even counter (tag or no tag)
|
||||||
|
$casing = NULL; // Case conversion function
|
||||||
|
$output = '';
|
||||||
|
$indent = array(); // All current indentation string chunks
|
||||||
|
$lists = array(); // Array of counters for opened lists
|
||||||
|
foreach ($split as $value) {
|
||||||
|
$chunk = NULL; // Holds a string ready to be formatted and output.
|
||||||
|
|
||||||
|
// Process HTML tags (but don't output any literally).
|
||||||
|
if ($tag) {
|
||||||
|
list($tagname) = explode(' ', strtolower($value), 2);
|
||||||
|
switch ($tagname) {
|
||||||
|
// List counters
|
||||||
|
case 'ul':
|
||||||
|
array_unshift($lists, '*');
|
||||||
|
break;
|
||||||
|
case 'ol':
|
||||||
|
array_unshift($lists, 1);
|
||||||
|
break;
|
||||||
|
case '/ul':
|
||||||
|
case '/ol':
|
||||||
|
array_shift($lists);
|
||||||
|
$chunk = ''; // Ensure blank new-line.
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Quotation/list markers, non-fancy headers
|
||||||
|
case 'blockquote':
|
||||||
|
// Format=flowed indentation cannot be mixed with lists.
|
||||||
|
$indent[] = count($lists) ? ' "' : '>';
|
||||||
|
break;
|
||||||
|
case 'li':
|
||||||
|
$indent[] = isset($lists[0]) && is_numeric($lists[0]) ? ' ' . $lists[0]++ . ') ' : ' * ';
|
||||||
|
break;
|
||||||
|
case 'dd':
|
||||||
|
$indent[] = ' ';
|
||||||
|
break;
|
||||||
|
case 'h3':
|
||||||
|
$indent[] = '.... ';
|
||||||
|
break;
|
||||||
|
case 'h4':
|
||||||
|
$indent[] = '.. ';
|
||||||
|
break;
|
||||||
|
case '/blockquote':
|
||||||
|
if (count($lists)) {
|
||||||
|
// Append closing quote for inline quotes (immediately).
|
||||||
|
$output = rtrim($output, "> \n") . "\"\n";
|
||||||
|
$chunk = ''; // Ensure blank new-line.
|
||||||
|
}
|
||||||
|
// Fall-through
|
||||||
|
case '/li':
|
||||||
|
case '/dd':
|
||||||
|
array_pop($indent);
|
||||||
|
break;
|
||||||
|
case '/h3':
|
||||||
|
case '/h4':
|
||||||
|
array_pop($indent);
|
||||||
|
case '/h5':
|
||||||
|
case '/h6':
|
||||||
|
$chunk = ''; // Ensure blank new-line.
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Fancy headers
|
||||||
|
case 'h1':
|
||||||
|
$indent[] = '======== ';
|
||||||
|
$casing = 'drupal_strtoupper';
|
||||||
|
break;
|
||||||
|
case 'h2':
|
||||||
|
$indent[] = '-------- ';
|
||||||
|
$casing = 'drupal_strtoupper';
|
||||||
|
break;
|
||||||
|
case '/h1':
|
||||||
|
case '/h2':
|
||||||
|
$casing = NULL;
|
||||||
|
// Pad the line with dashes.
|
||||||
|
$output = _drupal_html_to_text_pad($output, ($tagname == '/h1') ? '=' : '-', ' ');
|
||||||
|
array_pop($indent);
|
||||||
|
$chunk = ''; // Ensure blank new-line.
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Horizontal rulers
|
||||||
|
case 'hr':
|
||||||
|
// Insert immediately.
|
||||||
|
$output .= drupal_wrap_mail('', implode('', $indent)) . "\n";
|
||||||
|
$output = _drupal_html_to_text_pad($output, '-');
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Paragraphs and definition lists
|
||||||
|
case '/p':
|
||||||
|
case '/dl':
|
||||||
|
$chunk = ''; // Ensure blank new-line.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Process blocks of text.
|
||||||
|
else {
|
||||||
|
// Convert inline HTML text to plain text; not removing line-breaks or
|
||||||
|
// white-space, since that breaks newlines when sanitizing plain-text.
|
||||||
|
$value = trim(decode_entities($value));
|
||||||
|
if (drupal_strlen($value)) {
|
||||||
|
$chunk = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if there is something waiting to be output.
|
||||||
|
if (isset($chunk)) {
|
||||||
|
// Apply any necessary case conversion.
|
||||||
|
if (isset($casing)) {
|
||||||
|
$chunk = $casing($chunk);
|
||||||
|
}
|
||||||
|
// Format it and apply the current indentation.
|
||||||
|
$output .= drupal_wrap_mail($chunk, implode('', $indent)) . MAIL_LINE_ENDINGS;
|
||||||
|
// Remove non-quotation markers from indentation.
|
||||||
|
$indent = array_map('_drupal_html_to_text_clean', $indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tag = !$tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output . $footnotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps words on a single line.
|
||||||
|
*
|
||||||
|
* Callback for array_walk() winthin drupal_wrap_mail().
|
||||||
|
*/
|
||||||
|
function _drupal_wrap_mail_line(&$line, $key, $values) {
|
||||||
|
// Use soft-breaks only for purely quoted or unindented text.
|
||||||
|
$line = wordwrap($line, 77 - $values['length'], $values['soft'] ? " \n" : "\n");
|
||||||
|
// Break really long words at the maximum width allowed.
|
||||||
|
$line = wordwrap($line, 996 - $values['length'], $values['soft'] ? " \n" : "\n", TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of URLs and replaces them with placeholder tokens.
|
||||||
|
*
|
||||||
|
* Callback for preg_replace_callback() within drupal_html_to_text().
|
||||||
|
*/
|
||||||
|
function _drupal_html_to_mail_urls($match = NULL, $reset = FALSE) {
|
||||||
|
global $base_url, $base_path;
|
||||||
|
static $urls = array(), $regexp;
|
||||||
|
|
||||||
|
if ($reset) {
|
||||||
|
// Reset internal URL list.
|
||||||
|
$urls = array();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (empty($regexp)) {
|
||||||
|
$regexp = '@^' . preg_quote($base_path, '@') . '@';
|
||||||
|
}
|
||||||
|
if ($match) {
|
||||||
|
list(, , $url, $label) = $match;
|
||||||
|
// Ensure all URLs are absolute.
|
||||||
|
$urls[] = strpos($url, '://') ? $url : preg_replace($regexp, $base_url . '/', $url);
|
||||||
|
return $label . ' [' . count($urls) . ']';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces non-quotation markers from a given piece of indentation with spaces.
|
||||||
|
*
|
||||||
|
* Callback for array_map() within drupal_html_to_text().
|
||||||
|
*/
|
||||||
|
function _drupal_html_to_text_clean($indent) {
|
||||||
|
return preg_replace('/[^>]/', ' ', $indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pads the last line with the given character.
|
||||||
|
*
|
||||||
|
* @see drupal_html_to_text()
|
||||||
|
*/
|
||||||
|
function _drupal_html_to_text_pad($text, $pad, $prefix = '') {
|
||||||
|
// Remove last line break.
|
||||||
|
$text = substr($text, 0, -1);
|
||||||
|
// Calculate needed padding space and add it.
|
||||||
|
if (($p = strrpos($text, "\n")) === FALSE) {
|
||||||
|
$p = -1;
|
||||||
|
}
|
||||||
|
$n = max(0, 79 - (strlen($text) - $p) - strlen($prefix));
|
||||||
|
// Add prefix and padding, and restore linebreak.
|
||||||
|
return $text . $prefix . str_repeat($pad, $n) . "\n";
|
||||||
|
}
|
3952
includes/menu.inc
Normal file
3952
includes/menu.inc
Normal file
File diff suppressed because it is too large
Load diff
1165
includes/module.inc
Normal file
1165
includes/module.inc
Normal file
File diff suppressed because it is too large
Load diff
664
includes/pager.inc
Normal file
664
includes/pager.inc
Normal file
|
@ -0,0 +1,664 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Functions to aid in presenting database results as a set of pages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query extender for pager queries.
|
||||||
|
*
|
||||||
|
* This is the "default" pager mechanism. It creates a paged query with a fixed
|
||||||
|
* number of entries per page.
|
||||||
|
*/
|
||||||
|
class PagerDefault extends SelectQueryExtender {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The highest element we've autogenerated so far.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
static $maxElement = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of elements per page to allow.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $limit = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The unique ID of this pager on this page.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $element = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The count query that will be used for this pager.
|
||||||
|
*
|
||||||
|
* @var SelectQueryInterface
|
||||||
|
*/
|
||||||
|
protected $customCountQuery = FALSE;
|
||||||
|
|
||||||
|
public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
|
||||||
|
parent::__construct($query, $connection);
|
||||||
|
|
||||||
|
// Add pager tag. Do this here to ensure that it is always added before
|
||||||
|
// preExecute() is called.
|
||||||
|
$this->addTag('pager');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the execute method.
|
||||||
|
*
|
||||||
|
* Before we run the query, we need to add pager-based range() instructions
|
||||||
|
* to it.
|
||||||
|
*/
|
||||||
|
public function execute() {
|
||||||
|
|
||||||
|
// Add convenience tag to mark that this is an extended query. We have to
|
||||||
|
// do this in the constructor to ensure that it is set before preExecute()
|
||||||
|
// gets called.
|
||||||
|
if (!$this->preExecute($this)) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A NULL limit is the "kill switch" for pager queries.
|
||||||
|
if (empty($this->limit)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->ensureElement();
|
||||||
|
|
||||||
|
$total_items = $this->getCountQuery()->execute()->fetchField();
|
||||||
|
$current_page = pager_default_initialize($total_items, $this->limit, $this->element);
|
||||||
|
$this->range($current_page * $this->limit, $this->limit);
|
||||||
|
|
||||||
|
// Now that we've added our pager-based range instructions, run the query normally.
|
||||||
|
return $this->query->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that there is an element associated with this query.
|
||||||
|
* If an element was not specified previously, then the value of the
|
||||||
|
* $maxElement counter is taken, after which the counter is incremented.
|
||||||
|
*
|
||||||
|
* After running this method, access $this->element to get the element for this
|
||||||
|
* query.
|
||||||
|
*/
|
||||||
|
protected function ensureElement() {
|
||||||
|
if (!isset($this->element)) {
|
||||||
|
$this->element = self::$maxElement++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the count query object to use for this pager.
|
||||||
|
*
|
||||||
|
* You will rarely need to specify a count query directly. If not specified,
|
||||||
|
* one is generated off of the pager query itself.
|
||||||
|
*
|
||||||
|
* @param SelectQueryInterface $query
|
||||||
|
* The count query object. It must return a single row with a single column,
|
||||||
|
* which is the total number of records.
|
||||||
|
*/
|
||||||
|
public function setCountQuery(SelectQueryInterface $query) {
|
||||||
|
$this->customCountQuery = $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the count query for this pager.
|
||||||
|
*
|
||||||
|
* The count query may be specified manually or, by default, taken from the
|
||||||
|
* query we are extending.
|
||||||
|
*
|
||||||
|
* @return SelectQueryInterface
|
||||||
|
* A count query object.
|
||||||
|
*/
|
||||||
|
public function getCountQuery() {
|
||||||
|
if ($this->customCountQuery) {
|
||||||
|
return $this->customCountQuery;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return $this->query->countQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the maximum number of elements per page for this query.
|
||||||
|
*
|
||||||
|
* The default if not specified is 10 items per page.
|
||||||
|
*
|
||||||
|
* @param $limit
|
||||||
|
* An integer specifying the number of elements per page. If passed a false
|
||||||
|
* value (FALSE, 0, NULL), the pager is disabled.
|
||||||
|
*/
|
||||||
|
public function limit($limit = 10) {
|
||||||
|
$this->limit = $limit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify the element ID for this pager query.
|
||||||
|
*
|
||||||
|
* The element is used to differentiate different pager queries on the same
|
||||||
|
* page so that they may be operated independently. If you do not specify an
|
||||||
|
* element, every pager query on the page will get a unique element. If for
|
||||||
|
* whatever reason you want to explicitly define an element for a given query,
|
||||||
|
* you may do so here.
|
||||||
|
*
|
||||||
|
* Setting the element here also increments the static $maxElement counter,
|
||||||
|
* which is used for determining the $element when there's none specified.
|
||||||
|
*
|
||||||
|
* Note that no collision detection is done when setting an element ID
|
||||||
|
* explicitly, so it is possible for two pagers to end up using the same ID
|
||||||
|
* if both are set explicitly.
|
||||||
|
*
|
||||||
|
* @param $element
|
||||||
|
*/
|
||||||
|
public function element($element) {
|
||||||
|
$this->element = $element;
|
||||||
|
if ($element >= self::$maxElement) {
|
||||||
|
self::$maxElement = $element + 1;
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current page being requested for display within a pager.
|
||||||
|
*
|
||||||
|
* @param $element
|
||||||
|
* An optional integer to distinguish between multiple pagers on one page.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of the current requested page, within the pager represented by
|
||||||
|
* $element. This is determined from the URL query parameter $_GET['page'], or
|
||||||
|
* 0 by default. Note that this number may differ from the actual page being
|
||||||
|
* displayed. For example, if a search for "example text" brings up three
|
||||||
|
* pages of results, but a users visits search/node/example+text?page=10, this
|
||||||
|
* function will return 10, even though the default pager implementation
|
||||||
|
* adjusts for this and still displays the third page of search results at
|
||||||
|
* that URL.
|
||||||
|
*
|
||||||
|
* @see pager_default_initialize()
|
||||||
|
*/
|
||||||
|
function pager_find_page($element = 0) {
|
||||||
|
$page = isset($_GET['page']) ? $_GET['page'] : '';
|
||||||
|
$page_array = explode(',', $page);
|
||||||
|
if (!isset($page_array[$element])) {
|
||||||
|
$page_array[$element] = 0;
|
||||||
|
}
|
||||||
|
return (int) $page_array[$element];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a pager for theme('pager').
|
||||||
|
*
|
||||||
|
* This function sets up the necessary global variables so that future calls
|
||||||
|
* to theme('pager') will render a pager that correctly corresponds to the
|
||||||
|
* items being displayed.
|
||||||
|
*
|
||||||
|
* If the items being displayed result from a database query performed using
|
||||||
|
* Drupal's database API, and if you have control over the construction of the
|
||||||
|
* database query, you do not need to call this function directly; instead, you
|
||||||
|
* can simply extend the query object with the 'PagerDefault' extender before
|
||||||
|
* executing it. For example:
|
||||||
|
* @code
|
||||||
|
* $query = db_select('some_table')->extend('PagerDefault');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* However, if you are using a different method for generating the items to be
|
||||||
|
* paged through, then you should call this function in preparation.
|
||||||
|
*
|
||||||
|
* The following example shows how this function can be used in a page callback
|
||||||
|
* that invokes an external datastore with an SQL-like syntax:
|
||||||
|
* @code
|
||||||
|
* // First find the total number of items and initialize the pager.
|
||||||
|
* $where = "status = 1";
|
||||||
|
* $total = mymodule_select("SELECT COUNT(*) FROM data " . $where)->result();
|
||||||
|
* $num_per_page = variable_get('mymodule_num_per_page', 10);
|
||||||
|
* $page = pager_default_initialize($total, $num_per_page);
|
||||||
|
*
|
||||||
|
* // Next, retrieve and display the items for the current page.
|
||||||
|
* $offset = $num_per_page * $page;
|
||||||
|
* $result = mymodule_select("SELECT * FROM data " . $where . " LIMIT %d, %d", $offset, $num_per_page)->fetchAll();
|
||||||
|
* $output = theme('mymodule_results', array('result' => $result));
|
||||||
|
*
|
||||||
|
* // Finally, display the pager controls, and return.
|
||||||
|
* $output .= theme('pager');
|
||||||
|
* return $output;
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* A second example involves a page callback that invokes an external search
|
||||||
|
* service where the total number of matching results is provided as part of
|
||||||
|
* the returned set (so that we do not need a separate query in order to obtain
|
||||||
|
* this information). Here, we call pager_find_page() to calculate the desired
|
||||||
|
* offset before the search is invoked:
|
||||||
|
* @code
|
||||||
|
* // Perform the query, using the requested offset from pager_find_page().
|
||||||
|
* // This comes from a URL parameter, so here we are assuming that the URL
|
||||||
|
* // parameter corresponds to an actual page of results that will exist
|
||||||
|
* // within the set.
|
||||||
|
* $page = pager_find_page();
|
||||||
|
* $num_per_page = variable_get('mymodule_num_per_page', 10);
|
||||||
|
* $offset = $num_per_page * $page;
|
||||||
|
* $result = mymodule_remote_search($keywords, $offset, $num_per_page);
|
||||||
|
*
|
||||||
|
* // Now that we have the total number of results, initialize the pager.
|
||||||
|
* pager_default_initialize($result->total, $num_per_page);
|
||||||
|
*
|
||||||
|
* // Display the search results.
|
||||||
|
* $output = theme('search_results', array('results' => $result->data, 'type' => 'remote'));
|
||||||
|
*
|
||||||
|
* // Finally, display the pager controls, and return.
|
||||||
|
* $output .= theme('pager');
|
||||||
|
* return $output;
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @param $total
|
||||||
|
* The total number of items to be paged.
|
||||||
|
* @param $limit
|
||||||
|
* The number of items the calling code will display per page.
|
||||||
|
* @param $element
|
||||||
|
* An optional integer to distinguish between multiple pagers on one page.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of the current page, within the pager represented by $element.
|
||||||
|
* This is determined from the URL query parameter $_GET['page'], or 0 by
|
||||||
|
* default. However, if a page that does not correspond to the actual range
|
||||||
|
* of the result set was requested, this function will return the closest
|
||||||
|
* page actually within the result set.
|
||||||
|
*/
|
||||||
|
function pager_default_initialize($total, $limit, $element = 0) {
|
||||||
|
global $pager_page_array, $pager_total, $pager_total_items, $pager_limits;
|
||||||
|
|
||||||
|
$page = pager_find_page($element);
|
||||||
|
|
||||||
|
// We calculate the total of pages as ceil(items / limit).
|
||||||
|
$pager_total_items[$element] = $total;
|
||||||
|
$pager_total[$element] = ceil($pager_total_items[$element] / $limit);
|
||||||
|
$pager_page_array[$element] = max(0, min($page, ((int) $pager_total[$element]) - 1));
|
||||||
|
$pager_limits[$element] = $limit;
|
||||||
|
return $pager_page_array[$element];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a URL query parameter array for pager links.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A URL query parameter array that consists of all components of the current
|
||||||
|
* page request except for those pertaining to paging.
|
||||||
|
*/
|
||||||
|
function pager_get_query_parameters() {
|
||||||
|
$query = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($query)) {
|
||||||
|
$query = drupal_get_query_parameters($_GET, array('q', 'page'));
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for a query pager.
|
||||||
|
*
|
||||||
|
* Menu callbacks that display paged query results should call theme('pager') to
|
||||||
|
* retrieve a pager control so that users can view other results. Format a list
|
||||||
|
* of nearby pages with additional query results.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - tags: An array of labels for the controls in the pager.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager links.
|
||||||
|
* - quantity: The number of pages in the list.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager($variables) {
|
||||||
|
$tags = $variables['tags'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
$quantity = $variables['quantity'];
|
||||||
|
global $pager_page_array, $pager_total;
|
||||||
|
|
||||||
|
// Calculate various markers within this pager piece:
|
||||||
|
// Middle is used to "center" pages around the current page.
|
||||||
|
$pager_middle = ceil($quantity / 2);
|
||||||
|
// current is the page we are currently paged to
|
||||||
|
$pager_current = $pager_page_array[$element] + 1;
|
||||||
|
// first is the first page listed by this pager piece (re quantity)
|
||||||
|
$pager_first = $pager_current - $pager_middle + 1;
|
||||||
|
// last is the last page listed by this pager piece (re quantity)
|
||||||
|
$pager_last = $pager_current + $quantity - $pager_middle;
|
||||||
|
// max is the maximum page number
|
||||||
|
$pager_max = $pager_total[$element];
|
||||||
|
// End of marker calculations.
|
||||||
|
|
||||||
|
// Prepare for generation loop.
|
||||||
|
$i = $pager_first;
|
||||||
|
if ($pager_last > $pager_max) {
|
||||||
|
// Adjust "center" if at end of query.
|
||||||
|
$i = $i + ($pager_max - $pager_last);
|
||||||
|
$pager_last = $pager_max;
|
||||||
|
}
|
||||||
|
if ($i <= 0) {
|
||||||
|
// Adjust "center" if at start of query.
|
||||||
|
$pager_last = $pager_last + (1 - $i);
|
||||||
|
$i = 1;
|
||||||
|
}
|
||||||
|
// End of generation loop preparation.
|
||||||
|
|
||||||
|
$li_first = theme('pager_first', array('text' => (isset($tags[0]) ? $tags[0] : t('« first')), 'element' => $element, 'parameters' => $parameters));
|
||||||
|
$li_previous = theme('pager_previous', array('text' => (isset($tags[1]) ? $tags[1] : t('‹ previous')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
|
||||||
|
$li_next = theme('pager_next', array('text' => (isset($tags[3]) ? $tags[3] : t('next ›')), 'element' => $element, 'interval' => 1, 'parameters' => $parameters));
|
||||||
|
$li_last = theme('pager_last', array('text' => (isset($tags[4]) ? $tags[4] : t('last »')), 'element' => $element, 'parameters' => $parameters));
|
||||||
|
|
||||||
|
if ($pager_total[$element] > 1) {
|
||||||
|
if ($li_first) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-first'),
|
||||||
|
'data' => $li_first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($li_previous) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-previous'),
|
||||||
|
'data' => $li_previous,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When there is more than one page, create the pager list.
|
||||||
|
if ($i != $pager_max) {
|
||||||
|
if ($i > 1) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-ellipsis'),
|
||||||
|
'data' => '…',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Now generate the actual pager piece.
|
||||||
|
for (; $i <= $pager_last && $i <= $pager_max; $i++) {
|
||||||
|
if ($i < $pager_current) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-item'),
|
||||||
|
'data' => theme('pager_previous', array('text' => $i, 'element' => $element, 'interval' => ($pager_current - $i), 'parameters' => $parameters)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($i == $pager_current) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-current'),
|
||||||
|
'data' => $i,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($i > $pager_current) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-item'),
|
||||||
|
'data' => theme('pager_next', array('text' => $i, 'element' => $element, 'interval' => ($i - $pager_current), 'parameters' => $parameters)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($i < $pager_max) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-ellipsis'),
|
||||||
|
'data' => '…',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// End generation.
|
||||||
|
if ($li_next) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-next'),
|
||||||
|
'data' => $li_next,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if ($li_last) {
|
||||||
|
$items[] = array(
|
||||||
|
'class' => array('pager-last'),
|
||||||
|
'data' => $li_last,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '<h2 class="element-invisible">' . t('Pages') . '</h2>' . theme('item_list', array(
|
||||||
|
'items' => $items,
|
||||||
|
'attributes' => array('class' => array('pager')),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup pagerpieces Pager pieces
|
||||||
|
* @{
|
||||||
|
* Theme functions for customizing pager elements.
|
||||||
|
*
|
||||||
|
* Note that you should NOT modify this file to customize your pager.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the "first page" link in a query pager.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - text: The name (or image) of the link.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager links.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager_first($variables) {
|
||||||
|
$text = $variables['text'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
global $pager_page_array;
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
// If we are anywhere but the first page
|
||||||
|
if ($pager_page_array[$element] > 0) {
|
||||||
|
$output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array(0, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the "previous page" link in a query pager.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - text: The name (or image) of the link.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - interval: The number of pages to move backward when the link is clicked.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager links.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager_previous($variables) {
|
||||||
|
$text = $variables['text'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$interval = $variables['interval'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
global $pager_page_array;
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
// If we are anywhere but the first page
|
||||||
|
if ($pager_page_array[$element] > 0) {
|
||||||
|
$page_new = pager_load_array($pager_page_array[$element] - $interval, $element, $pager_page_array);
|
||||||
|
|
||||||
|
// If the previous page is the first page, mark the link as such.
|
||||||
|
if ($page_new[$element] == 0) {
|
||||||
|
$output = theme('pager_first', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
// The previous page is not the first page.
|
||||||
|
else {
|
||||||
|
$output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the "next page" link in a query pager.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - text: The name (or image) of the link.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - interval: The number of pages to move forward when the link is clicked.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager links.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager_next($variables) {
|
||||||
|
$text = $variables['text'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$interval = $variables['interval'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
global $pager_page_array, $pager_total;
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
// If we are anywhere but the last page
|
||||||
|
if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
|
||||||
|
$page_new = pager_load_array($pager_page_array[$element] + $interval, $element, $pager_page_array);
|
||||||
|
// If the next page is the last page, mark the link as such.
|
||||||
|
if ($page_new[$element] == ($pager_total[$element] - 1)) {
|
||||||
|
$output = theme('pager_last', array('text' => $text, 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
// The next page is not the last page.
|
||||||
|
else {
|
||||||
|
$output = theme('pager_link', array('text' => $text, 'page_new' => $page_new, 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the "last page" link in query pager.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - text: The name (or image) of the link.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager links.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager_last($variables) {
|
||||||
|
$text = $variables['text'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
global $pager_page_array, $pager_total;
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
// If we are anywhere but the last page
|
||||||
|
if ($pager_page_array[$element] < ($pager_total[$element] - 1)) {
|
||||||
|
$output = theme('pager_link', array('text' => $text, 'page_new' => pager_load_array($pager_total[$element] - 1, $element, $pager_page_array), 'element' => $element, 'parameters' => $parameters));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for a link to a specific query result page.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - text: The link text. Also used to figure out the title attribute of the
|
||||||
|
* link, if it is not provided in $variables['attributes']['title']; in
|
||||||
|
* this case, $variables['text'] must be one of the standard pager link
|
||||||
|
* text strings that would be generated by the pager theme functions, such
|
||||||
|
* as a number or t('« first').
|
||||||
|
* - page_new: The first result to display on the linked page.
|
||||||
|
* - element: An optional integer to distinguish between multiple pagers on
|
||||||
|
* one page.
|
||||||
|
* - parameters: An associative array of query string parameters to append to
|
||||||
|
* the pager link.
|
||||||
|
* - attributes: An associative array of HTML attributes to apply to the
|
||||||
|
* pager link.
|
||||||
|
*
|
||||||
|
* @see theme_pager()
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_pager_link($variables) {
|
||||||
|
$text = $variables['text'];
|
||||||
|
$page_new = $variables['page_new'];
|
||||||
|
$element = $variables['element'];
|
||||||
|
$parameters = $variables['parameters'];
|
||||||
|
$attributes = $variables['attributes'];
|
||||||
|
|
||||||
|
$page = isset($_GET['page']) ? $_GET['page'] : '';
|
||||||
|
if ($new_page = implode(',', pager_load_array($page_new[$element], $element, explode(',', $page)))) {
|
||||||
|
$parameters['page'] = $new_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query = array();
|
||||||
|
if (count($parameters)) {
|
||||||
|
$query = drupal_get_query_parameters($parameters, array());
|
||||||
|
}
|
||||||
|
if ($query_pager = pager_get_query_parameters()) {
|
||||||
|
$query = array_merge($query, $query_pager);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set each pager link title
|
||||||
|
if (!isset($attributes['title'])) {
|
||||||
|
static $titles = NULL;
|
||||||
|
if (!isset($titles)) {
|
||||||
|
$titles = array(
|
||||||
|
t('« first') => t('Go to first page'),
|
||||||
|
t('‹ previous') => t('Go to previous page'),
|
||||||
|
t('next ›') => t('Go to next page'),
|
||||||
|
t('last »') => t('Go to last page'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (isset($titles[$text])) {
|
||||||
|
$attributes['title'] = $titles[$text];
|
||||||
|
}
|
||||||
|
elseif (is_numeric($text)) {
|
||||||
|
$attributes['title'] = t('Go to page @number', array('@number' => $text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @todo l() cannot be used here, since it adds an 'active' class based on the
|
||||||
|
// path only (which is always the current path for pager links). Apparently,
|
||||||
|
// none of the pager links is active at any time - but it should still be
|
||||||
|
// possible to use l() here.
|
||||||
|
// @see http://drupal.org/node/1410574
|
||||||
|
$attributes['href'] = url($_GET['q'], array('query' => $query));
|
||||||
|
return '<a' . drupal_attributes($attributes) . '>' . check_plain($text) . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "Pager pieces".
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function
|
||||||
|
*
|
||||||
|
* Copies $old_array to $new_array and sets $new_array[$element] = $value
|
||||||
|
* Fills in $new_array[0 .. $element - 1] = 0
|
||||||
|
*/
|
||||||
|
function pager_load_array($value, $element, $old_array) {
|
||||||
|
$new_array = $old_array;
|
||||||
|
// Look for empty elements.
|
||||||
|
for ($i = 0; $i < $element; $i++) {
|
||||||
|
if (empty($new_array[$i])) {
|
||||||
|
// Load found empty element with 0.
|
||||||
|
$new_array[$i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update the changed element.
|
||||||
|
$new_array[$element] = (int) $value;
|
||||||
|
return $new_array;
|
||||||
|
}
|
291
includes/password.inc
Normal file
291
includes/password.inc
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Secure password hashing functions for user authentication.
|
||||||
|
*
|
||||||
|
* Based on the Portable PHP password hashing framework.
|
||||||
|
* @see http://www.openwall.com/phpass/
|
||||||
|
*
|
||||||
|
* An alternative or custom version of this password hashing API may be
|
||||||
|
* used by setting the variable password_inc to the name of the PHP file
|
||||||
|
* containing replacement user_hash_password(), user_check_password(), and
|
||||||
|
* user_needs_new_hash() functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The standard log2 number of iterations for password stretching. This should
|
||||||
|
* increase by 1 every Drupal version in order to counteract increases in the
|
||||||
|
* speed and power of computers available to crack the hashes.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_HASH_COUNT', 15);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The minimum allowed log2 number of iterations for password stretching.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_MIN_HASH_COUNT', 7);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The maximum allowed log2 number of iterations for password stretching.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_MAX_HASH_COUNT', 30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expected (and maximum) number of characters in a hashed password.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_HASH_LENGTH', 55);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a string for mapping an int to the corresponding base 64 character.
|
||||||
|
*/
|
||||||
|
function _password_itoa64() {
|
||||||
|
return './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes bytes into printable base 64 using the *nix standard from crypt().
|
||||||
|
*
|
||||||
|
* @param $input
|
||||||
|
* The string containing bytes to encode.
|
||||||
|
* @param $count
|
||||||
|
* The number of characters (bytes) to encode.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Encoded string
|
||||||
|
*/
|
||||||
|
function _password_base64_encode($input, $count) {
|
||||||
|
$output = '';
|
||||||
|
$i = 0;
|
||||||
|
$itoa64 = _password_itoa64();
|
||||||
|
do {
|
||||||
|
$value = ord($input[$i++]);
|
||||||
|
$output .= $itoa64[$value & 0x3f];
|
||||||
|
if ($i < $count) {
|
||||||
|
$value |= ord($input[$i]) << 8;
|
||||||
|
}
|
||||||
|
$output .= $itoa64[($value >> 6) & 0x3f];
|
||||||
|
if ($i++ >= $count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($i < $count) {
|
||||||
|
$value |= ord($input[$i]) << 16;
|
||||||
|
}
|
||||||
|
$output .= $itoa64[($value >> 12) & 0x3f];
|
||||||
|
if ($i++ >= $count) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$output .= $itoa64[($value >> 18) & 0x3f];
|
||||||
|
} while ($i < $count);
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a random base 64-encoded salt prefixed with settings for the hash.
|
||||||
|
*
|
||||||
|
* Proper use of salts may defeat a number of attacks, including:
|
||||||
|
* - The ability to try candidate passwords against multiple hashes at once.
|
||||||
|
* - The ability to use pre-hashed lists of candidate passwords.
|
||||||
|
* - The ability to determine whether two users have the same (or different)
|
||||||
|
* password without actually having to guess one of the passwords.
|
||||||
|
*
|
||||||
|
* @param $count_log2
|
||||||
|
* Integer that determines the number of iterations used in the hashing
|
||||||
|
* process. A larger value is more secure, but takes more time to complete.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A 12 character string containing the iteration count and a random salt.
|
||||||
|
*/
|
||||||
|
function _password_generate_salt($count_log2) {
|
||||||
|
$output = '$S$';
|
||||||
|
// Ensure that $count_log2 is within set bounds.
|
||||||
|
$count_log2 = _password_enforce_log2_boundaries($count_log2);
|
||||||
|
// We encode the final log2 iteration count in base 64.
|
||||||
|
$itoa64 = _password_itoa64();
|
||||||
|
$output .= $itoa64[$count_log2];
|
||||||
|
// 6 bytes is the standard salt for a portable phpass hash.
|
||||||
|
$output .= _password_base64_encode(drupal_random_bytes(6), 6);
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that $count_log2 is within set bounds.
|
||||||
|
*
|
||||||
|
* @param $count_log2
|
||||||
|
* Integer that determines the number of iterations used in the hashing
|
||||||
|
* process. A larger value is more secure, but takes more time to complete.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Integer within set bounds that is closest to $count_log2.
|
||||||
|
*/
|
||||||
|
function _password_enforce_log2_boundaries($count_log2) {
|
||||||
|
if ($count_log2 < DRUPAL_MIN_HASH_COUNT) {
|
||||||
|
return DRUPAL_MIN_HASH_COUNT;
|
||||||
|
}
|
||||||
|
elseif ($count_log2 > DRUPAL_MAX_HASH_COUNT) {
|
||||||
|
return DRUPAL_MAX_HASH_COUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $count_log2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password using a secure stretched hash.
|
||||||
|
*
|
||||||
|
* By using a salt and repeated hashing the password is "stretched". Its
|
||||||
|
* security is increased because it becomes much more computationally costly
|
||||||
|
* for an attacker to try to break the hash by brute-force computation of the
|
||||||
|
* hashes of a large number of plain-text words or strings to find a match.
|
||||||
|
*
|
||||||
|
* @param $algo
|
||||||
|
* The string name of a hashing algorithm usable by hash(), like 'sha256'.
|
||||||
|
* @param $password
|
||||||
|
* Plain-text password up to 512 bytes (128 to 512 UTF-8 characters) to hash.
|
||||||
|
* @param $setting
|
||||||
|
* An existing hash or the output of _password_generate_salt(). Must be
|
||||||
|
* at least 12 characters (the settings and salt).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A string containing the hashed password (and salt) or FALSE on failure.
|
||||||
|
* The return string will be truncated at DRUPAL_HASH_LENGTH characters max.
|
||||||
|
*/
|
||||||
|
function _password_crypt($algo, $password, $setting) {
|
||||||
|
// Prevent DoS attacks by refusing to hash large passwords.
|
||||||
|
if (strlen($password) > 512) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// The first 12 characters of an existing hash are its setting string.
|
||||||
|
$setting = substr($setting, 0, 12);
|
||||||
|
|
||||||
|
if ($setting[0] != '$' || $setting[2] != '$') {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$count_log2 = _password_get_count_log2($setting);
|
||||||
|
// Hashes may be imported from elsewhere, so we allow != DRUPAL_HASH_COUNT
|
||||||
|
if ($count_log2 < DRUPAL_MIN_HASH_COUNT || $count_log2 > DRUPAL_MAX_HASH_COUNT) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$salt = substr($setting, 4, 8);
|
||||||
|
// Hashes must have an 8 character salt.
|
||||||
|
if (strlen($salt) != 8) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the base 2 logarithm into an integer.
|
||||||
|
$count = 1 << $count_log2;
|
||||||
|
|
||||||
|
// We rely on the hash() function being available in PHP 5.2+.
|
||||||
|
$hash = hash($algo, $salt . $password, TRUE);
|
||||||
|
do {
|
||||||
|
$hash = hash($algo, $hash . $password, TRUE);
|
||||||
|
} while (--$count);
|
||||||
|
|
||||||
|
$len = strlen($hash);
|
||||||
|
$output = $setting . _password_base64_encode($hash, $len);
|
||||||
|
// _password_base64_encode() of a 16 byte MD5 will always be 22 characters.
|
||||||
|
// _password_base64_encode() of a 64 byte sha512 will always be 86 characters.
|
||||||
|
$expected = 12 + ceil((8 * $len) / 6);
|
||||||
|
return (strlen($output) == $expected) ? substr($output, 0, DRUPAL_HASH_LENGTH) : FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the log2 iteration count from a stored hash or setting string.
|
||||||
|
*/
|
||||||
|
function _password_get_count_log2($setting) {
|
||||||
|
$itoa64 = _password_itoa64();
|
||||||
|
return strpos($itoa64, $setting[3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash a password using a secure hash.
|
||||||
|
*
|
||||||
|
* @param $password
|
||||||
|
* A plain-text password.
|
||||||
|
* @param $count_log2
|
||||||
|
* Optional integer to specify the iteration count. Generally used only during
|
||||||
|
* mass operations where a value less than the default is needed for speed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A string containing the hashed password (and a salt), or FALSE on failure.
|
||||||
|
*/
|
||||||
|
function user_hash_password($password, $count_log2 = 0) {
|
||||||
|
if (empty($count_log2)) {
|
||||||
|
// Use the standard iteration count.
|
||||||
|
$count_log2 = variable_get('password_count_log2', DRUPAL_HASH_COUNT);
|
||||||
|
}
|
||||||
|
return _password_crypt('sha512', $password, _password_generate_salt($count_log2));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a plain text password matches a stored hashed password.
|
||||||
|
*
|
||||||
|
* Alternative implementations of this function may use other data in the
|
||||||
|
* $account object, for example the uid to look up the hash in a custom table
|
||||||
|
* or remote database.
|
||||||
|
*
|
||||||
|
* @param $password
|
||||||
|
* A plain-text password
|
||||||
|
* @param $account
|
||||||
|
* A user object with at least the fields from the {users} table.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE or FALSE.
|
||||||
|
*/
|
||||||
|
function user_check_password($password, $account) {
|
||||||
|
if (substr($account->pass, 0, 2) == 'U$') {
|
||||||
|
// This may be an updated password from user_update_7000(). Such hashes
|
||||||
|
// have 'U' added as the first character and need an extra md5().
|
||||||
|
$stored_hash = substr($account->pass, 1);
|
||||||
|
$password = md5($password);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$stored_hash = $account->pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = substr($stored_hash, 0, 3);
|
||||||
|
switch ($type) {
|
||||||
|
case '$S$':
|
||||||
|
// A normal Drupal 7 password using sha512.
|
||||||
|
$hash = _password_crypt('sha512', $password, $stored_hash);
|
||||||
|
break;
|
||||||
|
case '$H$':
|
||||||
|
// phpBB3 uses "$H$" for the same thing as "$P$".
|
||||||
|
case '$P$':
|
||||||
|
// A phpass password generated using md5. This is an
|
||||||
|
// imported password or from an earlier Drupal version.
|
||||||
|
$hash = _password_crypt('md5', $password, $stored_hash);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return ($hash && $stored_hash == $hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a user's hashed password needs to be replaced with a new hash.
|
||||||
|
*
|
||||||
|
* This is typically called during the login process when the plain text
|
||||||
|
* password is available. A new hash is needed when the desired iteration count
|
||||||
|
* has changed through a change in the variable password_count_log2 or
|
||||||
|
* DRUPAL_HASH_COUNT or if the user's password hash was generated in an update
|
||||||
|
* like user_update_7000().
|
||||||
|
*
|
||||||
|
* Alternative implementations of this function might use other criteria based
|
||||||
|
* on the fields in $account.
|
||||||
|
*
|
||||||
|
* @param $account
|
||||||
|
* A user object with at least the fields from the {users} table.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE or FALSE.
|
||||||
|
*/
|
||||||
|
function user_needs_new_hash($account) {
|
||||||
|
// Check whether this was an updated password.
|
||||||
|
if ((substr($account->pass, 0, 3) != '$S$') || (strlen($account->pass) != DRUPAL_HASH_LENGTH)) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
// Ensure that $count_log2 is within set bounds.
|
||||||
|
$count_log2 = _password_enforce_log2_boundaries(variable_get('password_count_log2', DRUPAL_HASH_COUNT));
|
||||||
|
// Check whether the iteration count used differs from the standard number.
|
||||||
|
return (_password_get_count_log2($account->pass) !== $count_log2);
|
||||||
|
}
|
588
includes/path.inc
Normal file
588
includes/path.inc
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Functions to handle paths in Drupal, including path aliasing.
|
||||||
|
*
|
||||||
|
* These functions are not loaded for cached pages, but modules that need
|
||||||
|
* to use them in hook_boot() or hook exit() can make them available, by
|
||||||
|
* executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the $_GET['q'] variable to the proper normal path.
|
||||||
|
*/
|
||||||
|
function drupal_path_initialize() {
|
||||||
|
// Ensure $_GET['q'] is set before calling drupal_normal_path(), to support
|
||||||
|
// path caching with hook_url_inbound_alter().
|
||||||
|
if (empty($_GET['q'])) {
|
||||||
|
$_GET['q'] = variable_get('site_frontpage', 'node');
|
||||||
|
}
|
||||||
|
$_GET['q'] = drupal_get_normal_path($_GET['q']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an alias, return its Drupal system URL if one exists. Given a Drupal
|
||||||
|
* system URL return one of its aliases if such a one exists. Otherwise,
|
||||||
|
* return FALSE.
|
||||||
|
*
|
||||||
|
* @param $action
|
||||||
|
* One of the following values:
|
||||||
|
* - wipe: delete the alias cache.
|
||||||
|
* - alias: return an alias for a given Drupal system path (if one exists).
|
||||||
|
* - source: return the Drupal system URL for a path alias (if one exists).
|
||||||
|
* @param $path
|
||||||
|
* The path to investigate for corresponding aliases or system URLs.
|
||||||
|
* @param $path_language
|
||||||
|
* Optional language code to search the path with. Defaults to the page language.
|
||||||
|
* If there's no path defined for that language it will search paths without
|
||||||
|
* language.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Either a Drupal system path, an aliased path, or FALSE if no path was
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
function drupal_lookup_path($action, $path = '', $path_language = NULL) {
|
||||||
|
global $language_url;
|
||||||
|
// Use the advanced drupal_static() pattern, since this is called very often.
|
||||||
|
static $drupal_static_fast;
|
||||||
|
if (!isset($drupal_static_fast)) {
|
||||||
|
$drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
|
||||||
|
}
|
||||||
|
$cache = &$drupal_static_fast['cache'];
|
||||||
|
|
||||||
|
if (!isset($cache)) {
|
||||||
|
$cache = array(
|
||||||
|
'map' => array(),
|
||||||
|
'no_source' => array(),
|
||||||
|
'whitelist' => NULL,
|
||||||
|
'system_paths' => array(),
|
||||||
|
'no_aliases' => array(),
|
||||||
|
'first_call' => TRUE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the path alias whitelist.
|
||||||
|
if (!isset($cache['whitelist'])) {
|
||||||
|
$cache['whitelist'] = variable_get('path_alias_whitelist', NULL);
|
||||||
|
if (!isset($cache['whitelist'])) {
|
||||||
|
$cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no language is explicitly specified we default to the current URL
|
||||||
|
// language. If we used a language different from the one conveyed by the
|
||||||
|
// requested URL, we might end up being unable to check if there is a path
|
||||||
|
// alias matching the URL path.
|
||||||
|
$path_language = $path_language ? $path_language : $language_url->language;
|
||||||
|
|
||||||
|
if ($action == 'wipe') {
|
||||||
|
$cache = array();
|
||||||
|
$cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
|
||||||
|
}
|
||||||
|
elseif ($cache['whitelist'] && $path != '') {
|
||||||
|
if ($action == 'alias') {
|
||||||
|
// During the first call to drupal_lookup_path() per language, load the
|
||||||
|
// expected system paths for the page from cache.
|
||||||
|
if (!empty($cache['first_call'])) {
|
||||||
|
$cache['first_call'] = FALSE;
|
||||||
|
|
||||||
|
$cache['map'][$path_language] = array();
|
||||||
|
// Load system paths from cache.
|
||||||
|
$cid = current_path();
|
||||||
|
if ($cached = cache_get($cid, 'cache_path')) {
|
||||||
|
$cache['system_paths'] = $cached->data;
|
||||||
|
// Now fetch the aliases corresponding to these system paths.
|
||||||
|
$args = array(
|
||||||
|
':system' => $cache['system_paths'],
|
||||||
|
':language' => $path_language,
|
||||||
|
':language_none' => LANGUAGE_NONE,
|
||||||
|
);
|
||||||
|
// Always get the language-specific alias before the language-neutral
|
||||||
|
// one. For example 'de' is less than 'und' so the order needs to be
|
||||||
|
// ASC, while 'xx-lolspeak' is more than 'und' so the order needs to
|
||||||
|
// be DESC. We also order by pid ASC so that fetchAllKeyed() returns
|
||||||
|
// the most recently created alias for each source. Subsequent queries
|
||||||
|
// using fetchField() must use pid DESC to have the same effect.
|
||||||
|
// For performance reasons, the query builder is not used here.
|
||||||
|
if ($path_language == LANGUAGE_NONE) {
|
||||||
|
// Prevent PDO from complaining about a token the query doesn't use.
|
||||||
|
unset($args[':language']);
|
||||||
|
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language = :language_none ORDER BY pid ASC', $args);
|
||||||
|
}
|
||||||
|
elseif ($path_language < LANGUAGE_NONE) {
|
||||||
|
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language ASC, pid ASC', $args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND language IN (:language, :language_none) ORDER BY language DESC, pid ASC', $args);
|
||||||
|
}
|
||||||
|
$cache['map'][$path_language] = $result->fetchAllKeyed();
|
||||||
|
// Keep a record of paths with no alias to avoid querying twice.
|
||||||
|
$cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language])));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If the alias has already been loaded, return it.
|
||||||
|
if (isset($cache['map'][$path_language][$path])) {
|
||||||
|
return $cache['map'][$path_language][$path];
|
||||||
|
}
|
||||||
|
// Check the path whitelist, if the top_level part before the first /
|
||||||
|
// is not in the list, then there is no need to do anything further,
|
||||||
|
// it is not in the database.
|
||||||
|
elseif (!isset($cache['whitelist'][strtok($path, '/')])) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// For system paths which were not cached, query aliases individually.
|
||||||
|
elseif (!isset($cache['no_aliases'][$path_language][$path])) {
|
||||||
|
$args = array(
|
||||||
|
':source' => $path,
|
||||||
|
':language' => $path_language,
|
||||||
|
':language_none' => LANGUAGE_NONE,
|
||||||
|
);
|
||||||
|
// See the queries above.
|
||||||
|
if ($path_language == LANGUAGE_NONE) {
|
||||||
|
unset($args[':language']);
|
||||||
|
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language = :language_none ORDER BY pid DESC", $args)->fetchField();
|
||||||
|
}
|
||||||
|
elseif ($path_language > LANGUAGE_NONE) {
|
||||||
|
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args)->fetchField();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args)->fetchField();
|
||||||
|
}
|
||||||
|
$cache['map'][$path_language][$path] = $alias;
|
||||||
|
return $alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check $no_source for this $path in case we've already determined that there
|
||||||
|
// isn't a path that has this alias
|
||||||
|
elseif ($action == 'source' && !isset($cache['no_source'][$path_language][$path])) {
|
||||||
|
// Look for the value $path within the cached $map
|
||||||
|
$source = FALSE;
|
||||||
|
if (!isset($cache['map'][$path_language]) || !($source = array_search($path, $cache['map'][$path_language]))) {
|
||||||
|
$args = array(
|
||||||
|
':alias' => $path,
|
||||||
|
':language' => $path_language,
|
||||||
|
':language_none' => LANGUAGE_NONE,
|
||||||
|
);
|
||||||
|
// See the queries above.
|
||||||
|
if ($path_language == LANGUAGE_NONE) {
|
||||||
|
unset($args[':language']);
|
||||||
|
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language = :language_none ORDER BY pid DESC", $args);
|
||||||
|
}
|
||||||
|
elseif ($path_language > LANGUAGE_NONE) {
|
||||||
|
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language DESC, pid DESC", $args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND language IN (:language, :language_none) ORDER BY language ASC, pid DESC", $args);
|
||||||
|
}
|
||||||
|
if ($source = $result->fetchField()) {
|
||||||
|
$cache['map'][$path_language][$source] = $path;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// We can't record anything into $map because we do not have a valid
|
||||||
|
// index and there is no need because we have not learned anything
|
||||||
|
// about any Drupal path. Thus cache to $no_source.
|
||||||
|
$cache['no_source'][$path_language][$path] = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache system paths for a page.
|
||||||
|
*
|
||||||
|
* Cache an array of the system paths available on each page. We assume
|
||||||
|
* that aliases will be needed for the majority of these paths during
|
||||||
|
* subsequent requests, and load them in a single query during
|
||||||
|
* drupal_lookup_path().
|
||||||
|
*/
|
||||||
|
function drupal_cache_system_paths() {
|
||||||
|
// Check if the system paths for this page were loaded from cache in this
|
||||||
|
// request to avoid writing to cache on every request.
|
||||||
|
$cache = &drupal_static('drupal_lookup_path', array());
|
||||||
|
if (empty($cache['system_paths']) && !empty($cache['map'])) {
|
||||||
|
// Generate a cache ID (cid) specifically for this page.
|
||||||
|
$cid = current_path();
|
||||||
|
// The static $map array used by drupal_lookup_path() includes all
|
||||||
|
// system paths for the page request.
|
||||||
|
if ($paths = current($cache['map'])) {
|
||||||
|
$data = array_keys($paths);
|
||||||
|
$expire = REQUEST_TIME + (60 * 60 * 24);
|
||||||
|
cache_set($cid, $data, 'cache_path', $expire);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an internal Drupal path, return the alias set by the administrator.
|
||||||
|
*
|
||||||
|
* If no path is provided, the function will return the alias of the current
|
||||||
|
* page.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* An internal Drupal path.
|
||||||
|
* @param $path_language
|
||||||
|
* An optional language code to look up the path in.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An aliased path if one was found, or the original path if no alias was
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
function drupal_get_path_alias($path = NULL, $path_language = NULL) {
|
||||||
|
// If no path is specified, use the current page's path.
|
||||||
|
if ($path == NULL) {
|
||||||
|
$path = $_GET['q'];
|
||||||
|
}
|
||||||
|
$result = $path;
|
||||||
|
if ($alias = drupal_lookup_path('alias', $path, $path_language)) {
|
||||||
|
$result = $alias;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a path alias, return the internal path it represents.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* A Drupal path alias.
|
||||||
|
* @param $path_language
|
||||||
|
* An optional language code to look up the path in.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The internal path represented by the alias, or the original alias if no
|
||||||
|
* internal path was found.
|
||||||
|
*/
|
||||||
|
function drupal_get_normal_path($path, $path_language = NULL) {
|
||||||
|
$original_path = $path;
|
||||||
|
|
||||||
|
// Lookup the path alias first.
|
||||||
|
if ($source = drupal_lookup_path('source', $path, $path_language)) {
|
||||||
|
$path = $source;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow other modules to alter the inbound URL. We cannot use drupal_alter()
|
||||||
|
// here because we need to run hook_url_inbound_alter() in the reverse order
|
||||||
|
// of hook_url_outbound_alter().
|
||||||
|
foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
|
||||||
|
$function = $module . '_url_inbound_alter';
|
||||||
|
$function($path, $original_path, $path_language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current page is the front page.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Boolean value: TRUE if the current page is the front page; FALSE if otherwise.
|
||||||
|
*/
|
||||||
|
function drupal_is_front_page() {
|
||||||
|
// Use the advanced drupal_static() pattern, since this is called very often.
|
||||||
|
static $drupal_static_fast;
|
||||||
|
if (!isset($drupal_static_fast)) {
|
||||||
|
$drupal_static_fast['is_front_page'] = &drupal_static(__FUNCTION__);
|
||||||
|
}
|
||||||
|
$is_front_page = &$drupal_static_fast['is_front_page'];
|
||||||
|
|
||||||
|
if (!isset($is_front_page)) {
|
||||||
|
// As drupal_path_initialize updates $_GET['q'] with the 'site_frontpage' path,
|
||||||
|
// we can check it against the 'site_frontpage' variable.
|
||||||
|
$is_front_page = ($_GET['q'] == variable_get('site_frontpage', 'node'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $is_front_page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a path matches any pattern in a set of patterns.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path to match.
|
||||||
|
* @param $patterns
|
||||||
|
* String containing a set of patterns separated by \n, \r or \r\n.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Boolean value: TRUE if the path matches a pattern, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function drupal_match_path($path, $patterns) {
|
||||||
|
$regexps = &drupal_static(__FUNCTION__);
|
||||||
|
|
||||||
|
if (!isset($regexps[$patterns])) {
|
||||||
|
// Convert path settings to a regular expression.
|
||||||
|
// Therefore replace newlines with a logical or, /* with asterisks and the <front> with the frontpage.
|
||||||
|
$to_replace = array(
|
||||||
|
'/(\r\n?|\n)/', // newlines
|
||||||
|
'/\\\\\*/', // asterisks
|
||||||
|
'/(^|\|)\\\\<front\\\\>($|\|)/' // <front>
|
||||||
|
);
|
||||||
|
$replacements = array(
|
||||||
|
'|',
|
||||||
|
'.*',
|
||||||
|
'\1' . preg_quote(variable_get('site_frontpage', 'node'), '/') . '\2'
|
||||||
|
);
|
||||||
|
$patterns_quoted = preg_quote($patterns, '/');
|
||||||
|
$regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
|
||||||
|
}
|
||||||
|
return (bool)preg_match($regexps[$patterns], $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current URL path of the page being viewed.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - http://example.com/node/306 returns "node/306".
|
||||||
|
* - http://example.com/drupalfolder/node/306 returns "node/306" while
|
||||||
|
* base_path() returns "/drupalfolder/".
|
||||||
|
* - http://example.com/path/alias (which is a path alias for node/306) returns
|
||||||
|
* "node/306" as opposed to the path alias.
|
||||||
|
*
|
||||||
|
* This function is not available in hook_boot() so use $_GET['q'] instead.
|
||||||
|
* However, be careful when doing that because in the case of Example #3
|
||||||
|
* $_GET['q'] will contain "path/alias". If "node/306" is needed, calling
|
||||||
|
* drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL) makes this function available.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The current Drupal URL path. The path is untrusted user input and must be
|
||||||
|
* treated as such.
|
||||||
|
*
|
||||||
|
* @see request_path()
|
||||||
|
*/
|
||||||
|
function current_path() {
|
||||||
|
return $_GET['q'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild the path alias white list.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* An optional system path for which an alias is being inserted.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array containing a white list of path aliases.
|
||||||
|
*/
|
||||||
|
function drupal_path_alias_whitelist_rebuild($source = NULL) {
|
||||||
|
// When paths are inserted, only rebuild the whitelist if the system path
|
||||||
|
// has a top level component which is not already in the whitelist.
|
||||||
|
if (!empty($source)) {
|
||||||
|
$whitelist = variable_get('path_alias_whitelist', NULL);
|
||||||
|
if (isset($whitelist[strtok($source, '/')])) {
|
||||||
|
return $whitelist;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For each alias in the database, get the top level component of the system
|
||||||
|
// path it corresponds to. This is the portion of the path before the first
|
||||||
|
// '/', if present, otherwise the whole path itself.
|
||||||
|
$whitelist = array();
|
||||||
|
$result = db_query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
|
||||||
|
foreach ($result as $row) {
|
||||||
|
$whitelist[$row->path] = TRUE;
|
||||||
|
}
|
||||||
|
variable_set('path_alias_whitelist', $whitelist);
|
||||||
|
return $whitelist;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a specific URL alias from the database.
|
||||||
|
*
|
||||||
|
* @param $conditions
|
||||||
|
* A string representing the source, a number representing the pid, or an
|
||||||
|
* array of query conditions.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* FALSE if no alias was found or an associative array containing the
|
||||||
|
* following keys:
|
||||||
|
* - source: The internal system path.
|
||||||
|
* - alias: The URL alias.
|
||||||
|
* - pid: Unique path alias identifier.
|
||||||
|
* - language: The language of the alias.
|
||||||
|
*/
|
||||||
|
function path_load($conditions) {
|
||||||
|
if (is_numeric($conditions)) {
|
||||||
|
$conditions = array('pid' => $conditions);
|
||||||
|
}
|
||||||
|
elseif (is_string($conditions)) {
|
||||||
|
$conditions = array('source' => $conditions);
|
||||||
|
}
|
||||||
|
elseif (!is_array($conditions)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$select = db_select('url_alias');
|
||||||
|
foreach ($conditions as $field => $value) {
|
||||||
|
$select->condition($field, $value);
|
||||||
|
}
|
||||||
|
return $select
|
||||||
|
->fields('url_alias')
|
||||||
|
->execute()
|
||||||
|
->fetchAssoc();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a path alias to the database.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* An associative array containing the following keys:
|
||||||
|
* - source: The internal system path.
|
||||||
|
* - alias: The URL alias.
|
||||||
|
* - pid: (optional) Unique path alias identifier.
|
||||||
|
* - language: (optional) The language of the alias.
|
||||||
|
*/
|
||||||
|
function path_save(&$path) {
|
||||||
|
$path += array('language' => LANGUAGE_NONE);
|
||||||
|
|
||||||
|
// Load the stored alias, if any.
|
||||||
|
if (!empty($path['pid']) && !isset($path['original'])) {
|
||||||
|
$path['original'] = path_load($path['pid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($path['pid'])) {
|
||||||
|
drupal_write_record('url_alias', $path);
|
||||||
|
module_invoke_all('path_insert', $path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
drupal_write_record('url_alias', $path, array('pid'));
|
||||||
|
module_invoke_all('path_update', $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear internal properties.
|
||||||
|
unset($path['original']);
|
||||||
|
|
||||||
|
// Clear the static alias cache.
|
||||||
|
drupal_clear_path_cache($path['source']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a URL alias.
|
||||||
|
*
|
||||||
|
* @param $criteria
|
||||||
|
* A number representing the pid or an array of criteria.
|
||||||
|
*/
|
||||||
|
function path_delete($criteria) {
|
||||||
|
if (!is_array($criteria)) {
|
||||||
|
$criteria = array('pid' => $criteria);
|
||||||
|
}
|
||||||
|
$path = path_load($criteria);
|
||||||
|
$query = db_delete('url_alias');
|
||||||
|
foreach ($criteria as $field => $value) {
|
||||||
|
$query->condition($field, $value);
|
||||||
|
}
|
||||||
|
$query->execute();
|
||||||
|
module_invoke_all('path_delete', $path);
|
||||||
|
drupal_clear_path_cache($path['source']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a path is in the administrative section of the site.
|
||||||
|
*
|
||||||
|
* By default, paths are considered to be non-administrative. If a path does
|
||||||
|
* not match any of the patterns in path_get_admin_paths(), or if it matches
|
||||||
|
* both administrative and non-administrative patterns, it is considered
|
||||||
|
* non-administrative.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* A Drupal path.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if the path is administrative, FALSE otherwise.
|
||||||
|
*
|
||||||
|
* @see path_get_admin_paths()
|
||||||
|
* @see hook_admin_paths()
|
||||||
|
* @see hook_admin_paths_alter()
|
||||||
|
*/
|
||||||
|
function path_is_admin($path) {
|
||||||
|
$path_map = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($path_map['admin'][$path])) {
|
||||||
|
$patterns = path_get_admin_paths();
|
||||||
|
$path_map['admin'][$path] = drupal_match_path($path, $patterns['admin']);
|
||||||
|
$path_map['non_admin'][$path] = drupal_match_path($path, $patterns['non_admin']);
|
||||||
|
}
|
||||||
|
return $path_map['admin'][$path] && !$path_map['non_admin'][$path];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a list of administrative and non-administrative paths.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An associative array containing the following keys:
|
||||||
|
* 'admin': An array of administrative paths and regular expressions
|
||||||
|
* in a format suitable for drupal_match_path().
|
||||||
|
* 'non_admin': An array of non-administrative paths and regular expressions.
|
||||||
|
*
|
||||||
|
* @see hook_admin_paths()
|
||||||
|
* @see hook_admin_paths_alter()
|
||||||
|
*/
|
||||||
|
function path_get_admin_paths() {
|
||||||
|
$patterns = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($patterns)) {
|
||||||
|
$paths = module_invoke_all('admin_paths');
|
||||||
|
drupal_alter('admin_paths', $paths);
|
||||||
|
// Combine all admin paths into one array, and likewise for non-admin paths,
|
||||||
|
// for easier handling.
|
||||||
|
$patterns = array();
|
||||||
|
$patterns['admin'] = array();
|
||||||
|
$patterns['non_admin'] = array();
|
||||||
|
foreach ($paths as $path => $enabled) {
|
||||||
|
if ($enabled) {
|
||||||
|
$patterns['admin'][] = $path;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$patterns['non_admin'][] = $path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$patterns['admin'] = implode("\n", $patterns['admin']);
|
||||||
|
$patterns['non_admin'] = implode("\n", $patterns['non_admin']);
|
||||||
|
}
|
||||||
|
return $patterns;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks a path exists and the current user has access to it.
|
||||||
|
*
|
||||||
|
* @param $path
|
||||||
|
* The path to check.
|
||||||
|
* @param $dynamic_allowed
|
||||||
|
* Whether paths with menu wildcards (like user/%) should be allowed.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if it is a valid path AND the current user has access permission,
|
||||||
|
* FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function drupal_valid_path($path, $dynamic_allowed = FALSE) {
|
||||||
|
global $menu_admin;
|
||||||
|
// We indicate that a menu administrator is running the menu access check.
|
||||||
|
$menu_admin = TRUE;
|
||||||
|
if ($path == '<front>' || url_is_external($path)) {
|
||||||
|
$item = array('access' => TRUE);
|
||||||
|
}
|
||||||
|
elseif ($dynamic_allowed && preg_match('/\/\%/', $path)) {
|
||||||
|
// Path is dynamic (ie 'user/%'), so check directly against menu_router table.
|
||||||
|
if ($item = db_query("SELECT * FROM {menu_router} where path = :path", array(':path' => $path))->fetchAssoc()) {
|
||||||
|
$item['link_path'] = $item['path'];
|
||||||
|
$item['link_title'] = $item['title'];
|
||||||
|
$item['external'] = FALSE;
|
||||||
|
$item['options'] = '';
|
||||||
|
_menu_link_translate($item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$item = menu_get_item($path);
|
||||||
|
}
|
||||||
|
$menu_admin = FALSE;
|
||||||
|
return $item && $item['access'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the path cache.
|
||||||
|
*
|
||||||
|
* @param $source
|
||||||
|
* An optional system path for which an alias is being changed.
|
||||||
|
*/
|
||||||
|
function drupal_clear_path_cache($source = NULL) {
|
||||||
|
// Clear the drupal_lookup_path() static cache.
|
||||||
|
drupal_static_reset('drupal_lookup_path');
|
||||||
|
drupal_path_alias_whitelist_rebuild($source);
|
||||||
|
}
|
192
includes/registry.inc
Normal file
192
includes/registry.inc
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* This file contains the code registry parser engine.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @defgroup registry Code registry
|
||||||
|
* @{
|
||||||
|
* The code registry engine.
|
||||||
|
*
|
||||||
|
* Drupal maintains an internal registry of all interfaces or classes in the
|
||||||
|
* system, allowing it to lazy-load code files as needed (reducing the amount
|
||||||
|
* of code that must be parsed on each request).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the work for registry_update().
|
||||||
|
*/
|
||||||
|
function _registry_update() {
|
||||||
|
|
||||||
|
// The registry serves as a central autoloader for all classes, including
|
||||||
|
// the database query builders. However, the registry rebuild process
|
||||||
|
// requires write ability to the database, which means having access to the
|
||||||
|
// query builders that require the registry in order to be loaded. That
|
||||||
|
// causes a fatal race condition. Therefore we manually include the
|
||||||
|
// appropriate query builders for the currently active database before the
|
||||||
|
// registry rebuild process runs.
|
||||||
|
$connection_info = Database::getConnectionInfo();
|
||||||
|
$driver = $connection_info['default']['driver'];
|
||||||
|
require_once DRUPAL_ROOT . '/includes/database/query.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/database/select.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/database/' . $driver . '/query.inc';
|
||||||
|
|
||||||
|
// Get current list of modules and their files.
|
||||||
|
$modules = db_query("SELECT * FROM {system} WHERE type = 'module'")->fetchAll();
|
||||||
|
// Get the list of files we are going to parse.
|
||||||
|
$files = array();
|
||||||
|
foreach ($modules as &$module) {
|
||||||
|
$module->info = unserialize($module->info);
|
||||||
|
$dir = dirname($module->filename);
|
||||||
|
|
||||||
|
// Store the module directory for use in hook_registry_files_alter().
|
||||||
|
$module->dir = $dir;
|
||||||
|
|
||||||
|
if ($module->status) {
|
||||||
|
// Add files for enabled modules to the registry.
|
||||||
|
foreach ($module->info['files'] as $file) {
|
||||||
|
$files["$dir/$file"] = array('module' => $module->name, 'weight' => $module->weight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (file_scan_directory('includes', '/\.inc$/') as $filename => $file) {
|
||||||
|
$files["$filename"] = array('module' => '', 'weight' => 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$transaction = db_transaction();
|
||||||
|
try {
|
||||||
|
// Allow modules to manually modify the list of files before the registry
|
||||||
|
// parses them. The $modules array provides the .info file information, which
|
||||||
|
// includes the list of files registered to each module. Any files in the
|
||||||
|
// list can then be added to the list of files that the registry will parse,
|
||||||
|
// or modify attributes of a file.
|
||||||
|
drupal_alter('registry_files', $files, $modules);
|
||||||
|
foreach (registry_get_parsed_files() as $filename => $file) {
|
||||||
|
// Add the hash for those files we have already parsed.
|
||||||
|
if (isset($files[$filename])) {
|
||||||
|
$files[$filename]['hash'] = $file['hash'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Flush the registry of resources in files that are no longer on disc
|
||||||
|
// or are in files that no installed modules require to be parsed.
|
||||||
|
db_delete('registry')
|
||||||
|
->condition('filename', $filename)
|
||||||
|
->execute();
|
||||||
|
db_delete('registry_file')
|
||||||
|
->condition('filename', $filename)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$parsed_files = _registry_parse_files($files);
|
||||||
|
|
||||||
|
$unchanged_resources = array();
|
||||||
|
$lookup_cache = array();
|
||||||
|
if ($cache = cache_get('lookup_cache', 'cache_bootstrap')) {
|
||||||
|
$lookup_cache = $cache->data;
|
||||||
|
}
|
||||||
|
foreach ($lookup_cache as $key => $file) {
|
||||||
|
// If the file for this cached resource is carried over unchanged from
|
||||||
|
// the last registry build, then we can safely re-cache it.
|
||||||
|
if ($file && in_array($file, array_keys($files)) && !in_array($file, $parsed_files)) {
|
||||||
|
$unchanged_resources[$key] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module_implements('', FALSE, TRUE);
|
||||||
|
_registry_check_code(REGISTRY_RESET_LOOKUP_CACHE);
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
$transaction->rollback();
|
||||||
|
watchdog_exception('registry', $e);
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have some unchanged resources, warm up the cache - no need to pay
|
||||||
|
// for looking them up again.
|
||||||
|
if (count($unchanged_resources) > 0) {
|
||||||
|
cache_set('lookup_cache', $unchanged_resources, 'cache_bootstrap');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the list of files in registry_file
|
||||||
|
*/
|
||||||
|
function registry_get_parsed_files() {
|
||||||
|
$files = array();
|
||||||
|
// We want the result as a keyed array.
|
||||||
|
$files = db_query("SELECT * FROM {registry_file}")->fetchAllAssoc('filename', PDO::FETCH_ASSOC);
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse all changed files and save their interface and class listings.
|
||||||
|
*
|
||||||
|
* Parse all files that have changed since the registry was last built, and save
|
||||||
|
* their interface and class listings.
|
||||||
|
*
|
||||||
|
* @param $files
|
||||||
|
* The list of files to check and parse.
|
||||||
|
*/
|
||||||
|
function _registry_parse_files($files) {
|
||||||
|
$parsed_files = array();
|
||||||
|
foreach ($files as $filename => $file) {
|
||||||
|
if (file_exists($filename)) {
|
||||||
|
$hash = hash_file('sha256', $filename);
|
||||||
|
if (empty($file['hash']) || $file['hash'] != $hash) {
|
||||||
|
$file['hash'] = $hash;
|
||||||
|
$parsed_files[$filename] = $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($parsed_files as $filename => $file) {
|
||||||
|
_registry_parse_file($filename, file_get_contents($filename), $file['module'], $file['weight']);
|
||||||
|
db_merge('registry_file')
|
||||||
|
->key(array('filename' => $filename))
|
||||||
|
->fields(array(
|
||||||
|
'hash' => $file['hash'],
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
return array_keys($parsed_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a file and save its interface and class listings.
|
||||||
|
*
|
||||||
|
* @param $filename
|
||||||
|
* Name of the file we are going to parse.
|
||||||
|
* @param $contents
|
||||||
|
* Contents of the file we are going to parse as a string.
|
||||||
|
* @param $module
|
||||||
|
* (optional) Name of the module this file belongs to.
|
||||||
|
* @param $weight
|
||||||
|
* (optional) Weight of the module.
|
||||||
|
*/
|
||||||
|
function _registry_parse_file($filename, $contents, $module = '', $weight = 0) {
|
||||||
|
if (preg_match_all('/^\s*(?:abstract|final)?\s*(class|interface|trait)\s+([a-zA-Z0-9_]+)/m', $contents, $matches)) {
|
||||||
|
foreach ($matches[2] as $key => $name) {
|
||||||
|
db_merge('registry')
|
||||||
|
->key(array(
|
||||||
|
'name' => $name,
|
||||||
|
'type' => $matches[1][$key],
|
||||||
|
))
|
||||||
|
->fields(array(
|
||||||
|
'filename' => $filename,
|
||||||
|
'module' => $module,
|
||||||
|
'weight' => $weight,
|
||||||
|
))
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
// Delete any resources for this file where the name is not in the list
|
||||||
|
// we just merged in.
|
||||||
|
db_delete('registry')
|
||||||
|
->condition('filename', $filename)
|
||||||
|
->condition('name', $matches[2], 'NOT IN')
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @} End of "defgroup registry".
|
||||||
|
*/
|
535
includes/session.inc
Normal file
535
includes/session.inc
Normal file
|
@ -0,0 +1,535 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* User session handling functions.
|
||||||
|
*
|
||||||
|
* The user-level session storage handlers:
|
||||||
|
* - _drupal_session_open()
|
||||||
|
* - _drupal_session_close()
|
||||||
|
* - _drupal_session_read()
|
||||||
|
* - _drupal_session_write()
|
||||||
|
* - _drupal_session_destroy()
|
||||||
|
* - _drupal_session_garbage_collection()
|
||||||
|
* are assigned by session_set_save_handler() in bootstrap.inc and are called
|
||||||
|
* automatically by PHP. These functions should not be called directly. Session
|
||||||
|
* data should instead be accessed via the $_SESSION superglobal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session handler assigned by session_set_save_handler().
|
||||||
|
*
|
||||||
|
* This function is used to handle any initialization, such as file paths or
|
||||||
|
* database connections, that is needed before accessing session data. Drupal
|
||||||
|
* does not need to initialize anything in this function.
|
||||||
|
*
|
||||||
|
* This function should not be called directly.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* This function will always return TRUE.
|
||||||
|
*/
|
||||||
|
function _drupal_session_open() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session handler assigned by session_set_save_handler().
|
||||||
|
*
|
||||||
|
* This function is used to close the current session. Because Drupal stores
|
||||||
|
* session data in the database immediately on write, this function does
|
||||||
|
* not need to do anything.
|
||||||
|
*
|
||||||
|
* This function should not be called directly.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* This function will always return TRUE.
|
||||||
|
*/
|
||||||
|
function _drupal_session_close() {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads an entire session from the database (internal use only).
|
||||||
|
*
|
||||||
|
* Also initializes the $user object for the user associated with the session.
|
||||||
|
* This function is registered with session_set_save_handler() to support
|
||||||
|
* database-backed sessions. It is called on every page load when PHP sets
|
||||||
|
* up the $_SESSION superglobal.
|
||||||
|
*
|
||||||
|
* This function is an internal function and must not be called directly.
|
||||||
|
* Doing so may result in logging out the current user, corrupting session data
|
||||||
|
* or other unexpected behavior. Session data must always be accessed via the
|
||||||
|
* $_SESSION superglobal.
|
||||||
|
*
|
||||||
|
* @param $sid
|
||||||
|
* The session ID of the session to retrieve.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The user's session, or an empty string if no session exists.
|
||||||
|
*/
|
||||||
|
function _drupal_session_read($sid) {
|
||||||
|
global $user, $is_https;
|
||||||
|
|
||||||
|
// Write and Close handlers are called after destructing objects
|
||||||
|
// since PHP 5.0.5.
|
||||||
|
// Thus destructors can use sessions but session handler can't use objects.
|
||||||
|
// So we are moving session closure before destructing objects.
|
||||||
|
drupal_register_shutdown_function('session_write_close');
|
||||||
|
|
||||||
|
// Handle the case of first time visitors and clients that don't store
|
||||||
|
// cookies (eg. web crawlers).
|
||||||
|
$insecure_session_name = substr(session_name(), 1);
|
||||||
|
if (empty($sid) || (!isset($_COOKIE[session_name()]) && !isset($_COOKIE[$insecure_session_name]))) {
|
||||||
|
$user = drupal_anonymous_user();
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, if the session is still active, we have a record of the
|
||||||
|
// client's session in the database. If it's HTTPS then we are either have
|
||||||
|
// a HTTPS session or we are about to log in so we check the sessions table
|
||||||
|
// for an anonymous session with the non-HTTPS-only cookie.
|
||||||
|
if ($is_https) {
|
||||||
|
$user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.ssid = :ssid", array(':ssid' => $sid))->fetchObject();
|
||||||
|
if (!$user) {
|
||||||
|
if (isset($_COOKIE[$insecure_session_name])) {
|
||||||
|
$user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid AND s.uid = 0", array(
|
||||||
|
':sid' => $_COOKIE[$insecure_session_name]))
|
||||||
|
->fetchObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$user = db_query("SELECT u.*, s.* FROM {users} u INNER JOIN {sessions} s ON u.uid = s.uid WHERE s.sid = :sid", array(':sid' => $sid))->fetchObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
// We found the client's session record and they are an authenticated,
|
||||||
|
// active user.
|
||||||
|
if ($user && $user->uid > 0 && $user->status == 1) {
|
||||||
|
// This is done to unserialize the data member of $user.
|
||||||
|
$user->data = unserialize($user->data);
|
||||||
|
|
||||||
|
// Add roles element to $user.
|
||||||
|
$user->roles = array();
|
||||||
|
$user->roles[DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
|
||||||
|
$user->roles += db_query("SELECT r.rid, r.name FROM {role} r INNER JOIN {users_roles} ur ON ur.rid = r.rid WHERE ur.uid = :uid", array(':uid' => $user->uid))->fetchAllKeyed(0, 1);
|
||||||
|
}
|
||||||
|
elseif ($user) {
|
||||||
|
// The user is anonymous or blocked. Only preserve two fields from the
|
||||||
|
// {sessions} table.
|
||||||
|
$account = drupal_anonymous_user();
|
||||||
|
$account->session = $user->session;
|
||||||
|
$account->timestamp = $user->timestamp;
|
||||||
|
$user = $account;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The session has expired.
|
||||||
|
$user = drupal_anonymous_user();
|
||||||
|
$user->session = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the session that was read for comparison in _drupal_session_write().
|
||||||
|
$last_read = &drupal_static('drupal_session_last_read');
|
||||||
|
$last_read = array(
|
||||||
|
'sid' => $sid,
|
||||||
|
'value' => $user->session,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $user->session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes an entire session to the database (internal use only).
|
||||||
|
*
|
||||||
|
* This function is registered with session_set_save_handler() to support
|
||||||
|
* database-backed sessions.
|
||||||
|
*
|
||||||
|
* This function is an internal function and must not be called directly.
|
||||||
|
* Doing so may result in corrupted session data or other unexpected behavior.
|
||||||
|
* Session data must always be accessed via the $_SESSION superglobal.
|
||||||
|
*
|
||||||
|
* @param $sid
|
||||||
|
* The session ID of the session to write to.
|
||||||
|
* @param $value
|
||||||
|
* Session data to write as a serialized string.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Always returns TRUE.
|
||||||
|
*/
|
||||||
|
function _drupal_session_write($sid, $value) {
|
||||||
|
global $user, $is_https;
|
||||||
|
|
||||||
|
// The exception handler is not active at this point, so we need to do it
|
||||||
|
// manually.
|
||||||
|
try {
|
||||||
|
if (!drupal_save_session()) {
|
||||||
|
// We don't have anything to do if we are not allowed to save the session.
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check whether $_SESSION has been changed in this request.
|
||||||
|
$last_read = &drupal_static('drupal_session_last_read');
|
||||||
|
$is_changed = !isset($last_read) || $last_read['sid'] != $sid || $last_read['value'] !== $value;
|
||||||
|
|
||||||
|
// For performance reasons, do not update the sessions table, unless
|
||||||
|
// $_SESSION has changed or more than 180 has passed since the last update.
|
||||||
|
if ($is_changed || !isset($user->timestamp) || REQUEST_TIME - $user->timestamp > variable_get('session_write_interval', 180)) {
|
||||||
|
// Either ssid or sid or both will be added from $key below.
|
||||||
|
$fields = array(
|
||||||
|
'uid' => $user->uid,
|
||||||
|
'cache' => isset($user->cache) ? $user->cache : 0,
|
||||||
|
'hostname' => ip_address(),
|
||||||
|
'session' => $value,
|
||||||
|
'timestamp' => REQUEST_TIME,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use the session ID as 'sid' and an empty string as 'ssid' by default.
|
||||||
|
// _drupal_session_read() does not allow empty strings so that's a safe
|
||||||
|
// default.
|
||||||
|
$key = array('sid' => $sid, 'ssid' => '');
|
||||||
|
// On HTTPS connections, use the session ID as both 'sid' and 'ssid'.
|
||||||
|
if ($is_https) {
|
||||||
|
$key['ssid'] = $sid;
|
||||||
|
// The "secure pages" setting allows a site to simultaneously use both
|
||||||
|
// secure and insecure session cookies. If enabled and both cookies are
|
||||||
|
// presented then use both keys.
|
||||||
|
if (variable_get('https', FALSE)) {
|
||||||
|
$insecure_session_name = substr(session_name(), 1);
|
||||||
|
if (isset($_COOKIE[$insecure_session_name])) {
|
||||||
|
$key['sid'] = $_COOKIE[$insecure_session_name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (variable_get('https', FALSE)) {
|
||||||
|
unset($key['ssid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
db_merge('sessions')
|
||||||
|
->key($key)
|
||||||
|
->fields($fields)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Likewise, do not update access time more than once per 180 seconds.
|
||||||
|
if ($user->uid && REQUEST_TIME - $user->access > variable_get('session_write_interval', 180)) {
|
||||||
|
db_update('users')
|
||||||
|
->fields(array(
|
||||||
|
'access' => REQUEST_TIME
|
||||||
|
))
|
||||||
|
->condition('uid', $user->uid)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
catch (Exception $exception) {
|
||||||
|
require_once DRUPAL_ROOT . '/includes/errors.inc';
|
||||||
|
// If we are displaying errors, then do so with no possibility of a further
|
||||||
|
// uncaught exception being thrown.
|
||||||
|
if (error_displayable()) {
|
||||||
|
print '<h1>Uncaught exception thrown in session handler.</h1>';
|
||||||
|
print '<p>' . _drupal_render_exception_safe($exception) . '</p><hr />';
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the session handler, starting a session if needed.
|
||||||
|
*/
|
||||||
|
function drupal_session_initialize() {
|
||||||
|
global $user, $is_https;
|
||||||
|
|
||||||
|
session_set_save_handler('_drupal_session_open', '_drupal_session_close', '_drupal_session_read', '_drupal_session_write', '_drupal_session_destroy', '_drupal_session_garbage_collection');
|
||||||
|
|
||||||
|
// We use !empty() in the following check to ensure that blank session IDs
|
||||||
|
// are not valid.
|
||||||
|
if (!empty($_COOKIE[session_name()]) || ($is_https && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) {
|
||||||
|
// If a session cookie exists, initialize the session. Otherwise the
|
||||||
|
// session is only started on demand in drupal_session_commit(), making
|
||||||
|
// anonymous users not use a session cookie unless something is stored in
|
||||||
|
// $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
|
||||||
|
drupal_session_start();
|
||||||
|
if (!empty($user->uid) || !empty($_SESSION)) {
|
||||||
|
drupal_page_is_cacheable(FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Set a session identifier for this request. This is necessary because
|
||||||
|
// we lazily start sessions at the end of this request, and some
|
||||||
|
// processes (like drupal_get_token()) needs to know the future
|
||||||
|
// session ID in advance.
|
||||||
|
$GLOBALS['lazy_session'] = TRUE;
|
||||||
|
$user = drupal_anonymous_user();
|
||||||
|
// Less random sessions (which are much faster to generate) are used for
|
||||||
|
// anonymous users than are generated in drupal_session_regenerate() when
|
||||||
|
// a user becomes authenticated.
|
||||||
|
session_id(drupal_random_key());
|
||||||
|
if ($is_https && variable_get('https', FALSE)) {
|
||||||
|
$insecure_session_name = substr(session_name(), 1);
|
||||||
|
$session_id = drupal_random_key();
|
||||||
|
$_COOKIE[$insecure_session_name] = $session_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date_default_timezone_set(drupal_get_user_timezone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a session forcefully, preserving already set session data.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_session_start() {
|
||||||
|
// Command line clients do not support cookies nor sessions.
|
||||||
|
if (!drupal_session_started() && !drupal_is_cli()) {
|
||||||
|
// Save current session data before starting it, as PHP will destroy it.
|
||||||
|
$session_data = isset($_SESSION) ? $_SESSION : NULL;
|
||||||
|
|
||||||
|
session_start();
|
||||||
|
drupal_session_started(TRUE);
|
||||||
|
|
||||||
|
// Restore session data.
|
||||||
|
if (!empty($session_data)) {
|
||||||
|
$_SESSION += $session_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits the current session, if necessary.
|
||||||
|
*
|
||||||
|
* If an anonymous user already have an empty session, destroy it.
|
||||||
|
*/
|
||||||
|
function drupal_session_commit() {
|
||||||
|
global $user, $is_https;
|
||||||
|
|
||||||
|
if (!drupal_save_session()) {
|
||||||
|
// We don't have anything to do if we are not allowed to save the session.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($user->uid) && empty($_SESSION)) {
|
||||||
|
// There is no session data to store, destroy the session if it was
|
||||||
|
// previously started.
|
||||||
|
if (drupal_session_started()) {
|
||||||
|
session_destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// There is session data to store. Start the session if it is not already
|
||||||
|
// started.
|
||||||
|
if (!drupal_session_started()) {
|
||||||
|
drupal_session_start();
|
||||||
|
if ($is_https && variable_get('https', FALSE)) {
|
||||||
|
$insecure_session_name = substr(session_name(), 1);
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||||
|
setcookie($insecure_session_name, $_COOKIE[$insecure_session_name], $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Write the session data.
|
||||||
|
session_write_close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a session has been started.
|
||||||
|
*/
|
||||||
|
function drupal_session_started($set = NULL) {
|
||||||
|
static $session_started = FALSE;
|
||||||
|
if (isset($set)) {
|
||||||
|
$session_started = $set;
|
||||||
|
}
|
||||||
|
return $session_started && session_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when an anonymous user becomes authenticated or vice-versa.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_session_regenerate() {
|
||||||
|
global $user, $is_https;
|
||||||
|
// Nothing to do if we are not allowed to change the session.
|
||||||
|
if (!drupal_save_session()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_https && variable_get('https', FALSE)) {
|
||||||
|
$insecure_session_name = substr(session_name(), 1);
|
||||||
|
if (!isset($GLOBALS['lazy_session']) && isset($_COOKIE[$insecure_session_name])) {
|
||||||
|
$old_insecure_session_id = $_COOKIE[$insecure_session_name];
|
||||||
|
}
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
$session_id = drupal_random_key();
|
||||||
|
// If a session cookie lifetime is set, the session will expire
|
||||||
|
// $params['lifetime'] seconds from the current request. If it is not set,
|
||||||
|
// it will expire when the browser is closed.
|
||||||
|
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||||
|
setcookie($insecure_session_name, $session_id, $expire, $params['path'], $params['domain'], FALSE, $params['httponly']);
|
||||||
|
$_COOKIE[$insecure_session_name] = $session_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drupal_session_started()) {
|
||||||
|
$old_session_id = session_id();
|
||||||
|
}
|
||||||
|
session_id(drupal_random_key());
|
||||||
|
|
||||||
|
if (isset($old_session_id)) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
$expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
|
||||||
|
setcookie(session_name(), session_id(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||||
|
$fields = array('sid' => session_id());
|
||||||
|
if ($is_https) {
|
||||||
|
$fields['ssid'] = session_id();
|
||||||
|
// If the "secure pages" setting is enabled, use the newly-created
|
||||||
|
// insecure session identifier as the regenerated sid.
|
||||||
|
if (variable_get('https', FALSE)) {
|
||||||
|
$fields['sid'] = $session_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
db_update('sessions')
|
||||||
|
->fields($fields)
|
||||||
|
->condition($is_https ? 'ssid' : 'sid', $old_session_id)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
elseif (isset($old_insecure_session_id)) {
|
||||||
|
// If logging in to the secure site, and there was no active session on the
|
||||||
|
// secure site but a session was active on the insecure site, update the
|
||||||
|
// insecure session with the new session identifiers.
|
||||||
|
db_update('sessions')
|
||||||
|
->fields(array('sid' => $session_id, 'ssid' => session_id()))
|
||||||
|
->condition('sid', $old_insecure_session_id)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Start the session when it doesn't exist yet.
|
||||||
|
// Preserve the logged in user, as it will be reset to anonymous
|
||||||
|
// by _drupal_session_read.
|
||||||
|
$account = $user;
|
||||||
|
drupal_session_start();
|
||||||
|
$user = $account;
|
||||||
|
}
|
||||||
|
date_default_timezone_set(drupal_get_user_timezone());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session handler assigned by session_set_save_handler().
|
||||||
|
*
|
||||||
|
* Cleans up a specific session.
|
||||||
|
*
|
||||||
|
* @param $sid
|
||||||
|
* Session ID.
|
||||||
|
*/
|
||||||
|
function _drupal_session_destroy($sid) {
|
||||||
|
global $user, $is_https;
|
||||||
|
|
||||||
|
// Nothing to do if we are not allowed to change the session.
|
||||||
|
if (!drupal_save_session()) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete session data.
|
||||||
|
db_delete('sessions')
|
||||||
|
->condition($is_https ? 'ssid' : 'sid', $sid)
|
||||||
|
->execute();
|
||||||
|
|
||||||
|
// Reset $_SESSION and $user to prevent a new session from being started
|
||||||
|
// in drupal_session_commit().
|
||||||
|
$_SESSION = array();
|
||||||
|
$user = drupal_anonymous_user();
|
||||||
|
|
||||||
|
// Unset the session cookies.
|
||||||
|
_drupal_session_delete_cookie(session_name());
|
||||||
|
if ($is_https) {
|
||||||
|
_drupal_session_delete_cookie(substr(session_name(), 1), FALSE);
|
||||||
|
}
|
||||||
|
elseif (variable_get('https', FALSE)) {
|
||||||
|
_drupal_session_delete_cookie('S' . session_name(), TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the session cookie.
|
||||||
|
*
|
||||||
|
* @param $name
|
||||||
|
* Name of session cookie to delete.
|
||||||
|
* @param boolean $secure
|
||||||
|
* Force the secure value of the cookie.
|
||||||
|
*/
|
||||||
|
function _drupal_session_delete_cookie($name, $secure = NULL) {
|
||||||
|
global $is_https;
|
||||||
|
if (isset($_COOKIE[$name]) || (!$is_https && $secure === TRUE)) {
|
||||||
|
$params = session_get_cookie_params();
|
||||||
|
if ($secure !== NULL) {
|
||||||
|
$params['secure'] = $secure;
|
||||||
|
}
|
||||||
|
setcookie($name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
|
||||||
|
unset($_COOKIE[$name]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends a specific user's session(s).
|
||||||
|
*
|
||||||
|
* @param $uid
|
||||||
|
* User ID.
|
||||||
|
*/
|
||||||
|
function drupal_session_destroy_uid($uid) {
|
||||||
|
// Nothing to do if we are not allowed to change the session.
|
||||||
|
if (!drupal_save_session()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
db_delete('sessions')
|
||||||
|
->condition('uid', $uid)
|
||||||
|
->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session handler assigned by session_set_save_handler().
|
||||||
|
*
|
||||||
|
* Cleans up stalled sessions.
|
||||||
|
*
|
||||||
|
* @param $lifetime
|
||||||
|
* The value of session.gc_maxlifetime, passed by PHP.
|
||||||
|
* Sessions not updated for more than $lifetime seconds will be removed.
|
||||||
|
*/
|
||||||
|
function _drupal_session_garbage_collection($lifetime) {
|
||||||
|
// Be sure to adjust 'php_value session.gc_maxlifetime' to a large enough
|
||||||
|
// value. For example, if you want user sessions to stay in your database
|
||||||
|
// for three weeks before deleting them, you need to set gc_maxlifetime
|
||||||
|
// to '1814400'. At that value, only after a user doesn't log in after
|
||||||
|
// three weeks (1814400 seconds) will his/her session be removed.
|
||||||
|
db_delete('sessions')
|
||||||
|
->condition('timestamp', REQUEST_TIME - $lifetime, '<')
|
||||||
|
->execute();
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether to save session data of the current request.
|
||||||
|
*
|
||||||
|
* This function allows the caller to temporarily disable writing of
|
||||||
|
* session data, should the request end while performing potentially
|
||||||
|
* dangerous operations, such as manipulating the global $user object.
|
||||||
|
* See http://drupal.org/node/218104 for usage.
|
||||||
|
*
|
||||||
|
* @param $status
|
||||||
|
* Disables writing of session data when FALSE, (re-)enables
|
||||||
|
* writing when TRUE.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* FALSE if writing session data has been disabled. Otherwise, TRUE.
|
||||||
|
*/
|
||||||
|
function drupal_save_session($status = NULL) {
|
||||||
|
// PHP session ID, session, and cookie handling happens in the global scope.
|
||||||
|
// This value has to persist across calls to drupal_static_reset(), since a
|
||||||
|
// potentially wrong or disallowed session would be written otherwise.
|
||||||
|
static $save_session = TRUE;
|
||||||
|
if (isset($status)) {
|
||||||
|
$save_session = $status;
|
||||||
|
}
|
||||||
|
return $save_session;
|
||||||
|
}
|
982
includes/stream_wrappers.inc
Normal file
982
includes/stream_wrappers.inc
Normal file
|
@ -0,0 +1,982 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Drupal stream wrapper interface.
|
||||||
|
*
|
||||||
|
* Provides a Drupal interface and classes to implement PHP stream wrappers for
|
||||||
|
* public, private, and temporary files.
|
||||||
|
*
|
||||||
|
* A stream wrapper is an abstraction of a file system that allows Drupal to
|
||||||
|
* use the same set of methods to access both local files and remote resources.
|
||||||
|
*
|
||||||
|
* Note that PHP 5.2 fopen() only supports URIs of the form "scheme://target"
|
||||||
|
* despite the fact that according to RFC 3986 a URI's scheme component
|
||||||
|
* delimiter is in general just ":", not "://". Because of this PHP limitation
|
||||||
|
* and for consistency Drupal will only accept URIs of form "scheme://target".
|
||||||
|
*
|
||||||
|
* @see http://www.faqs.org/rfcs/rfc3986.html
|
||||||
|
* @see http://bugs.php.net/bug.php?id=47070
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flags that are the basis for composite types.
|
||||||
|
*
|
||||||
|
* Note that 0x0002 is skipped, because it was the value of a constant that has
|
||||||
|
* since been removed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flag -- a filter that matches all wrappers.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_ALL', 0x0000);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flag -- refers to a local file system location.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_LOCAL', 0x0001);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flag -- wrapper is readable (almost always true).
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_READ', 0x0004);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flag -- wrapper is writeable.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_WRITE', 0x0008);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper bit flag -- exposed in the UI and potentially web accessible.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_VISIBLE', 0x0010);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composite stream wrapper bit flags that are usually used as the types.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- not visible in the UI or accessible via web,
|
||||||
|
* but readable and writable. E.g. the temporary directory for uploads.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_HIDDEN', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- hidden, readable and writeable using local files.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_LOCAL_HIDDEN', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_HIDDEN);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- visible, readable and writeable.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_WRITE_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_WRITE | STREAM_WRAPPERS_VISIBLE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- visible and read-only.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_READ_VISIBLE', STREAM_WRAPPERS_READ | STREAM_WRAPPERS_VISIBLE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- the default when 'type' is omitted from
|
||||||
|
* hook_stream_wrappers(). This does not include STREAM_WRAPPERS_LOCAL,
|
||||||
|
* because PHP grants a greater trust level to local files (for example, they
|
||||||
|
* can be used in an "include" statement, regardless of the "allow_url_include"
|
||||||
|
* setting), so stream wrappers need to explicitly opt-in to this.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_NORMAL', STREAM_WRAPPERS_WRITE_VISIBLE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stream wrapper type flag -- visible, readable and writeable using local files.
|
||||||
|
*/
|
||||||
|
define('STREAM_WRAPPERS_LOCAL_NORMAL', STREAM_WRAPPERS_LOCAL | STREAM_WRAPPERS_NORMAL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic PHP stream wrapper interface.
|
||||||
|
*
|
||||||
|
* @see http://www.php.net/manual/class.streamwrapper.php
|
||||||
|
*/
|
||||||
|
interface StreamWrapperInterface {
|
||||||
|
public function stream_open($uri, $mode, $options, &$opened_url);
|
||||||
|
public function stream_close();
|
||||||
|
public function stream_lock($operation);
|
||||||
|
public function stream_read($count);
|
||||||
|
public function stream_write($data);
|
||||||
|
public function stream_eof();
|
||||||
|
public function stream_seek($offset, $whence);
|
||||||
|
public function stream_flush();
|
||||||
|
public function stream_tell();
|
||||||
|
public function stream_stat();
|
||||||
|
public function unlink($uri);
|
||||||
|
public function rename($from_uri, $to_uri);
|
||||||
|
public function mkdir($uri, $mode, $options);
|
||||||
|
public function rmdir($uri, $options);
|
||||||
|
public function url_stat($uri, $flags);
|
||||||
|
public function dir_opendir($uri, $options);
|
||||||
|
public function dir_readdir();
|
||||||
|
public function dir_rewinddir();
|
||||||
|
public function dir_closedir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal stream wrapper extension.
|
||||||
|
*
|
||||||
|
* Extend the StreamWrapperInterface with methods expected by Drupal stream
|
||||||
|
* wrapper classes.
|
||||||
|
*/
|
||||||
|
interface DrupalStreamWrapperInterface extends StreamWrapperInterface {
|
||||||
|
/**
|
||||||
|
* Set the absolute stream resource URI.
|
||||||
|
*
|
||||||
|
* This allows you to set the URI. Generally is only called by the factory
|
||||||
|
* method.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI that should be used for this instance.
|
||||||
|
*/
|
||||||
|
public function setUri($uri);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the stream resource URI.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns the current URI of the instance.
|
||||||
|
*/
|
||||||
|
public function getUri();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a web accessible URL for the resource.
|
||||||
|
*
|
||||||
|
* This function should return a URL that can be embedded in a web page
|
||||||
|
* and accessed from a browser. For example, the external URL of
|
||||||
|
* "youtube://xIpLd0WQKCY" might be
|
||||||
|
* "http://www.youtube.com/watch?v=xIpLd0WQKCY".
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns a string containing a web accessible URL for the resource.
|
||||||
|
*/
|
||||||
|
public function getExternalUrl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type of the resource.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* The URI, path, or filename.
|
||||||
|
* @param $mapping
|
||||||
|
* An optional map of extensions to their mimetypes, in the form:
|
||||||
|
* - 'mimetypes': a list of mimetypes, keyed by an identifier,
|
||||||
|
* - 'extensions': the mapping itself, an associative array in which
|
||||||
|
* the key is the extension and the value is the mimetype identifier.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns a string containing the MIME type of the resource.
|
||||||
|
*/
|
||||||
|
public static function getMimeType($uri, $mapping = NULL);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes permissions of the resource.
|
||||||
|
*
|
||||||
|
* PHP lacks this functionality and it is not part of the official stream
|
||||||
|
* wrapper interface. This is a custom implementation for Drupal.
|
||||||
|
*
|
||||||
|
* @param $mode
|
||||||
|
* Integer value for the permissions. Consult PHP chmod() documentation
|
||||||
|
* for more information.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns TRUE on success or FALSE on failure.
|
||||||
|
*/
|
||||||
|
public function chmod($mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns canonical, absolute path of the resource.
|
||||||
|
*
|
||||||
|
* Implementation placeholder. PHP's realpath() does not support stream
|
||||||
|
* wrappers. We provide this as a default so that individual wrappers may
|
||||||
|
* implement their own solutions.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns a string with absolute pathname on success (implemented
|
||||||
|
* by core wrappers), or FALSE on failure or if the registered
|
||||||
|
* wrapper does not provide an implementation.
|
||||||
|
*/
|
||||||
|
public function realpath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the directory from a given path.
|
||||||
|
*
|
||||||
|
* This method is usually accessed through drupal_dirname(), which wraps
|
||||||
|
* around the normal PHP dirname() function, which does not support stream
|
||||||
|
* wrappers.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* An optional URI.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A string containing the directory name, or FALSE if not applicable.
|
||||||
|
*
|
||||||
|
* @see drupal_dirname()
|
||||||
|
*/
|
||||||
|
public function dirname($uri = NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal stream wrapper base class for local files.
|
||||||
|
*
|
||||||
|
* This class provides a complete stream wrapper implementation. URIs such as
|
||||||
|
* "public://example.txt" are expanded to a normal filesystem path such as
|
||||||
|
* "sites/default/files/example.txt" and then PHP filesystem functions are
|
||||||
|
* invoked.
|
||||||
|
*
|
||||||
|
* DrupalLocalStreamWrapper implementations need to implement at least the
|
||||||
|
* getDirectoryPath() and getExternalUrl() methods.
|
||||||
|
*/
|
||||||
|
abstract class DrupalLocalStreamWrapper implements DrupalStreamWrapperInterface {
|
||||||
|
/**
|
||||||
|
* Stream context resource.
|
||||||
|
*
|
||||||
|
* @var Resource
|
||||||
|
*/
|
||||||
|
public $context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A generic resource handle.
|
||||||
|
*
|
||||||
|
* @var Resource
|
||||||
|
*/
|
||||||
|
public $handle = NULL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance URI (stream).
|
||||||
|
*
|
||||||
|
* A stream is referenced as "scheme://target".
|
||||||
|
*
|
||||||
|
* @var String
|
||||||
|
*/
|
||||||
|
protected $uri;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path that the wrapper is responsible for.
|
||||||
|
* @TODO: Review this method name in D8 per http://drupal.org/node/701358
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* String specifying the path.
|
||||||
|
*/
|
||||||
|
abstract function getDirectoryPath();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of setUri().
|
||||||
|
*/
|
||||||
|
function setUri($uri) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of getUri().
|
||||||
|
*/
|
||||||
|
function getUri() {
|
||||||
|
return $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the local writable target of the resource within the stream.
|
||||||
|
*
|
||||||
|
* This function should be used in place of calls to realpath() or similar
|
||||||
|
* functions when attempting to determine the location of a file. While
|
||||||
|
* functions like realpath() may return the location of a read-only file, this
|
||||||
|
* method may return a URI or path suitable for writing that is completely
|
||||||
|
* separate from the URI used for reading.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* Optional URI.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns a string representing a location suitable for writing of a file,
|
||||||
|
* or FALSE if unable to write to the file such as with read-only streams.
|
||||||
|
*/
|
||||||
|
protected function getTarget($uri = NULL) {
|
||||||
|
if (!isset($uri)) {
|
||||||
|
$uri = $this->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
list($scheme, $target) = explode('://', $uri, 2);
|
||||||
|
|
||||||
|
// Remove erroneous leading or trailing, forward-slashes and backslashes.
|
||||||
|
return trim($target, '\/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of getMimeType().
|
||||||
|
*/
|
||||||
|
static function getMimeType($uri, $mapping = NULL) {
|
||||||
|
if (!isset($mapping)) {
|
||||||
|
// The default file map, defined in file.mimetypes.inc is quite big.
|
||||||
|
// We only load it when necessary.
|
||||||
|
include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc';
|
||||||
|
$mapping = file_mimetype_mapping();
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = '';
|
||||||
|
$file_parts = explode('.', drupal_basename($uri));
|
||||||
|
|
||||||
|
// Remove the first part: a full filename should not match an extension.
|
||||||
|
array_shift($file_parts);
|
||||||
|
|
||||||
|
// Iterate over the file parts, trying to find a match.
|
||||||
|
// For my.awesome.image.jpeg, we try:
|
||||||
|
// - jpeg
|
||||||
|
// - image.jpeg, and
|
||||||
|
// - awesome.image.jpeg
|
||||||
|
while ($additional_part = array_pop($file_parts)) {
|
||||||
|
$extension = strtolower($additional_part . ($extension ? '.' . $extension : ''));
|
||||||
|
if (isset($mapping['extensions'][$extension])) {
|
||||||
|
return $mapping['mimetypes'][$mapping['extensions'][$extension]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of chmod().
|
||||||
|
*/
|
||||||
|
function chmod($mode) {
|
||||||
|
$output = @chmod($this->getLocalPath(), $mode);
|
||||||
|
// We are modifying the underlying file here, so we have to clear the stat
|
||||||
|
// cache so that PHP understands that URI has changed too.
|
||||||
|
clearstatcache();
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base implementation of realpath().
|
||||||
|
*/
|
||||||
|
function realpath() {
|
||||||
|
return $this->getLocalPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the canonical absolute path of the URI, if possible.
|
||||||
|
*
|
||||||
|
* @param string $uri
|
||||||
|
* (optional) The stream wrapper URI to be converted to a canonical
|
||||||
|
* absolute path. This may point to a directory or another type of file.
|
||||||
|
*
|
||||||
|
* @return string|false
|
||||||
|
* If $uri is not set, returns the canonical absolute path of the URI
|
||||||
|
* previously set by the DrupalStreamWrapperInterface::setUri() function.
|
||||||
|
* If $uri is set and valid for this class, returns its canonical absolute
|
||||||
|
* path, as determined by the realpath() function. If $uri is set but not
|
||||||
|
* valid, returns FALSE.
|
||||||
|
*/
|
||||||
|
protected function getLocalPath($uri = NULL) {
|
||||||
|
if (!isset($uri)) {
|
||||||
|
$uri = $this->uri;
|
||||||
|
}
|
||||||
|
$path = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
|
||||||
|
$realpath = realpath($path);
|
||||||
|
if (!$realpath) {
|
||||||
|
// This file does not yet exist.
|
||||||
|
$realpath = realpath(dirname($path)) . '/' . drupal_basename($path);
|
||||||
|
}
|
||||||
|
$directory = realpath($this->getDirectoryPath());
|
||||||
|
if (!$realpath || !$directory || strpos($realpath, $directory) !== 0) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return $realpath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fopen(), file_get_contents(), file_put_contents() etc.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to the file to open.
|
||||||
|
* @param $mode
|
||||||
|
* The file mode ("r", "wb" etc.).
|
||||||
|
* @param $options
|
||||||
|
* A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS.
|
||||||
|
* @param $opened_path
|
||||||
|
* A string containing the path actually opened.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Returns TRUE if file was opened successfully.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-open.php
|
||||||
|
*/
|
||||||
|
public function stream_open($uri, $mode, $options, &$opened_path) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
$path = $this->getLocalPath();
|
||||||
|
$this->handle = ($options & STREAM_REPORT_ERRORS) ? fopen($path, $mode) : @fopen($path, $mode);
|
||||||
|
|
||||||
|
if ((bool) $this->handle && $options & STREAM_USE_PATH) {
|
||||||
|
$opened_path = $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for flock().
|
||||||
|
*
|
||||||
|
* @param $operation
|
||||||
|
* One of the following:
|
||||||
|
* - LOCK_SH to acquire a shared lock (reader).
|
||||||
|
* - LOCK_EX to acquire an exclusive lock (writer).
|
||||||
|
* - LOCK_UN to release a lock (shared or exclusive).
|
||||||
|
* - LOCK_NB if you don't want flock() to block while locking (not
|
||||||
|
* supported on Windows).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Always returns TRUE at the present time.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-lock.php
|
||||||
|
*/
|
||||||
|
public function stream_lock($operation) {
|
||||||
|
if (in_array($operation, array(LOCK_SH, LOCK_EX, LOCK_UN, LOCK_NB))) {
|
||||||
|
return flock($this->handle, $operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fread(), file_get_contents() etc.
|
||||||
|
*
|
||||||
|
* @param $count
|
||||||
|
* Maximum number of bytes to be read.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The string that was read, or FALSE in case of an error.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-read.php
|
||||||
|
*/
|
||||||
|
public function stream_read($count) {
|
||||||
|
return fread($this->handle, $count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fwrite(), file_put_contents() etc.
|
||||||
|
*
|
||||||
|
* @param $data
|
||||||
|
* The string to be written.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The number of bytes written (integer).
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-write.php
|
||||||
|
*/
|
||||||
|
public function stream_write($data) {
|
||||||
|
return fwrite($this->handle, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for feof().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if end-of-file has been reached.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-eof.php
|
||||||
|
*/
|
||||||
|
public function stream_eof() {
|
||||||
|
return feof($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fseek().
|
||||||
|
*
|
||||||
|
* @param $offset
|
||||||
|
* The byte offset to got to.
|
||||||
|
* @param $whence
|
||||||
|
* SEEK_SET, SEEK_CUR, or SEEK_END.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-seek.php
|
||||||
|
*/
|
||||||
|
public function stream_seek($offset, $whence) {
|
||||||
|
// fseek returns 0 on success and -1 on a failure.
|
||||||
|
// stream_seek 1 on success and 0 on a failure.
|
||||||
|
return !fseek($this->handle, $offset, $whence);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fflush().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if data was successfully stored (or there was no data to store).
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-flush.php
|
||||||
|
*/
|
||||||
|
public function stream_flush() {
|
||||||
|
return fflush($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for ftell().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The current offset in bytes from the beginning of file.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-tell.php
|
||||||
|
*/
|
||||||
|
public function stream_tell() {
|
||||||
|
return ftell($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fstat().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array with file status, or FALSE in case of an error - see fstat()
|
||||||
|
* for a description of this array.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-stat.php
|
||||||
|
*/
|
||||||
|
public function stream_stat() {
|
||||||
|
return fstat($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for fclose().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if stream was successfully closed.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.stream-close.php
|
||||||
|
*/
|
||||||
|
public function stream_close() {
|
||||||
|
return fclose($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets metadata on the stream.
|
||||||
|
*
|
||||||
|
* WARNING: Do not call this method directly! It will be called internally by
|
||||||
|
* PHP itself when one of the following functions is called on a stream URL:
|
||||||
|
*
|
||||||
|
* @param string $uri
|
||||||
|
* A string containing the URI to the file to set metadata on.
|
||||||
|
* @param int $option
|
||||||
|
* One of:
|
||||||
|
* - STREAM_META_TOUCH: The method was called in response to touch().
|
||||||
|
* - STREAM_META_OWNER_NAME: The method was called in response to chown()
|
||||||
|
* with string parameter.
|
||||||
|
* - STREAM_META_OWNER: The method was called in response to chown().
|
||||||
|
* - STREAM_META_GROUP_NAME: The method was called in response to chgrp().
|
||||||
|
* - STREAM_META_GROUP: The method was called in response to chgrp().
|
||||||
|
* - STREAM_META_ACCESS: The method was called in response to chmod().
|
||||||
|
* @param mixed $value
|
||||||
|
* If option is:
|
||||||
|
* - STREAM_META_TOUCH: Array consisting of two arguments of the touch()
|
||||||
|
* function.
|
||||||
|
* - STREAM_META_OWNER_NAME or STREAM_META_GROUP_NAME: The name of the owner
|
||||||
|
* user/group as string.
|
||||||
|
* - STREAM_META_OWNER or STREAM_META_GROUP: The value of the owner
|
||||||
|
* user/group as integer.
|
||||||
|
* - STREAM_META_ACCESS: The argument of the chmod() as integer.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* Returns TRUE on success or FALSE on failure. If $option is not
|
||||||
|
* implemented, FALSE should be returned.
|
||||||
|
*
|
||||||
|
* @see touch()
|
||||||
|
* @see chmod()
|
||||||
|
* @see chown()
|
||||||
|
* @see chgrp()
|
||||||
|
* @link http://php.net/manual/streamwrapper.stream-metadata.php
|
||||||
|
*/
|
||||||
|
public function stream_metadata($uri, $option, $value) {
|
||||||
|
$target = $this->getLocalPath($uri);
|
||||||
|
$return = FALSE;
|
||||||
|
switch ($option) {
|
||||||
|
case STREAM_META_TOUCH:
|
||||||
|
if (!empty($value)) {
|
||||||
|
$return = touch($target, $value[0], $value[1]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return = touch($target);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STREAM_META_OWNER_NAME:
|
||||||
|
case STREAM_META_OWNER:
|
||||||
|
$return = chown($target, $value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STREAM_META_GROUP_NAME:
|
||||||
|
case STREAM_META_GROUP:
|
||||||
|
$return = chgrp($target, $value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STREAM_META_ACCESS:
|
||||||
|
$return = chmod($target, $value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($return) {
|
||||||
|
// For convenience clear the file status cache of the underlying file,
|
||||||
|
// since metadata operations are often followed by file status checks.
|
||||||
|
clearstatcache(TRUE, $target);
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate stream.
|
||||||
|
*
|
||||||
|
* Will respond to truncation; e.g., through ftruncate().
|
||||||
|
*
|
||||||
|
* @param int $new_size
|
||||||
|
* The new size.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* TRUE on success, FALSE otherwise.
|
||||||
|
*/
|
||||||
|
public function stream_truncate($new_size) {
|
||||||
|
return ftruncate($this->handle, $new_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the underlying stream resource.
|
||||||
|
*
|
||||||
|
* This method is called in response to stream_select().
|
||||||
|
*
|
||||||
|
* @param int $cast_as
|
||||||
|
* Can be STREAM_CAST_FOR_SELECT when stream_select() is calling
|
||||||
|
* stream_cast() or STREAM_CAST_AS_STREAM when stream_cast() is called for
|
||||||
|
* other uses.
|
||||||
|
*
|
||||||
|
* @return resource|false
|
||||||
|
* The underlying stream resource or FALSE if stream_select() is not
|
||||||
|
* supported.
|
||||||
|
*
|
||||||
|
* @see stream_select()
|
||||||
|
* @link http://php.net/manual/streamwrapper.stream-cast.php
|
||||||
|
*/
|
||||||
|
public function stream_cast($cast_as) {
|
||||||
|
return $this->handle ? $this->handle : FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change stream options.
|
||||||
|
*
|
||||||
|
* This method is called to set options on the stream.
|
||||||
|
*
|
||||||
|
* Since Windows systems do not allow it and it is not needed for most use
|
||||||
|
* cases anyway, this method is not supported on local files and will trigger
|
||||||
|
* an error and return false. If needed, custom subclasses can provide
|
||||||
|
* OS-specific implementations for advanced use cases.
|
||||||
|
*
|
||||||
|
* @param int $option
|
||||||
|
* One of:
|
||||||
|
* - STREAM_OPTION_BLOCKING: The method was called in response to
|
||||||
|
* stream_set_blocking().
|
||||||
|
* - STREAM_OPTION_READ_TIMEOUT: The method was called in response to
|
||||||
|
* stream_set_timeout().
|
||||||
|
* - STREAM_OPTION_WRITE_BUFFER: The method was called in response to
|
||||||
|
* stream_set_write_buffer().
|
||||||
|
* @param int $arg1
|
||||||
|
* If option is:
|
||||||
|
* - STREAM_OPTION_BLOCKING: The requested blocking mode:
|
||||||
|
* - 1 means blocking.
|
||||||
|
* - 0 means not blocking.
|
||||||
|
* - STREAM_OPTION_READ_TIMEOUT: The timeout in seconds.
|
||||||
|
* - STREAM_OPTION_WRITE_BUFFER: The buffer mode, STREAM_BUFFER_NONE or
|
||||||
|
* STREAM_BUFFER_FULL.
|
||||||
|
* @param int $arg2
|
||||||
|
* If option is:
|
||||||
|
* - STREAM_OPTION_BLOCKING: This option is not set.
|
||||||
|
* - STREAM_OPTION_READ_TIMEOUT: The timeout in microseconds.
|
||||||
|
* - STREAM_OPTION_WRITE_BUFFER: The requested buffer size.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* TRUE on success, FALSE otherwise. If $option is not implemented, FALSE
|
||||||
|
* should be returned.
|
||||||
|
*/
|
||||||
|
public function stream_set_option($option, $arg1, $arg2) {
|
||||||
|
trigger_error('stream_set_option() not supported for local file based stream wrappers', E_USER_WARNING);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for unlink().
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to the resource to delete.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if resource was successfully deleted.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.unlink.php
|
||||||
|
*/
|
||||||
|
public function unlink($uri) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
return drupal_unlink($this->getLocalPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for rename().
|
||||||
|
*
|
||||||
|
* @param $from_uri,
|
||||||
|
* The URI to the file to rename.
|
||||||
|
* @param $to_uri
|
||||||
|
* The new URI for file.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if file was successfully renamed.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.rename.php
|
||||||
|
*/
|
||||||
|
public function rename($from_uri, $to_uri) {
|
||||||
|
return rename($this->getLocalPath($from_uri), $this->getLocalPath($to_uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the name of the directory from a given path.
|
||||||
|
*
|
||||||
|
* This method is usually accessed through drupal_dirname(), which wraps
|
||||||
|
* around the PHP dirname() function because it does not support stream
|
||||||
|
* wrappers.
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A URI or path.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A string containing the directory name.
|
||||||
|
*
|
||||||
|
* @see drupal_dirname()
|
||||||
|
*/
|
||||||
|
public function dirname($uri = NULL) {
|
||||||
|
list($scheme, $target) = explode('://', $uri, 2);
|
||||||
|
$target = $this->getTarget($uri);
|
||||||
|
$dirname = dirname($target);
|
||||||
|
|
||||||
|
if ($dirname == '.') {
|
||||||
|
$dirname = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scheme . '://' . $dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for mkdir().
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to the directory to create.
|
||||||
|
* @param $mode
|
||||||
|
* Permission flags - see mkdir().
|
||||||
|
* @param $options
|
||||||
|
* A bit mask of STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if directory was successfully created.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.mkdir.php
|
||||||
|
*/
|
||||||
|
public function mkdir($uri, $mode, $options) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
$recursive = (bool) ($options & STREAM_MKDIR_RECURSIVE);
|
||||||
|
if ($recursive) {
|
||||||
|
// $this->getLocalPath() fails if $uri has multiple levels of directories
|
||||||
|
// that do not yet exist.
|
||||||
|
$localpath = $this->getDirectoryPath() . '/' . $this->getTarget($uri);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$localpath = $this->getLocalPath($uri);
|
||||||
|
}
|
||||||
|
if ($options & STREAM_REPORT_ERRORS) {
|
||||||
|
return mkdir($localpath, $mode, $recursive);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return @mkdir($localpath, $mode, $recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for rmdir().
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to the directory to delete.
|
||||||
|
* @param $options
|
||||||
|
* A bit mask of STREAM_REPORT_ERRORS.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if directory was successfully removed.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.rmdir.php
|
||||||
|
*/
|
||||||
|
public function rmdir($uri, $options) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
if ($options & STREAM_REPORT_ERRORS) {
|
||||||
|
return drupal_rmdir($this->getLocalPath());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return @drupal_rmdir($this->getLocalPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for stat().
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to get information about.
|
||||||
|
* @param $flags
|
||||||
|
* A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array with file status, or FALSE in case of an error - see fstat()
|
||||||
|
* for a description of this array.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.url-stat.php
|
||||||
|
*/
|
||||||
|
public function url_stat($uri, $flags) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
$path = $this->getLocalPath();
|
||||||
|
// Suppress warnings if requested or if the file or directory does not
|
||||||
|
// exist. This is consistent with PHP's plain filesystem stream wrapper.
|
||||||
|
if ($flags & STREAM_URL_STAT_QUIET || !file_exists($path)) {
|
||||||
|
return @stat($path);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return stat($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for opendir().
|
||||||
|
*
|
||||||
|
* @param $uri
|
||||||
|
* A string containing the URI to the directory to open.
|
||||||
|
* @param $options
|
||||||
|
* Unknown (parameter is not documented in PHP Manual).
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.dir-opendir.php
|
||||||
|
*/
|
||||||
|
public function dir_opendir($uri, $options) {
|
||||||
|
$this->uri = $uri;
|
||||||
|
$this->handle = opendir($this->getLocalPath());
|
||||||
|
|
||||||
|
return (bool) $this->handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for readdir().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The next filename, or FALSE if there are no more files in the directory.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.dir-readdir.php
|
||||||
|
*/
|
||||||
|
public function dir_readdir() {
|
||||||
|
return readdir($this->handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for rewinddir().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.dir-rewinddir.php
|
||||||
|
*/
|
||||||
|
public function dir_rewinddir() {
|
||||||
|
rewinddir($this->handle);
|
||||||
|
// We do not really have a way to signal a failure as rewinddir() does not
|
||||||
|
// have a return value and there is no way to read a directory handler
|
||||||
|
// without advancing to the next file.
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for closedir().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE on success.
|
||||||
|
*
|
||||||
|
* @see http://php.net/manual/streamwrapper.dir-closedir.php
|
||||||
|
*/
|
||||||
|
public function dir_closedir() {
|
||||||
|
closedir($this->handle);
|
||||||
|
// We do not really have a way to signal a failure as closedir() does not
|
||||||
|
// have a return value.
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal public (public://) stream wrapper class.
|
||||||
|
*
|
||||||
|
* Provides support for storing publicly accessible files with the Drupal file
|
||||||
|
* interface.
|
||||||
|
*/
|
||||||
|
class DrupalPublicStreamWrapper extends DrupalLocalStreamWrapper {
|
||||||
|
/**
|
||||||
|
* Implements abstract public function getDirectoryPath()
|
||||||
|
*/
|
||||||
|
public function getDirectoryPath() {
|
||||||
|
return variable_get('file_public_path', conf_path() . '/files');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides getExternalUrl().
|
||||||
|
*
|
||||||
|
* Return the HTML URI of a public file.
|
||||||
|
*/
|
||||||
|
function getExternalUrl() {
|
||||||
|
$path = str_replace('\\', '/', $this->getTarget());
|
||||||
|
return $GLOBALS['base_url'] . '/' . self::getDirectoryPath() . '/' . drupal_encode_path($path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal private (private://) stream wrapper class.
|
||||||
|
*
|
||||||
|
* Provides support for storing privately accessible files with the Drupal file
|
||||||
|
* interface.
|
||||||
|
*/
|
||||||
|
class DrupalPrivateStreamWrapper extends DrupalLocalStreamWrapper {
|
||||||
|
/**
|
||||||
|
* Implements abstract public function getDirectoryPath()
|
||||||
|
*/
|
||||||
|
public function getDirectoryPath() {
|
||||||
|
return variable_get('file_private_path', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides getExternalUrl().
|
||||||
|
*
|
||||||
|
* Return the HTML URI of a private file.
|
||||||
|
*/
|
||||||
|
function getExternalUrl() {
|
||||||
|
$path = str_replace('\\', '/', $this->getTarget());
|
||||||
|
return url('system/files/' . $path, array('absolute' => TRUE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal temporary (temporary://) stream wrapper class.
|
||||||
|
*
|
||||||
|
* Provides support for storing temporarily accessible files with the Drupal
|
||||||
|
* file interface.
|
||||||
|
*
|
||||||
|
* Extends DrupalPublicStreamWrapper.
|
||||||
|
*/
|
||||||
|
class DrupalTemporaryStreamWrapper extends DrupalLocalStreamWrapper {
|
||||||
|
/**
|
||||||
|
* Implements abstract public function getDirectoryPath()
|
||||||
|
*/
|
||||||
|
public function getDirectoryPath() {
|
||||||
|
return variable_get('file_temporary_path', file_directory_temp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides getExternalUrl().
|
||||||
|
*/
|
||||||
|
public function getExternalUrl() {
|
||||||
|
$path = str_replace('\\', '/', $this->getTarget());
|
||||||
|
return url('system/temporary/' . $path, array('absolute' => TRUE));
|
||||||
|
}
|
||||||
|
}
|
255
includes/tablesort.inc
Normal file
255
includes/tablesort.inc
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Functions to aid in the creation of sortable tables.
|
||||||
|
*
|
||||||
|
* All tables created with a call to theme('table') have the option of having
|
||||||
|
* column headers that the user can click on to sort the table by that column.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query extender class for tablesort queries.
|
||||||
|
*/
|
||||||
|
class TableSort extends SelectQueryExtender {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The array of fields that can be sorted by.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $header = array();
|
||||||
|
|
||||||
|
public function __construct(SelectQueryInterface $query, DatabaseConnection $connection) {
|
||||||
|
parent::__construct($query, $connection);
|
||||||
|
|
||||||
|
// Add convenience tag to mark that this is an extended query. We have to
|
||||||
|
// do this in the constructor to ensure that it is set before preExecute()
|
||||||
|
// gets called.
|
||||||
|
$this->addTag('tablesort');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order the query based on a header array.
|
||||||
|
*
|
||||||
|
* @see theme_table()
|
||||||
|
* @param $header
|
||||||
|
* Table header array.
|
||||||
|
* @return SelectQueryInterface
|
||||||
|
* The called object.
|
||||||
|
*/
|
||||||
|
public function orderByHeader(Array $header) {
|
||||||
|
$this->header = $header;
|
||||||
|
$ts = $this->init();
|
||||||
|
if (!empty($ts['sql'])) {
|
||||||
|
// Based on code from db_escape_table(), but this can also contain a dot.
|
||||||
|
$field = preg_replace('/[^A-Za-z0-9_.]+/', '', $ts['sql']);
|
||||||
|
|
||||||
|
// orderBy() will ensure that only ASC/DESC values are accepted, so we
|
||||||
|
// don't need to sanitize that here.
|
||||||
|
$this->orderBy($field, $ts['sort']);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the table sort context.
|
||||||
|
*/
|
||||||
|
protected function init() {
|
||||||
|
$ts = $this->order();
|
||||||
|
$ts['sort'] = $this->getSort();
|
||||||
|
$ts['query'] = $this->getQueryParameters();
|
||||||
|
return $ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the current sort direction.
|
||||||
|
*
|
||||||
|
* @param $headers
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
* @return
|
||||||
|
* The current sort direction ("asc" or "desc").
|
||||||
|
*/
|
||||||
|
protected function getSort() {
|
||||||
|
return tablesort_get_sort($this->header);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose a URL query parameter array to append to table sorting requests.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A URL query parameter array that consists of all components of the current
|
||||||
|
* page request except for those pertaining to table sorting.
|
||||||
|
*
|
||||||
|
* @see tablesort_get_query_parameters()
|
||||||
|
*/
|
||||||
|
protected function getQueryParameters() {
|
||||||
|
return tablesort_get_query_parameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the current sort criterion.
|
||||||
|
*
|
||||||
|
* @param $headers
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
* @return
|
||||||
|
* An associative array describing the criterion, containing the keys:
|
||||||
|
* - "name": The localized title of the table column.
|
||||||
|
* - "sql": The name of the database field to sort on.
|
||||||
|
*/
|
||||||
|
protected function order() {
|
||||||
|
return tablesort_get_order($this->header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the table sort context.
|
||||||
|
*/
|
||||||
|
function tablesort_init($header) {
|
||||||
|
$ts = tablesort_get_order($header);
|
||||||
|
$ts['sort'] = tablesort_get_sort($header);
|
||||||
|
$ts['query'] = tablesort_get_query_parameters();
|
||||||
|
return $ts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a column header.
|
||||||
|
*
|
||||||
|
* If the cell in question is the column header for the current sort criterion,
|
||||||
|
* it gets special formatting. All possible sort criteria become links.
|
||||||
|
*
|
||||||
|
* @param $cell
|
||||||
|
* The cell to format.
|
||||||
|
* @param $header
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
* @param $ts
|
||||||
|
* The current table sort context as returned from tablesort_init().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A properly formatted cell, ready for _theme_table_cell().
|
||||||
|
*/
|
||||||
|
function tablesort_header($cell, $header, $ts) {
|
||||||
|
// Special formatting for the currently sorted column header.
|
||||||
|
if (is_array($cell) && isset($cell['field'])) {
|
||||||
|
$title = t('sort by @s', array('@s' => $cell['data']));
|
||||||
|
if ($cell['data'] == $ts['name']) {
|
||||||
|
$ts['sort'] = (($ts['sort'] == 'asc') ? 'desc' : 'asc');
|
||||||
|
$cell['class'][] = 'active';
|
||||||
|
$image = theme('tablesort_indicator', array('style' => $ts['sort']));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the user clicks a different header, we want to sort ascending initially.
|
||||||
|
$ts['sort'] = 'asc';
|
||||||
|
$image = '';
|
||||||
|
}
|
||||||
|
$cell['data'] = l($cell['data'] . $image, $_GET['q'], array('attributes' => array('title' => $title), 'query' => array_merge($ts['query'], array('sort' => $ts['sort'], 'order' => $cell['data'])), 'html' => TRUE));
|
||||||
|
|
||||||
|
unset($cell['field'], $cell['sort']);
|
||||||
|
}
|
||||||
|
return $cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a table cell.
|
||||||
|
*
|
||||||
|
* Adds a class attribute to all cells in the currently active column.
|
||||||
|
*
|
||||||
|
* @param $cell
|
||||||
|
* The cell to format.
|
||||||
|
* @param $header
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
* @param $ts
|
||||||
|
* The current table sort context as returned from tablesort_init().
|
||||||
|
* @param $i
|
||||||
|
* The index of the cell's table column.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A properly formatted cell, ready for _theme_table_cell().
|
||||||
|
*/
|
||||||
|
function tablesort_cell($cell, $header, $ts, $i) {
|
||||||
|
if (isset($header[$i]['data']) && $header[$i]['data'] == $ts['name'] && !empty($header[$i]['field'])) {
|
||||||
|
if (is_array($cell)) {
|
||||||
|
$cell['class'][] = 'active';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$cell = array('data' => $cell, 'class' => array('active'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $cell;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes a URL query parameter array for table sorting links.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A URL query parameter array that consists of all components of the current
|
||||||
|
* page request except for those pertaining to table sorting.
|
||||||
|
*/
|
||||||
|
function tablesort_get_query_parameters() {
|
||||||
|
return drupal_get_query_parameters($_GET, array('q', 'sort', 'order'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the current sort criterion.
|
||||||
|
*
|
||||||
|
* @param $headers
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array describing the criterion, containing the keys:
|
||||||
|
* - "name": The localized title of the table column.
|
||||||
|
* - "sql": The name of the database field to sort on.
|
||||||
|
*/
|
||||||
|
function tablesort_get_order($headers) {
|
||||||
|
$order = isset($_GET['order']) ? $_GET['order'] : '';
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (is_array($header)) {
|
||||||
|
if (isset($header['data']) && $order == $header['data']) {
|
||||||
|
$default = $header;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($default) && isset($header['sort']) && ($header['sort'] == 'asc' || $header['sort'] == 'desc')) {
|
||||||
|
$default = $header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($default)) {
|
||||||
|
$default = reset($headers);
|
||||||
|
if (!is_array($default)) {
|
||||||
|
$default = array('data' => $default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$default += array('data' => NULL, 'field' => NULL);
|
||||||
|
return array('name' => $default['data'], 'sql' => $default['field']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the current sort direction.
|
||||||
|
*
|
||||||
|
* @param $headers
|
||||||
|
* An array of column headers in the format described in theme_table().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The current sort direction ("asc" or "desc").
|
||||||
|
*/
|
||||||
|
function tablesort_get_sort($headers) {
|
||||||
|
if (isset($_GET['sort'])) {
|
||||||
|
return (strtolower($_GET['sort']) == 'desc') ? 'desc' : 'asc';
|
||||||
|
}
|
||||||
|
// The user has not specified a sort. Use the default for the currently sorted
|
||||||
|
// header if specified; otherwise use "asc".
|
||||||
|
else {
|
||||||
|
// Find out which header is currently being sorted.
|
||||||
|
$ts = tablesort_get_order($headers);
|
||||||
|
foreach ($headers as $header) {
|
||||||
|
if (is_array($header) && isset($header['data']) && $header['data'] == $ts['name'] && isset($header['sort'])) {
|
||||||
|
return $header['sort'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'asc';
|
||||||
|
}
|
3014
includes/theme.inc
Normal file
3014
includes/theme.inc
Normal file
File diff suppressed because it is too large
Load diff
211
includes/theme.maintenance.inc
Normal file
211
includes/theme.maintenance.inc
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Theming for maintenance pages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the theming system for maintenance page.
|
||||||
|
*
|
||||||
|
* Used for site installs, updates and when the site is in maintenance mode.
|
||||||
|
* It also applies when the database is unavailable or bootstrap was not
|
||||||
|
* complete. Seven is always used for the initial install and update
|
||||||
|
* operations. In other cases, Bartik is used, but this can be overridden by
|
||||||
|
* setting a "maintenance_theme" key in the $conf variable in settings.php.
|
||||||
|
*/
|
||||||
|
function _drupal_maintenance_theme() {
|
||||||
|
global $theme, $theme_key, $conf;
|
||||||
|
|
||||||
|
// If $theme is already set, assume the others are set too, and do nothing.
|
||||||
|
if (isset($theme)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
|
||||||
|
require_once DRUPAL_ROOT . '/includes/theme.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/common.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/unicode.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/file.inc';
|
||||||
|
require_once DRUPAL_ROOT . '/includes/module.inc';
|
||||||
|
unicode_check();
|
||||||
|
|
||||||
|
// Install and update pages are treated differently to prevent theming overrides.
|
||||||
|
if (defined('MAINTENANCE_MODE') && (MAINTENANCE_MODE == 'install' || MAINTENANCE_MODE == 'update')) {
|
||||||
|
$custom_theme = (isset($conf['maintenance_theme']) ? $conf['maintenance_theme'] : 'seven');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// The bootstrap was not complete. So we are operating in a crippled
|
||||||
|
// environment, we need to bootstrap just enough to allow hook invocations
|
||||||
|
// to work. See _drupal_log_error().
|
||||||
|
if (!class_exists('Database', FALSE)) {
|
||||||
|
require_once DRUPAL_ROOT . '/includes/database/database.inc';
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use the default theme as the maintenance theme. If a default theme
|
||||||
|
// isn't specified in the database or in settings.php, we use Bartik.
|
||||||
|
$custom_theme = variable_get('maintenance_theme', variable_get('theme_default', 'bartik'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that system.module is loaded.
|
||||||
|
if (!function_exists('_system_rebuild_theme_data')) {
|
||||||
|
$module_list['system']['filename'] = 'modules/system/system.module';
|
||||||
|
module_list(TRUE, FALSE, FALSE, $module_list);
|
||||||
|
drupal_load('module', 'system');
|
||||||
|
}
|
||||||
|
|
||||||
|
$themes = list_themes();
|
||||||
|
|
||||||
|
// list_themes() triggers a drupal_alter() in maintenance mode, but we can't
|
||||||
|
// let themes alter the .info data until we know a theme's base themes. So
|
||||||
|
// don't set global $theme until after list_themes() builds its cache.
|
||||||
|
$theme = $custom_theme;
|
||||||
|
|
||||||
|
// Store the identifier for retrieving theme settings with.
|
||||||
|
$theme_key = $theme;
|
||||||
|
|
||||||
|
// Find all our ancestor themes and put them in an array.
|
||||||
|
$base_theme = array();
|
||||||
|
$ancestor = $theme;
|
||||||
|
while ($ancestor && isset($themes[$ancestor]->base_theme)) {
|
||||||
|
$base_theme[] = $new_base_theme = $themes[$themes[$ancestor]->base_theme];
|
||||||
|
$ancestor = $themes[$ancestor]->base_theme;
|
||||||
|
}
|
||||||
|
_drupal_theme_initialize($themes[$theme], array_reverse($base_theme), '_theme_load_offline_registry');
|
||||||
|
|
||||||
|
// These are usually added from system_init() -except maintenance.css.
|
||||||
|
// When the database is inactive it's not called so we add it here.
|
||||||
|
$path = drupal_get_path('module', 'system');
|
||||||
|
drupal_add_css($path . '/system.base.css');
|
||||||
|
drupal_add_css($path . '/system.admin.css');
|
||||||
|
drupal_add_css($path . '/system.menus.css');
|
||||||
|
drupal_add_css($path . '/system.messages.css');
|
||||||
|
drupal_add_css($path . '/system.theme.css');
|
||||||
|
drupal_add_css($path . '/system.maintenance.css');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the registry when the site needs to bypass any database calls.
|
||||||
|
*/
|
||||||
|
function _theme_load_offline_registry($theme, $base_theme = NULL, $theme_engine = NULL) {
|
||||||
|
return _theme_build_registry($theme, $base_theme, $theme_engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for a list of maintenance tasks to perform.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - items: An associative array of maintenance tasks.
|
||||||
|
* - active: The key for the currently active maintenance task.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_task_list($variables) {
|
||||||
|
$items = $variables['items'];
|
||||||
|
$active = $variables['active'];
|
||||||
|
|
||||||
|
$done = isset($items[$active]) || $active == NULL;
|
||||||
|
$output = '<h2 class="element-invisible">Installation tasks</h2>';
|
||||||
|
$output .= '<ol class="task-list">';
|
||||||
|
|
||||||
|
foreach ($items as $k => $item) {
|
||||||
|
if ($active == $k) {
|
||||||
|
$class = 'active';
|
||||||
|
$status = '(' . t('active') . ')';
|
||||||
|
$done = FALSE;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$class = $done ? 'done' : '';
|
||||||
|
$status = $done ? '(' . t('done') . ')' : '';
|
||||||
|
}
|
||||||
|
$output .= '<li';
|
||||||
|
$output .= ($class ? ' class="' . $class . '"' : '') . '>';
|
||||||
|
$output .= $item;
|
||||||
|
$output .= ($status ? '<span class="element-invisible">' . $status . '</span>' : '');
|
||||||
|
$output .= '</li>';
|
||||||
|
}
|
||||||
|
$output .= '</ol>';
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the installation page.
|
||||||
|
*
|
||||||
|
* Note: this function is not themeable.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - content: The page content to show.
|
||||||
|
*/
|
||||||
|
function theme_install_page($variables) {
|
||||||
|
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
return theme('maintenance_page', $variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for the update page.
|
||||||
|
*
|
||||||
|
* Note: this function is not themeable.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - content: The page content to show.
|
||||||
|
* - show_messages: Whether to output status and error messages.
|
||||||
|
* FALSE can be useful to postpone the messages to a subsequent page.
|
||||||
|
*/
|
||||||
|
function theme_update_page($variables) {
|
||||||
|
drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
|
||||||
|
return theme('maintenance_page', $variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for a results report of an operation run by authorize.php.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - messages: An array of result messages.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_authorize_report($variables) {
|
||||||
|
$messages = $variables['messages'];
|
||||||
|
$output = '';
|
||||||
|
if (!empty($messages)) {
|
||||||
|
$output .= '<div id="authorize-results">';
|
||||||
|
foreach ($messages as $heading => $logs) {
|
||||||
|
$items = array();
|
||||||
|
foreach ($logs as $number => $log_message) {
|
||||||
|
if ($number === '#abort') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items[] = theme('authorize_message', array('message' => $log_message['message'], 'success' => $log_message['success']));
|
||||||
|
}
|
||||||
|
$output .= theme('item_list', array('items' => $items, 'title' => $heading));
|
||||||
|
}
|
||||||
|
$output .= '</div>';
|
||||||
|
}
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTML for a single log message from the authorize.php batch operation.
|
||||||
|
*
|
||||||
|
* @param $variables
|
||||||
|
* An associative array containing:
|
||||||
|
* - message: The log message.
|
||||||
|
* - success: A boolean indicating failure or success.
|
||||||
|
*
|
||||||
|
* @ingroup themeable
|
||||||
|
*/
|
||||||
|
function theme_authorize_message($variables) {
|
||||||
|
$message = $variables['message'];
|
||||||
|
$success = $variables['success'];
|
||||||
|
if ($success) {
|
||||||
|
$item = array('data' => $message, 'class' => array('success'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$item = array('data' => '<strong>' . $message . '</strong>', 'class' => array('failure'));
|
||||||
|
}
|
||||||
|
return $item;
|
||||||
|
}
|
264
includes/token.inc
Normal file
264
includes/token.inc
Normal file
|
@ -0,0 +1,264 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Drupal placeholder/token replacement system.
|
||||||
|
*
|
||||||
|
* API functions for replacing placeholders in text with meaningful values.
|
||||||
|
*
|
||||||
|
* For example: When configuring automated emails, an administrator enters
|
||||||
|
* standard text for the email. Variables like the title of a node and the date
|
||||||
|
* the email was sent can be entered as placeholders like [node:title] and
|
||||||
|
* [date:short]. When a Drupal module prepares to send the email, it can call
|
||||||
|
* the token_replace() function, passing in the text. The token system will
|
||||||
|
* scan the text for placeholder tokens, give other modules an opportunity to
|
||||||
|
* replace them with meaningful text, then return the final product to the
|
||||||
|
* original module.
|
||||||
|
*
|
||||||
|
* Tokens follow the form: [$type:$name], where $type is a general class of
|
||||||
|
* tokens like 'node', 'user', or 'comment' and $name is the name of a given
|
||||||
|
* placeholder. For example, [node:title] or [node:created:since].
|
||||||
|
*
|
||||||
|
* In addition to raw text containing placeholders, modules may pass in an array
|
||||||
|
* of objects to be used when performing the replacement. The objects should be
|
||||||
|
* keyed by the token type they correspond to. For example:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* // Load a node and a user, then replace tokens in the text.
|
||||||
|
* $text = 'On [date:short], [user:name] read [node:title].';
|
||||||
|
* $node = node_load(1);
|
||||||
|
* $user = user_load(1);
|
||||||
|
*
|
||||||
|
* // [date:...] tokens use the current date automatically.
|
||||||
|
* $data = array('node' => $node, 'user' => $user);
|
||||||
|
* return token_replace($text, $data);
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Some tokens may be chained in the form of [$type:$pointer:$name], where $type
|
||||||
|
* is a normal token type, $pointer is a reference to another token type, and
|
||||||
|
* $name is the name of a given placeholder. For example, [node:author:mail]. In
|
||||||
|
* that example, 'author' is a pointer to the 'user' account that created the
|
||||||
|
* node, and 'mail' is a placeholder available for any 'user'.
|
||||||
|
*
|
||||||
|
* @see token_replace()
|
||||||
|
* @see hook_tokens()
|
||||||
|
* @see hook_token_info()
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all tokens in a given string with appropriate values.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* A string potentially containing replaceable tokens.
|
||||||
|
* @param $data
|
||||||
|
* (optional) An array of keyed objects. For simple replacement scenarios
|
||||||
|
* 'node', 'user', and others are common keys, with an accompanying node or
|
||||||
|
* user object being the value. Some token types, like 'site', do not require
|
||||||
|
* any explicit information from $data and can be replaced even if it is
|
||||||
|
* empty.
|
||||||
|
* @param $options
|
||||||
|
* (optional) A keyed array of settings and flags to control the token
|
||||||
|
* replacement process. Supported options are:
|
||||||
|
* - language: A language object to be used when generating locale-sensitive
|
||||||
|
* tokens.
|
||||||
|
* - callback: A callback function that will be used to post-process the array
|
||||||
|
* of token replacements after they are generated. For example, a module
|
||||||
|
* using tokens in a text-only email might provide a callback to strip HTML
|
||||||
|
* entities from token values before they are inserted into the final text.
|
||||||
|
* - clear: A boolean flag indicating that tokens should be removed from the
|
||||||
|
* final text if no replacement value can be generated.
|
||||||
|
* - sanitize: A boolean flag indicating that tokens should be sanitized for
|
||||||
|
* display to a web browser. Defaults to TRUE. Developers who set this
|
||||||
|
* option to FALSE assume responsibility for running filter_xss(),
|
||||||
|
* check_plain() or other appropriate scrubbing functions before displaying
|
||||||
|
* data to users.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Text with tokens replaced.
|
||||||
|
*/
|
||||||
|
function token_replace($text, array $data = array(), array $options = array()) {
|
||||||
|
$text_tokens = token_scan($text);
|
||||||
|
if (empty($text_tokens)) {
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
|
||||||
|
$replacements = array();
|
||||||
|
foreach ($text_tokens as $type => $tokens) {
|
||||||
|
$replacements += token_generate($type, $tokens, $data, $options);
|
||||||
|
if (!empty($options['clear'])) {
|
||||||
|
$replacements += array_fill_keys($tokens, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optionally alter the list of replacement values.
|
||||||
|
if (!empty($options['callback']) && function_exists($options['callback'])) {
|
||||||
|
$function = $options['callback'];
|
||||||
|
$function($replacements, $data, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tokens = array_keys($replacements);
|
||||||
|
$values = array_values($replacements);
|
||||||
|
|
||||||
|
return str_replace($tokens, $values, $text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a list of all token-like patterns that appear in the text.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The text to be scanned for possible tokens.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array of discovered tokens, grouped by type.
|
||||||
|
*/
|
||||||
|
function token_scan($text) {
|
||||||
|
// Matches tokens with the following pattern: [$type:$name]
|
||||||
|
// $type and $name may not contain [ ] characters.
|
||||||
|
// $type may not contain : or whitespace characters, but $name may.
|
||||||
|
preg_match_all('/
|
||||||
|
\[ # [ - pattern start
|
||||||
|
([^\s\[\]:]*) # match $type not containing whitespace : [ or ]
|
||||||
|
: # : - separator
|
||||||
|
([^\[\]]*) # match $name not containing [ or ]
|
||||||
|
\] # ] - pattern end
|
||||||
|
/x', $text, $matches);
|
||||||
|
|
||||||
|
$types = $matches[1];
|
||||||
|
$tokens = $matches[2];
|
||||||
|
|
||||||
|
// Iterate through the matches, building an associative array containing
|
||||||
|
// $tokens grouped by $types, pointing to the version of the token found in
|
||||||
|
// the source text. For example, $results['node']['title'] = '[node:title]';
|
||||||
|
$results = array();
|
||||||
|
for ($i = 0; $i < count($tokens); $i++) {
|
||||||
|
$results[$types[$i]][$tokens[$i]] = $matches[0][$i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates replacement values for a list of tokens.
|
||||||
|
*
|
||||||
|
* @param $type
|
||||||
|
* The type of token being replaced. 'node', 'user', and 'date' are common.
|
||||||
|
* @param $tokens
|
||||||
|
* An array of tokens to be replaced, keyed by the literal text of the token
|
||||||
|
* as it appeared in the source text.
|
||||||
|
* @param $data
|
||||||
|
* (optional) An array of keyed objects. For simple replacement scenarios
|
||||||
|
* 'node', 'user', and others are common keys, with an accompanying node or
|
||||||
|
* user object being the value. Some token types, like 'site', do not require
|
||||||
|
* any explicit information from $data and can be replaced even if it is
|
||||||
|
* empty.
|
||||||
|
* @param $options
|
||||||
|
* (optional) A keyed array of settings and flags to control the token
|
||||||
|
* replacement process. Supported options are:
|
||||||
|
* - language: A language object to be used when generating locale-sensitive
|
||||||
|
* tokens.
|
||||||
|
* - callback: A callback function that will be used to post-process the
|
||||||
|
* array of token replacements after they are generated. Can be used when
|
||||||
|
* modules require special formatting of token text, for example URL
|
||||||
|
* encoding or truncation to a specific length.
|
||||||
|
* - sanitize: A boolean flag indicating that tokens should be sanitized for
|
||||||
|
* display to a web browser. Developers who set this option to FALSE assume
|
||||||
|
* responsibility for running filter_xss(), check_plain() or other
|
||||||
|
* appropriate scrubbing functions before displaying data to users.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array of replacement values, keyed by the original 'raw'
|
||||||
|
* tokens that were found in the source text. For example:
|
||||||
|
* $results['[node:title]'] = 'My new node';
|
||||||
|
*
|
||||||
|
* @see hook_tokens()
|
||||||
|
* @see hook_tokens_alter()
|
||||||
|
*/
|
||||||
|
function token_generate($type, array $tokens, array $data = array(), array $options = array()) {
|
||||||
|
$options += array('sanitize' => TRUE);
|
||||||
|
$replacements = module_invoke_all('tokens', $type, $tokens, $data, $options);
|
||||||
|
|
||||||
|
// Allow other modules to alter the replacements.
|
||||||
|
$context = array(
|
||||||
|
'type' => $type,
|
||||||
|
'tokens' => $tokens,
|
||||||
|
'data' => $data,
|
||||||
|
'options' => $options,
|
||||||
|
);
|
||||||
|
drupal_alter('tokens', $replacements, $context);
|
||||||
|
|
||||||
|
return $replacements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of tokens that begin with a specific prefix.
|
||||||
|
*
|
||||||
|
* Used to extract a group of 'chained' tokens (such as [node:author:name])
|
||||||
|
* from the full list of tokens found in text. For example:
|
||||||
|
* @code
|
||||||
|
* $data = array(
|
||||||
|
* 'author:name' => '[node:author:name]',
|
||||||
|
* 'title' => '[node:title]',
|
||||||
|
* 'created' => '[node:created]',
|
||||||
|
* );
|
||||||
|
* $results = token_find_with_prefix($data, 'author');
|
||||||
|
* $results == array('name' => '[node:author:name]');
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @param $tokens
|
||||||
|
* A keyed array of tokens, and their original raw form in the source text.
|
||||||
|
* @param $prefix
|
||||||
|
* A textual string to be matched at the beginning of the token.
|
||||||
|
* @param $delimiter
|
||||||
|
* An optional string containing the character that separates the prefix from
|
||||||
|
* the rest of the token. Defaults to ':'.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array of discovered tokens, with the prefix and delimiter
|
||||||
|
* stripped from the key.
|
||||||
|
*/
|
||||||
|
function token_find_with_prefix(array $tokens, $prefix, $delimiter = ':') {
|
||||||
|
$results = array();
|
||||||
|
foreach ($tokens as $token => $raw) {
|
||||||
|
$parts = explode($delimiter, $token, 2);
|
||||||
|
if (count($parts) == 2 && $parts[0] == $prefix) {
|
||||||
|
$results[$parts[1]] = $raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns metadata describing supported tokens.
|
||||||
|
*
|
||||||
|
* The metadata array contains token type, name, and description data as well
|
||||||
|
* as an optional pointer indicating that the token chains to another set of
|
||||||
|
* tokens.
|
||||||
|
*
|
||||||
|
* For example:
|
||||||
|
* @code
|
||||||
|
* $data['types']['node'] = array(
|
||||||
|
* 'name' => t('Nodes'),
|
||||||
|
* 'description' => t('Tokens related to node objects.'),
|
||||||
|
* );
|
||||||
|
* $data['tokens']['node']['title'] = array(
|
||||||
|
* 'name' => t('Title'),
|
||||||
|
* 'description' => t('The title of the current node.'),
|
||||||
|
* );
|
||||||
|
* $data['tokens']['node']['author'] = array(
|
||||||
|
* 'name' => t('Author'),
|
||||||
|
* 'description' => t('The author of the current node.'),
|
||||||
|
* 'type' => 'user',
|
||||||
|
* );
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An associative array of token information, grouped by token type.
|
||||||
|
*/
|
||||||
|
function token_info() {
|
||||||
|
$data = &drupal_static(__FUNCTION__);
|
||||||
|
if (!isset($data)) {
|
||||||
|
$data = module_invoke_all('token_info');
|
||||||
|
drupal_alter('token_info', $data);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
265
includes/unicode.entities.inc
Normal file
265
includes/unicode.entities.inc
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* (X)HTML entities, as defined in HTML 4.01.
|
||||||
|
*
|
||||||
|
* @see http://www.w3.org/TR/html401/sgml/entities.html
|
||||||
|
*/
|
||||||
|
|
||||||
|
$html_entities = array(
|
||||||
|
'Á' => 'Á',
|
||||||
|
'á' => 'á',
|
||||||
|
'Â' => 'Â',
|
||||||
|
'â' => 'â',
|
||||||
|
'´' => '´',
|
||||||
|
'Æ' => 'Æ',
|
||||||
|
'æ' => 'æ',
|
||||||
|
'À' => 'À',
|
||||||
|
'à' => 'à',
|
||||||
|
'ℵ' => 'ℵ',
|
||||||
|
'Α' => 'Α',
|
||||||
|
'α' => 'α',
|
||||||
|
'&' => '&',
|
||||||
|
'∧' => '∧',
|
||||||
|
'∠' => '∠',
|
||||||
|
'Å' => 'Å',
|
||||||
|
'å' => 'å',
|
||||||
|
'≈' => '≈',
|
||||||
|
'Ã' => 'Ã',
|
||||||
|
'ã' => 'ã',
|
||||||
|
'Ä' => 'Ä',
|
||||||
|
'ä' => 'ä',
|
||||||
|
'„' => '„',
|
||||||
|
'Β' => 'Β',
|
||||||
|
'β' => 'β',
|
||||||
|
'¦' => '¦',
|
||||||
|
'•' => '•',
|
||||||
|
'∩' => '∩',
|
||||||
|
'Ç' => 'Ç',
|
||||||
|
'ç' => 'ç',
|
||||||
|
'¸' => '¸',
|
||||||
|
'¢' => '¢',
|
||||||
|
'Χ' => 'Χ',
|
||||||
|
'χ' => 'χ',
|
||||||
|
'ˆ' => 'ˆ',
|
||||||
|
'♣' => '♣',
|
||||||
|
'≅' => '≅',
|
||||||
|
'©' => '©',
|
||||||
|
'↵' => '↵',
|
||||||
|
'∪' => '∪',
|
||||||
|
'¤' => '¤',
|
||||||
|
'†' => '†',
|
||||||
|
'‡' => '‡',
|
||||||
|
'↓' => '↓',
|
||||||
|
'⇓' => '⇓',
|
||||||
|
'°' => '°',
|
||||||
|
'Δ' => 'Δ',
|
||||||
|
'δ' => 'δ',
|
||||||
|
'♦' => '♦',
|
||||||
|
'÷' => '÷',
|
||||||
|
'É' => 'É',
|
||||||
|
'é' => 'é',
|
||||||
|
'Ê' => 'Ê',
|
||||||
|
'ê' => 'ê',
|
||||||
|
'È' => 'È',
|
||||||
|
'è' => 'è',
|
||||||
|
'∅' => '∅',
|
||||||
|
' ' => ' ',
|
||||||
|
' ' => ' ',
|
||||||
|
'Ε' => 'Ε',
|
||||||
|
'ε' => 'ε',
|
||||||
|
'≡' => '≡',
|
||||||
|
'Η' => 'Η',
|
||||||
|
'η' => 'η',
|
||||||
|
'Ð' => 'Ð',
|
||||||
|
'ð' => 'ð',
|
||||||
|
'Ë' => 'Ë',
|
||||||
|
'ë' => 'ë',
|
||||||
|
'€' => '€',
|
||||||
|
'∃' => '∃',
|
||||||
|
'ƒ' => 'ƒ',
|
||||||
|
'∀' => '∀',
|
||||||
|
'½' => '½',
|
||||||
|
'¼' => '¼',
|
||||||
|
'¾' => '¾',
|
||||||
|
'⁄' => '⁄',
|
||||||
|
'Γ' => 'Γ',
|
||||||
|
'γ' => 'γ',
|
||||||
|
'≥' => '≥',
|
||||||
|
'↔' => '↔',
|
||||||
|
'⇔' => '⇔',
|
||||||
|
'♥' => '♥',
|
||||||
|
'…' => '…',
|
||||||
|
'Í' => 'Í',
|
||||||
|
'í' => 'í',
|
||||||
|
'Î' => 'Î',
|
||||||
|
'î' => 'î',
|
||||||
|
'¡' => '¡',
|
||||||
|
'Ì' => 'Ì',
|
||||||
|
'ì' => 'ì',
|
||||||
|
'ℑ' => 'ℑ',
|
||||||
|
'∞' => '∞',
|
||||||
|
'∫' => '∫',
|
||||||
|
'Ι' => 'Ι',
|
||||||
|
'ι' => 'ι',
|
||||||
|
'¿' => '¿',
|
||||||
|
'∈' => '∈',
|
||||||
|
'Ï' => 'Ï',
|
||||||
|
'ï' => 'ï',
|
||||||
|
'Κ' => 'Κ',
|
||||||
|
'κ' => 'κ',
|
||||||
|
'Λ' => 'Λ',
|
||||||
|
'λ' => 'λ',
|
||||||
|
'⟨' => '〈',
|
||||||
|
'«' => '«',
|
||||||
|
'←' => '←',
|
||||||
|
'⇐' => '⇐',
|
||||||
|
'⌈' => '⌈',
|
||||||
|
'“' => '“',
|
||||||
|
'≤' => '≤',
|
||||||
|
'⌊' => '⌊',
|
||||||
|
'∗' => '∗',
|
||||||
|
'◊' => '◊',
|
||||||
|
'‎' => '',
|
||||||
|
'‹' => '‹',
|
||||||
|
'‘' => '‘',
|
||||||
|
'¯' => '¯',
|
||||||
|
'—' => '—',
|
||||||
|
'µ' => 'µ',
|
||||||
|
'·' => '·',
|
||||||
|
'−' => '−',
|
||||||
|
'Μ' => 'Μ',
|
||||||
|
'μ' => 'μ',
|
||||||
|
'∇' => '∇',
|
||||||
|
' ' => ' ',
|
||||||
|
'–' => '–',
|
||||||
|
'≠' => '≠',
|
||||||
|
'∋' => '∋',
|
||||||
|
'¬' => '¬',
|
||||||
|
'∉' => '∉',
|
||||||
|
'⊄' => '⊄',
|
||||||
|
'Ñ' => 'Ñ',
|
||||||
|
'ñ' => 'ñ',
|
||||||
|
'Ν' => 'Ν',
|
||||||
|
'ν' => 'ν',
|
||||||
|
'Ó' => 'Ó',
|
||||||
|
'ó' => 'ó',
|
||||||
|
'Ô' => 'Ô',
|
||||||
|
'ô' => 'ô',
|
||||||
|
'Œ' => 'Œ',
|
||||||
|
'œ' => 'œ',
|
||||||
|
'Ò' => 'Ò',
|
||||||
|
'ò' => 'ò',
|
||||||
|
'‾' => '‾',
|
||||||
|
'Ω' => 'Ω',
|
||||||
|
'ω' => 'ω',
|
||||||
|
'Ο' => 'Ο',
|
||||||
|
'ο' => 'ο',
|
||||||
|
'⊕' => '⊕',
|
||||||
|
'∨' => '∨',
|
||||||
|
'ª' => 'ª',
|
||||||
|
'º' => 'º',
|
||||||
|
'Ø' => 'Ø',
|
||||||
|
'ø' => 'ø',
|
||||||
|
'Õ' => 'Õ',
|
||||||
|
'õ' => 'õ',
|
||||||
|
'⊗' => '⊗',
|
||||||
|
'Ö' => 'Ö',
|
||||||
|
'ö' => 'ö',
|
||||||
|
'¶' => '¶',
|
||||||
|
'∂' => '∂',
|
||||||
|
'‰' => '‰',
|
||||||
|
'⊥' => '⊥',
|
||||||
|
'Φ' => 'Φ',
|
||||||
|
'φ' => 'φ',
|
||||||
|
'Π' => 'Π',
|
||||||
|
'π' => 'π',
|
||||||
|
'ϖ' => 'ϖ',
|
||||||
|
'±' => '±',
|
||||||
|
'£' => '£',
|
||||||
|
'′' => '′',
|
||||||
|
'″' => '″',
|
||||||
|
'∏' => '∏',
|
||||||
|
'∝' => '∝',
|
||||||
|
'Ψ' => 'Ψ',
|
||||||
|
'ψ' => 'ψ',
|
||||||
|
'√' => '√',
|
||||||
|
'⟩' => '〉',
|
||||||
|
'»' => '»',
|
||||||
|
'→' => '→',
|
||||||
|
'⇒' => '⇒',
|
||||||
|
'⌉' => '⌉',
|
||||||
|
'”' => '”',
|
||||||
|
'ℜ' => 'ℜ',
|
||||||
|
'®' => '®',
|
||||||
|
'⌋' => '⌋',
|
||||||
|
'Ρ' => 'Ρ',
|
||||||
|
'ρ' => 'ρ',
|
||||||
|
'‏' => '',
|
||||||
|
'›' => '›',
|
||||||
|
'’' => '’',
|
||||||
|
'‚' => '‚',
|
||||||
|
'Š' => 'Š',
|
||||||
|
'š' => 'š',
|
||||||
|
'⋅' => '⋅',
|
||||||
|
'§' => '§',
|
||||||
|
'­' => '',
|
||||||
|
'Σ' => 'Σ',
|
||||||
|
'σ' => 'σ',
|
||||||
|
'ς' => 'ς',
|
||||||
|
'∼' => '∼',
|
||||||
|
'♠' => '♠',
|
||||||
|
'⊂' => '⊂',
|
||||||
|
'⊆' => '⊆',
|
||||||
|
'∑' => '∑',
|
||||||
|
'¹' => '¹',
|
||||||
|
'²' => '²',
|
||||||
|
'³' => '³',
|
||||||
|
'⊃' => '⊃',
|
||||||
|
'⊇' => '⊇',
|
||||||
|
'ß' => 'ß',
|
||||||
|
'Τ' => 'Τ',
|
||||||
|
'τ' => 'τ',
|
||||||
|
'∴' => '∴',
|
||||||
|
'Θ' => 'Θ',
|
||||||
|
'θ' => 'θ',
|
||||||
|
'ϑ' => 'ϑ',
|
||||||
|
' ' => ' ',
|
||||||
|
'Þ' => 'Þ',
|
||||||
|
'þ' => 'þ',
|
||||||
|
'˜' => '˜',
|
||||||
|
'×' => '×',
|
||||||
|
'™' => '™',
|
||||||
|
'Ú' => 'Ú',
|
||||||
|
'ú' => 'ú',
|
||||||
|
'↑' => '↑',
|
||||||
|
'⇑' => '⇑',
|
||||||
|
'Û' => 'Û',
|
||||||
|
'û' => 'û',
|
||||||
|
'Ù' => 'Ù',
|
||||||
|
'ù' => 'ù',
|
||||||
|
'¨' => '¨',
|
||||||
|
'ϒ' => 'ϒ',
|
||||||
|
'Υ' => 'Υ',
|
||||||
|
'υ' => 'υ',
|
||||||
|
'Ü' => 'Ü',
|
||||||
|
'ü' => 'ü',
|
||||||
|
'℘' => '℘',
|
||||||
|
'Ξ' => 'Ξ',
|
||||||
|
'ξ' => 'ξ',
|
||||||
|
'Ý' => 'Ý',
|
||||||
|
'ý' => 'ý',
|
||||||
|
'¥' => '¥',
|
||||||
|
'ÿ' => 'ÿ',
|
||||||
|
'Ÿ' => 'Ÿ',
|
||||||
|
'Ζ' => 'Ζ',
|
||||||
|
'ζ' => 'ζ',
|
||||||
|
'‍' => '',
|
||||||
|
'‌' => '',
|
||||||
|
'>' => '>',
|
||||||
|
'<' => '<',
|
||||||
|
'"' => '"',
|
||||||
|
// Add apostrophe (XML).
|
||||||
|
''' => "'",
|
||||||
|
);
|
676
includes/unicode.inc
Normal file
676
includes/unicode.inc
Normal file
|
@ -0,0 +1,676 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides Unicode-related conversions and operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates an error during check for PHP unicode support.
|
||||||
|
*/
|
||||||
|
define('UNICODE_ERROR', -1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that standard PHP (emulated) unicode support is being used.
|
||||||
|
*/
|
||||||
|
define('UNICODE_SINGLEBYTE', 0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that full unicode support with the PHP mbstring extension is being
|
||||||
|
* used.
|
||||||
|
*/
|
||||||
|
define('UNICODE_MULTIBYTE', 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches Unicode characters that are word boundaries.
|
||||||
|
*
|
||||||
|
* Characters with the following General_category (gc) property values are used
|
||||||
|
* as word boundaries. While this does not fully conform to the Word Boundaries
|
||||||
|
* algorithm described in http://unicode.org/reports/tr29, as PCRE does not
|
||||||
|
* contain the Word_Break property table, this simpler algorithm has to do.
|
||||||
|
* - Cc, Cf, Cn, Co, Cs: Other.
|
||||||
|
* - Pc, Pd, Pe, Pf, Pi, Po, Ps: Punctuation.
|
||||||
|
* - Sc, Sk, Sm, So: Symbols.
|
||||||
|
* - Zl, Zp, Zs: Separators.
|
||||||
|
*
|
||||||
|
* Non-boundary characters include the following General_category (gc) property
|
||||||
|
* values:
|
||||||
|
* - Ll, Lm, Lo, Lt, Lu: Letters.
|
||||||
|
* - Mc, Me, Mn: Combining Marks.
|
||||||
|
* - Nd, Nl, No: Numbers.
|
||||||
|
*
|
||||||
|
* Note that the PCRE property matcher is not used because we wanted to be
|
||||||
|
* compatible with Unicode 5.2.0 regardless of the PCRE version used (and any
|
||||||
|
* bugs in PCRE property tables).
|
||||||
|
*
|
||||||
|
* @see http://unicode.org/glossary
|
||||||
|
*/
|
||||||
|
define('PREG_CLASS_UNICODE_WORD_BOUNDARY',
|
||||||
|
'\x{0}-\x{2F}\x{3A}-\x{40}\x{5B}-\x{60}\x{7B}-\x{A9}\x{AB}-\x{B1}\x{B4}' .
|
||||||
|
'\x{B6}-\x{B8}\x{BB}\x{BF}\x{D7}\x{F7}\x{2C2}-\x{2C5}\x{2D2}-\x{2DF}' .
|
||||||
|
'\x{2E5}-\x{2EB}\x{2ED}\x{2EF}-\x{2FF}\x{375}\x{37E}-\x{385}\x{387}\x{3F6}' .
|
||||||
|
'\x{482}\x{55A}-\x{55F}\x{589}-\x{58A}\x{5BE}\x{5C0}\x{5C3}\x{5C6}' .
|
||||||
|
'\x{5F3}-\x{60F}\x{61B}-\x{61F}\x{66A}-\x{66D}\x{6D4}\x{6DD}\x{6E9}' .
|
||||||
|
'\x{6FD}-\x{6FE}\x{700}-\x{70F}\x{7F6}-\x{7F9}\x{830}-\x{83E}' .
|
||||||
|
'\x{964}-\x{965}\x{970}\x{9F2}-\x{9F3}\x{9FA}-\x{9FB}\x{AF1}\x{B70}' .
|
||||||
|
'\x{BF3}-\x{BFA}\x{C7F}\x{CF1}-\x{CF2}\x{D79}\x{DF4}\x{E3F}\x{E4F}' .
|
||||||
|
'\x{E5A}-\x{E5B}\x{F01}-\x{F17}\x{F1A}-\x{F1F}\x{F34}\x{F36}\x{F38}' .
|
||||||
|
'\x{F3A}-\x{F3D}\x{F85}\x{FBE}-\x{FC5}\x{FC7}-\x{FD8}\x{104A}-\x{104F}' .
|
||||||
|
'\x{109E}-\x{109F}\x{10FB}\x{1360}-\x{1368}\x{1390}-\x{1399}\x{1400}' .
|
||||||
|
'\x{166D}-\x{166E}\x{1680}\x{169B}-\x{169C}\x{16EB}-\x{16ED}' .
|
||||||
|
'\x{1735}-\x{1736}\x{17B4}-\x{17B5}\x{17D4}-\x{17D6}\x{17D8}-\x{17DB}' .
|
||||||
|
'\x{1800}-\x{180A}\x{180E}\x{1940}-\x{1945}\x{19DE}-\x{19FF}' .
|
||||||
|
'\x{1A1E}-\x{1A1F}\x{1AA0}-\x{1AA6}\x{1AA8}-\x{1AAD}\x{1B5A}-\x{1B6A}' .
|
||||||
|
'\x{1B74}-\x{1B7C}\x{1C3B}-\x{1C3F}\x{1C7E}-\x{1C7F}\x{1CD3}\x{1FBD}' .
|
||||||
|
'\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}' .
|
||||||
|
'\x{1FFD}-\x{206F}\x{207A}-\x{207E}\x{208A}-\x{208E}\x{20A0}-\x{20B8}' .
|
||||||
|
'\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}' .
|
||||||
|
'\x{2116}-\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}' .
|
||||||
|
'\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}-\x{214D}\x{214F}' .
|
||||||
|
'\x{2190}-\x{244A}\x{249C}-\x{24E9}\x{2500}-\x{2775}\x{2794}-\x{2B59}' .
|
||||||
|
'\x{2CE5}-\x{2CEA}\x{2CF9}-\x{2CFC}\x{2CFE}-\x{2CFF}\x{2E00}-\x{2E2E}' .
|
||||||
|
'\x{2E30}-\x{3004}\x{3008}-\x{3020}\x{3030}\x{3036}-\x{3037}' .
|
||||||
|
'\x{303D}-\x{303F}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{3190}-\x{3191}' .
|
||||||
|
'\x{3196}-\x{319F}\x{31C0}-\x{31E3}\x{3200}-\x{321E}\x{322A}-\x{3250}' .
|
||||||
|
'\x{3260}-\x{327F}\x{328A}-\x{32B0}\x{32C0}-\x{33FF}\x{4DC0}-\x{4DFF}' .
|
||||||
|
'\x{A490}-\x{A4C6}\x{A4FE}-\x{A4FF}\x{A60D}-\x{A60F}\x{A673}\x{A67E}' .
|
||||||
|
'\x{A6F2}-\x{A716}\x{A720}-\x{A721}\x{A789}-\x{A78A}\x{A828}-\x{A82B}' .
|
||||||
|
'\x{A836}-\x{A839}\x{A874}-\x{A877}\x{A8CE}-\x{A8CF}\x{A8F8}-\x{A8FA}' .
|
||||||
|
'\x{A92E}-\x{A92F}\x{A95F}\x{A9C1}-\x{A9CD}\x{A9DE}-\x{A9DF}' .
|
||||||
|
'\x{AA5C}-\x{AA5F}\x{AA77}-\x{AA79}\x{AADE}-\x{AADF}\x{ABEB}' .
|
||||||
|
'\x{E000}-\x{F8FF}\x{FB29}\x{FD3E}-\x{FD3F}\x{FDFC}-\x{FDFD}' .
|
||||||
|
'\x{FE10}-\x{FE19}\x{FE30}-\x{FE6B}\x{FEFF}-\x{FF0F}\x{FF1A}-\x{FF20}' .
|
||||||
|
'\x{FF3B}-\x{FF40}\x{FF5B}-\x{FF65}\x{FFE0}-\x{FFFD}');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper around _unicode_check().
|
||||||
|
*/
|
||||||
|
function unicode_check() {
|
||||||
|
list($GLOBALS['multibyte']) = _unicode_check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform checks about Unicode support in PHP, and set the right settings if
|
||||||
|
* needed.
|
||||||
|
*
|
||||||
|
* Because Drupal needs to be able to handle text in various encodings, we do
|
||||||
|
* not support mbstring function overloading. HTTP input/output conversion must
|
||||||
|
* be disabled for similar reasons.
|
||||||
|
*
|
||||||
|
* @param $errors
|
||||||
|
* Whether to report any fatal errors with form_set_error().
|
||||||
|
*/
|
||||||
|
function _unicode_check() {
|
||||||
|
// Ensure translations don't break during installation.
|
||||||
|
$t = get_t();
|
||||||
|
|
||||||
|
// Check for mbstring extension
|
||||||
|
if (!function_exists('mb_strlen')) {
|
||||||
|
return array(UNICODE_SINGLEBYTE, $t('Operations on Unicode strings are emulated on a best-effort basis. Install the <a href="@url">PHP mbstring extension</a> for improved Unicode support.', array('@url' => 'http://www.php.net/mbstring')));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check mbstring configuration
|
||||||
|
if (ini_get('mbstring.func_overload') != 0) {
|
||||||
|
return array(UNICODE_ERROR, $t('Multibyte string function overloading in PHP is active and must be disabled. Check the php.ini <em>mbstring.func_overload</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
|
||||||
|
}
|
||||||
|
if (ini_get('mbstring.encoding_translation') != 0) {
|
||||||
|
return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.encoding_translation</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
|
||||||
|
}
|
||||||
|
// mbstring.http_input and mbstring.http_output are deprecated and empty by
|
||||||
|
// default in PHP 5.6.
|
||||||
|
if (version_compare(PHP_VERSION, '5.6.0') == -1) {
|
||||||
|
if (ini_get('mbstring.http_input') != 'pass') {
|
||||||
|
return array(UNICODE_ERROR, $t('Multibyte string input conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.http_input</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
|
||||||
|
}
|
||||||
|
if (ini_get('mbstring.http_output') != 'pass') {
|
||||||
|
return array(UNICODE_ERROR, $t('Multibyte string output conversion in PHP is active and must be disabled. Check the php.ini <em>mbstring.http_output</em> setting. Please refer to the <a href="@url">PHP mbstring documentation</a> for more information.', array('@url' => 'http://www.php.net/mbstring')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate configuration
|
||||||
|
mb_internal_encoding('utf-8');
|
||||||
|
mb_language('uni');
|
||||||
|
return array(UNICODE_MULTIBYTE, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns Unicode library status and errors.
|
||||||
|
*/
|
||||||
|
function unicode_requirements() {
|
||||||
|
// Ensure translations don't break during installation.
|
||||||
|
$t = get_t();
|
||||||
|
|
||||||
|
$libraries = array(
|
||||||
|
UNICODE_SINGLEBYTE => $t('Standard PHP'),
|
||||||
|
UNICODE_MULTIBYTE => $t('PHP Mbstring Extension'),
|
||||||
|
UNICODE_ERROR => $t('Error'),
|
||||||
|
);
|
||||||
|
$severities = array(
|
||||||
|
UNICODE_SINGLEBYTE => REQUIREMENT_WARNING,
|
||||||
|
UNICODE_MULTIBYTE => REQUIREMENT_OK,
|
||||||
|
UNICODE_ERROR => REQUIREMENT_ERROR,
|
||||||
|
);
|
||||||
|
list($library, $description) = _unicode_check();
|
||||||
|
|
||||||
|
$requirements['unicode'] = array(
|
||||||
|
'title' => $t('Unicode library'),
|
||||||
|
'value' => $libraries[$library],
|
||||||
|
);
|
||||||
|
if ($description) {
|
||||||
|
$requirements['unicode']['description'] = $description;
|
||||||
|
}
|
||||||
|
|
||||||
|
$requirements['unicode']['severity'] = $severities[$library];
|
||||||
|
|
||||||
|
return $requirements;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a new XML parser.
|
||||||
|
*
|
||||||
|
* This is a wrapper around xml_parser_create() which extracts the encoding
|
||||||
|
* from the XML data first and sets the output encoding to UTF-8. This function
|
||||||
|
* should be used instead of xml_parser_create(), because PHP 4's XML parser
|
||||||
|
* doesn't check the input encoding itself. "Starting from PHP 5, the input
|
||||||
|
* encoding is automatically detected, so that the encoding parameter specifies
|
||||||
|
* only the output encoding."
|
||||||
|
*
|
||||||
|
* This is also where unsupported encodings will be converted. Callers should
|
||||||
|
* take this into account: $data might have been changed after the call.
|
||||||
|
*
|
||||||
|
* @param $data
|
||||||
|
* The XML data which will be parsed later.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An XML parser object or FALSE on error.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_xml_parser_create(&$data) {
|
||||||
|
// Default XML encoding is UTF-8
|
||||||
|
$encoding = 'utf-8';
|
||||||
|
$bom = FALSE;
|
||||||
|
|
||||||
|
// Check for UTF-8 byte order mark (PHP5's XML parser doesn't handle it).
|
||||||
|
if (!strncmp($data, "\xEF\xBB\xBF", 3)) {
|
||||||
|
$bom = TRUE;
|
||||||
|
$data = substr($data, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for an encoding declaration in the XML prolog if no BOM was found.
|
||||||
|
if (!$bom && preg_match('/^<\?xml[^>]+encoding="(.+?)"/', $data, $match)) {
|
||||||
|
$encoding = $match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsupported encodings are converted here into UTF-8.
|
||||||
|
$php_supported = array('utf-8', 'iso-8859-1', 'us-ascii');
|
||||||
|
if (!in_array(strtolower($encoding), $php_supported)) {
|
||||||
|
$out = drupal_convert_to_utf8($data, $encoding);
|
||||||
|
if ($out !== FALSE) {
|
||||||
|
$encoding = 'utf-8';
|
||||||
|
$data = preg_replace('/^(<\?xml[^>]+encoding)="(.+?)"/', '\\1="utf-8"', $out);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
watchdog('php', 'Could not convert XML encoding %s to UTF-8.', array('%s' => $encoding), WATCHDOG_WARNING);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$xml_parser = xml_parser_create($encoding);
|
||||||
|
xml_parser_set_option($xml_parser, XML_OPTION_TARGET_ENCODING, 'utf-8');
|
||||||
|
return $xml_parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts data to UTF-8.
|
||||||
|
*
|
||||||
|
* Requires the iconv, GNU recode or mbstring PHP extension.
|
||||||
|
*
|
||||||
|
* @param $data
|
||||||
|
* The data to be converted.
|
||||||
|
* @param $encoding
|
||||||
|
* The encoding that the data is in.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Converted data or FALSE.
|
||||||
|
*/
|
||||||
|
function drupal_convert_to_utf8($data, $encoding) {
|
||||||
|
if (function_exists('iconv')) {
|
||||||
|
$out = @iconv($encoding, 'utf-8', $data);
|
||||||
|
}
|
||||||
|
elseif (function_exists('mb_convert_encoding')) {
|
||||||
|
$out = @mb_convert_encoding($data, 'utf-8', $encoding);
|
||||||
|
}
|
||||||
|
elseif (function_exists('recode_string')) {
|
||||||
|
$out = @recode_string($encoding . '..utf-8', $data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
watchdog('php', 'Unsupported encoding %s. Please install iconv, GNU recode or mbstring for PHP.', array('%s' => $encoding), WATCHDOG_ERROR);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a UTF-8-encoded string safely to a number of bytes.
|
||||||
|
*
|
||||||
|
* If the end position is in the middle of a UTF-8 sequence, it scans backwards
|
||||||
|
* until the beginning of the byte sequence.
|
||||||
|
*
|
||||||
|
* Use this function whenever you want to chop off a string at an unsure
|
||||||
|
* location. On the other hand, if you're sure that you're splitting on a
|
||||||
|
* character boundary (e.g. after using strpos() or similar), you can safely
|
||||||
|
* use substr() instead.
|
||||||
|
*
|
||||||
|
* @param $string
|
||||||
|
* The string to truncate.
|
||||||
|
* @param $len
|
||||||
|
* An upper limit on the returned string length.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The truncated string.
|
||||||
|
*/
|
||||||
|
function drupal_truncate_bytes($string, $len) {
|
||||||
|
if (strlen($string) <= $len) {
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
if ((ord($string[$len]) < 0x80) || (ord($string[$len]) >= 0xC0)) {
|
||||||
|
return substr($string, 0, $len);
|
||||||
|
}
|
||||||
|
// Scan backwards to beginning of the byte sequence.
|
||||||
|
while (--$len >= 0 && ord($string[$len]) >= 0x80 && ord($string[$len]) < 0xC0);
|
||||||
|
|
||||||
|
return substr($string, 0, $len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncates a UTF-8-encoded string safely to a number of characters.
|
||||||
|
*
|
||||||
|
* @param $string
|
||||||
|
* The string to truncate.
|
||||||
|
* @param $max_length
|
||||||
|
* An upper limit on the returned string length, including trailing ellipsis
|
||||||
|
* if $add_ellipsis is TRUE.
|
||||||
|
* @param $wordsafe
|
||||||
|
* If TRUE, attempt to truncate on a word boundary. Word boundaries are
|
||||||
|
* spaces, punctuation, and Unicode characters used as word boundaries in
|
||||||
|
* non-Latin languages; see PREG_CLASS_UNICODE_WORD_BOUNDARY for more
|
||||||
|
* information. If a word boundary cannot be found that would make the length
|
||||||
|
* of the returned string fall within length guidelines (see parameters
|
||||||
|
* $max_length and $min_wordsafe_length), word boundaries are ignored.
|
||||||
|
* @param $add_ellipsis
|
||||||
|
* If TRUE, add t('...') to the end of the truncated string (defaults to
|
||||||
|
* FALSE). The string length will still fall within $max_length.
|
||||||
|
* @param $min_wordsafe_length
|
||||||
|
* If $wordsafe is TRUE, the minimum acceptable length for truncation (before
|
||||||
|
* adding an ellipsis, if $add_ellipsis is TRUE). Has no effect if $wordsafe
|
||||||
|
* is FALSE. This can be used to prevent having a very short resulting string
|
||||||
|
* that will not be understandable. For instance, if you are truncating the
|
||||||
|
* string "See myverylongurlexample.com for more information" to a word-safe
|
||||||
|
* return length of 20, the only available word boundary within 20 characters
|
||||||
|
* is after the word "See", which wouldn't leave a very informative string. If
|
||||||
|
* you had set $min_wordsafe_length to 10, though, the function would realise
|
||||||
|
* that "See" alone is too short, and would then just truncate ignoring word
|
||||||
|
* boundaries, giving you "See myverylongurl..." (assuming you had set
|
||||||
|
* $add_ellipses to TRUE).
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The truncated string.
|
||||||
|
*/
|
||||||
|
function truncate_utf8($string, $max_length, $wordsafe = FALSE, $add_ellipsis = FALSE, $min_wordsafe_length = 1) {
|
||||||
|
$ellipsis = '';
|
||||||
|
$max_length = max($max_length, 0);
|
||||||
|
$min_wordsafe_length = max($min_wordsafe_length, 0);
|
||||||
|
|
||||||
|
if (drupal_strlen($string) <= $max_length) {
|
||||||
|
// No truncation needed, so don't add ellipsis, just return.
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($add_ellipsis) {
|
||||||
|
// Truncate ellipsis in case $max_length is small.
|
||||||
|
$ellipsis = drupal_substr(t('...'), 0, $max_length);
|
||||||
|
$max_length -= drupal_strlen($ellipsis);
|
||||||
|
$max_length = max($max_length, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($max_length <= $min_wordsafe_length) {
|
||||||
|
// Do not attempt word-safe if lengths are bad.
|
||||||
|
$wordsafe = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($wordsafe) {
|
||||||
|
$matches = array();
|
||||||
|
// Find the last word boundary, if there is one within $min_wordsafe_length
|
||||||
|
// to $max_length characters. preg_match() is always greedy, so it will
|
||||||
|
// find the longest string possible.
|
||||||
|
$found = preg_match('/^(.{' . $min_wordsafe_length . ',' . $max_length . '})[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . ']/u', $string, $matches);
|
||||||
|
if ($found) {
|
||||||
|
$string = $matches[1];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$string = drupal_substr($string, 0, $max_length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$string = drupal_substr($string, 0, $max_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($add_ellipsis) {
|
||||||
|
$string .= $ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes MIME/HTTP header values that contain incorrectly encoded characters.
|
||||||
|
*
|
||||||
|
* For example, mime_header_encode('tést.txt') returns "=?UTF-8?B?dMOpc3QudHh0?=".
|
||||||
|
*
|
||||||
|
* See http://www.rfc-editor.org/rfc/rfc2047.txt for more information.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Only encode strings that contain non-ASCII characters.
|
||||||
|
* - We progressively cut-off a chunk with truncate_utf8(). This is to ensure
|
||||||
|
* each chunk starts and ends on a character boundary.
|
||||||
|
* - Using \n as the chunk separator may cause problems on some systems and may
|
||||||
|
* have to be changed to \r\n or \r.
|
||||||
|
*
|
||||||
|
* @param $string
|
||||||
|
* The header to encode.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The mime-encoded header.
|
||||||
|
*
|
||||||
|
* @see mime_header_decode()
|
||||||
|
*/
|
||||||
|
function mime_header_encode($string) {
|
||||||
|
if (preg_match('/[^\x20-\x7E]/', $string)) {
|
||||||
|
$chunk_size = 47; // floor((75 - strlen("=?UTF-8?B??=")) * 0.75);
|
||||||
|
$len = strlen($string);
|
||||||
|
$output = '';
|
||||||
|
while ($len > 0) {
|
||||||
|
$chunk = drupal_truncate_bytes($string, $chunk_size);
|
||||||
|
$output .= ' =?UTF-8?B?' . base64_encode($chunk) . "?=\n";
|
||||||
|
$c = strlen($chunk);
|
||||||
|
$string = substr($string, $c);
|
||||||
|
$len -= $c;
|
||||||
|
}
|
||||||
|
return trim($output);
|
||||||
|
}
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes MIME/HTTP encoded header values.
|
||||||
|
*
|
||||||
|
* @param $header
|
||||||
|
* The header to decode.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The mime-decoded header.
|
||||||
|
*
|
||||||
|
* @see mime_header_encode()
|
||||||
|
*/
|
||||||
|
function mime_header_decode($header) {
|
||||||
|
// First step: encoded chunks followed by other encoded chunks (need to collapse whitespace)
|
||||||
|
$header = preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=\s+(?==\?)/', '_mime_header_decode', $header);
|
||||||
|
// Second step: remaining chunks (do not collapse whitespace)
|
||||||
|
return preg_replace_callback('/=\?([^?]+)\?(Q|B)\?([^?]+|\?(?!=))\?=/', '_mime_header_decode', $header);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes encoded header data passed from mime_header_decode().
|
||||||
|
*
|
||||||
|
* Callback for preg_replace_callback() within mime_header_decode().
|
||||||
|
*
|
||||||
|
* @param $matches
|
||||||
|
* The array of matches from preg_replace_callback().
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The mime-decoded string.
|
||||||
|
*
|
||||||
|
* @see mime_header_decode()
|
||||||
|
*/
|
||||||
|
function _mime_header_decode($matches) {
|
||||||
|
// Regexp groups:
|
||||||
|
// 1: Character set name
|
||||||
|
// 2: Escaping method (Q or B)
|
||||||
|
// 3: Encoded data
|
||||||
|
$data = ($matches[2] == 'B') ? base64_decode($matches[3]) : str_replace('_', ' ', quoted_printable_decode($matches[3]));
|
||||||
|
if (strtolower($matches[1]) != 'utf-8') {
|
||||||
|
$data = drupal_convert_to_utf8($data, $matches[1]);
|
||||||
|
}
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes all HTML entities (including numerical ones) to regular UTF-8 bytes.
|
||||||
|
*
|
||||||
|
* Double-escaped entities will only be decoded once ("&lt;" becomes "<"
|
||||||
|
* , not "<"). Be careful when using this function, as decode_entities can
|
||||||
|
* revert previous sanitization efforts (<script> will become <script>).
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The text to decode entities in.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The input $text, with all HTML entities decoded once.
|
||||||
|
*/
|
||||||
|
function decode_entities($text) {
|
||||||
|
return html_entity_decode($text, ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts the number of characters in a UTF-8 string.
|
||||||
|
*
|
||||||
|
* This is less than or equal to the byte count.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The string to run the operation on.
|
||||||
|
*
|
||||||
|
* @return integer
|
||||||
|
* The length of the string.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_strlen($text) {
|
||||||
|
global $multibyte;
|
||||||
|
if ($multibyte == UNICODE_MULTIBYTE) {
|
||||||
|
return mb_strlen($text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Do not count UTF-8 continuation bytes.
|
||||||
|
return strlen(preg_replace("/[\x80-\xBF]/", '', $text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uppercase a UTF-8 string.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The string to run the operation on.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The string in uppercase.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_strtoupper($text) {
|
||||||
|
global $multibyte;
|
||||||
|
if ($multibyte == UNICODE_MULTIBYTE) {
|
||||||
|
return mb_strtoupper($text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use C-locale for ASCII-only uppercase
|
||||||
|
$text = strtoupper($text);
|
||||||
|
// Case flip Latin-1 accented letters
|
||||||
|
$text = preg_replace_callback('/\xC3[\xA0-\xB6\xB8-\xBE]/', '_unicode_caseflip', $text);
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lowercase a UTF-8 string.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The string to run the operation on.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The string in lowercase.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_strtolower($text) {
|
||||||
|
global $multibyte;
|
||||||
|
if ($multibyte == UNICODE_MULTIBYTE) {
|
||||||
|
return mb_strtolower($text);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use C-locale for ASCII-only lowercase
|
||||||
|
$text = strtolower($text);
|
||||||
|
// Case flip Latin-1 accented letters
|
||||||
|
$text = preg_replace_callback('/\xC3[\x80-\x96\x98-\x9E]/', '_unicode_caseflip', $text);
|
||||||
|
return $text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Flips U+C0-U+DE to U+E0-U+FD and back.
|
||||||
|
*
|
||||||
|
* @param $matches
|
||||||
|
* An array of matches.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* The Latin-1 version of the array of matches.
|
||||||
|
*
|
||||||
|
* @see drupal_strtolower()
|
||||||
|
*/
|
||||||
|
function _unicode_caseflip($matches) {
|
||||||
|
return $matches[0][0] . chr(ord($matches[0][1]) ^ 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capitalizes the first letter of a UTF-8 string.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The string to convert.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The string with the first letter as uppercase.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_ucfirst($text) {
|
||||||
|
// Note: no mbstring equivalent!
|
||||||
|
return drupal_strtoupper(drupal_substr($text, 0, 1)) . drupal_substr($text, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cuts off a piece of a string based on character indices and counts.
|
||||||
|
*
|
||||||
|
* Follows the same behavior as PHP's own substr() function. Note that for
|
||||||
|
* cutting off a string at a known character/substring location, the usage of
|
||||||
|
* PHP's normal strpos/substr is safe and much faster.
|
||||||
|
*
|
||||||
|
* @param $text
|
||||||
|
* The input string.
|
||||||
|
* @param $start
|
||||||
|
* The position at which to start reading.
|
||||||
|
* @param $length
|
||||||
|
* The number of characters to read.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The shortened string.
|
||||||
|
*
|
||||||
|
* @ingroup php_wrappers
|
||||||
|
*/
|
||||||
|
function drupal_substr($text, $start, $length = NULL) {
|
||||||
|
global $multibyte;
|
||||||
|
if ($multibyte == UNICODE_MULTIBYTE) {
|
||||||
|
return $length === NULL ? mb_substr($text, $start) : mb_substr($text, $start, $length);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$strlen = strlen($text);
|
||||||
|
// Find the starting byte offset.
|
||||||
|
$bytes = 0;
|
||||||
|
if ($start > 0) {
|
||||||
|
// Count all the continuation bytes from the start until we have found
|
||||||
|
// $start characters or the end of the string.
|
||||||
|
$bytes = -1; $chars = -1;
|
||||||
|
while ($bytes < $strlen - 1 && $chars < $start) {
|
||||||
|
$bytes++;
|
||||||
|
$c = ord($text[$bytes]);
|
||||||
|
if ($c < 0x80 || $c >= 0xC0) {
|
||||||
|
$chars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($start < 0) {
|
||||||
|
// Count all the continuation bytes from the end until we have found
|
||||||
|
// abs($start) characters.
|
||||||
|
$start = abs($start);
|
||||||
|
$bytes = $strlen; $chars = 0;
|
||||||
|
while ($bytes > 0 && $chars < $start) {
|
||||||
|
$bytes--;
|
||||||
|
$c = ord($text[$bytes]);
|
||||||
|
if ($c < 0x80 || $c >= 0xC0) {
|
||||||
|
$chars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$istart = $bytes;
|
||||||
|
|
||||||
|
// Find the ending byte offset.
|
||||||
|
if ($length === NULL) {
|
||||||
|
$iend = $strlen;
|
||||||
|
}
|
||||||
|
elseif ($length > 0) {
|
||||||
|
// Count all the continuation bytes from the starting index until we have
|
||||||
|
// found $length characters or reached the end of the string, then
|
||||||
|
// backtrace one byte.
|
||||||
|
$iend = $istart - 1;
|
||||||
|
$chars = -1;
|
||||||
|
$last_real = FALSE;
|
||||||
|
while ($iend < $strlen - 1 && $chars < $length) {
|
||||||
|
$iend++;
|
||||||
|
$c = ord($text[$iend]);
|
||||||
|
$last_real = FALSE;
|
||||||
|
if ($c < 0x80 || $c >= 0xC0) {
|
||||||
|
$chars++;
|
||||||
|
$last_real = TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Backtrace one byte if the last character we found was a real character
|
||||||
|
// and we don't need it.
|
||||||
|
if ($last_real && $chars >= $length) {
|
||||||
|
$iend--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif ($length < 0) {
|
||||||
|
// Count all the continuation bytes from the end until we have found
|
||||||
|
// abs($start) characters, then backtrace one byte.
|
||||||
|
$length = abs($length);
|
||||||
|
$iend = $strlen; $chars = 0;
|
||||||
|
while ($iend > 0 && $chars < $length) {
|
||||||
|
$iend--;
|
||||||
|
$c = ord($text[$iend]);
|
||||||
|
if ($c < 0x80 || $c >= 0xC0) {
|
||||||
|
$chars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Backtrace one byte if we are not at the beginning of the string.
|
||||||
|
if ($iend > 0) {
|
||||||
|
$iend--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// $length == 0, return an empty string.
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($text, $istart, max(0, $iend - $istart + 1));
|
||||||
|
}
|
||||||
|
}
|
1489
includes/update.inc
Normal file
1489
includes/update.inc
Normal file
File diff suppressed because it is too large
Load diff
427
includes/updater.inc
Normal file
427
includes/updater.inc
Normal file
|
@ -0,0 +1,427 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Classes used for updating various files in the Drupal webroot. These
|
||||||
|
* classes use a FileTransfer object to actually perform the operations.
|
||||||
|
* Normally, the FileTransfer is provided when the site owner is redirected to
|
||||||
|
* authorize.php as part of a multistep process.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for a class which can update a Drupal project.
|
||||||
|
*
|
||||||
|
* An Updater currently serves the following purposes:
|
||||||
|
* - It can take a given directory, and determine if it can operate on it.
|
||||||
|
* - It can move the contents of that directory into the appropriate place
|
||||||
|
* on the system using FileTransfer classes.
|
||||||
|
* - It can return a list of "next steps" after an update or install.
|
||||||
|
* - In the future, it will most likely perform some of those steps as well.
|
||||||
|
*/
|
||||||
|
interface DrupalUpdaterInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the project is installed.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isInstalled();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the system name of the project.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
* A directory containing a project.
|
||||||
|
*/
|
||||||
|
public static function getProjectName($directory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
* An absolute path to the default install location.
|
||||||
|
*/
|
||||||
|
public function getInstallDirectory();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the Updater can handle the project provided in $directory.
|
||||||
|
*
|
||||||
|
* @todo: Provide something more rational here, like a project spec file.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
* TRUE if the project is installed, FALSE if not.
|
||||||
|
*/
|
||||||
|
public static function canUpdateDirectory($directory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to run after an install has occurred.
|
||||||
|
*/
|
||||||
|
public function postInstall();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Actions to run after an update has occurred.
|
||||||
|
*/
|
||||||
|
public function postUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for Updaters used in Drupal.
|
||||||
|
*/
|
||||||
|
class Updater {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string $source Directory to install from.
|
||||||
|
*/
|
||||||
|
public $source;
|
||||||
|
|
||||||
|
public function __construct($source) {
|
||||||
|
$this->source = $source;
|
||||||
|
$this->name = self::getProjectName($source);
|
||||||
|
$this->title = self::getProjectTitle($source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an Updater of the appropriate type depending on the source.
|
||||||
|
*
|
||||||
|
* If a directory is provided which contains a module, will return a
|
||||||
|
* ModuleUpdater.
|
||||||
|
*
|
||||||
|
* @param string $source
|
||||||
|
* Directory of a Drupal project.
|
||||||
|
*
|
||||||
|
* @return Updater
|
||||||
|
*/
|
||||||
|
public static function factory($source) {
|
||||||
|
if (is_dir($source)) {
|
||||||
|
$updater = self::getUpdaterFromDirectory($source);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw new UpdaterException(t('Unable to determine the type of the source directory.'));
|
||||||
|
}
|
||||||
|
return new $updater($source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine which Updater class can operate on the given directory.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
* Extracted Drupal project.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The class name which can work with this project type.
|
||||||
|
*/
|
||||||
|
public static function getUpdaterFromDirectory($directory) {
|
||||||
|
// Gets a list of possible implementing classes.
|
||||||
|
$updaters = drupal_get_updaters();
|
||||||
|
foreach ($updaters as $updater) {
|
||||||
|
$class = $updater['class'];
|
||||||
|
if (call_user_func(array($class, 'canUpdateDirectory'), $directory)) {
|
||||||
|
return $class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UpdaterException(t('Cannot determine the type of project.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Figure out what the most important (or only) info file is in a directory.
|
||||||
|
*
|
||||||
|
* Since there is no enforcement of which info file is the project's "main"
|
||||||
|
* info file, this will get one with the same name as the directory, or the
|
||||||
|
* first one it finds. Not ideal, but needs a larger solution.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
* Directory to search in.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* Path to the info file.
|
||||||
|
*/
|
||||||
|
public static function findInfoFile($directory) {
|
||||||
|
$info_files = file_scan_directory($directory, '/.*\.info$/');
|
||||||
|
if (!$info_files) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
foreach ($info_files as $info_file) {
|
||||||
|
if (drupal_substr($info_file->filename, 0, -5) == drupal_basename($directory)) {
|
||||||
|
// Info file Has the same name as the directory, return it.
|
||||||
|
return $info_file->uri;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, return the first one.
|
||||||
|
$info_file = array_shift($info_files);
|
||||||
|
return $info_file->uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the name of the project directory (basename).
|
||||||
|
*
|
||||||
|
* @todo: It would be nice, if projects contained an info file which could
|
||||||
|
* provide their canonical name.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The name of the project.
|
||||||
|
*/
|
||||||
|
public static function getProjectName($directory) {
|
||||||
|
return drupal_basename($directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the project name from a Drupal info file.
|
||||||
|
*
|
||||||
|
* @param string $directory
|
||||||
|
* Directory to search for the info file.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The title of the project.
|
||||||
|
*/
|
||||||
|
public static function getProjectTitle($directory) {
|
||||||
|
$info_file = self::findInfoFile($directory);
|
||||||
|
$info = drupal_parse_info_file($info_file);
|
||||||
|
if (empty($info)) {
|
||||||
|
throw new UpdaterException(t('Unable to parse info file: %info_file.', array('%info_file' => $info_file)));
|
||||||
|
}
|
||||||
|
if (empty($info['name'])) {
|
||||||
|
throw new UpdaterException(t("The info file (%info_file) does not define a 'name' attribute.", array('%info_file' => $info_file)));
|
||||||
|
}
|
||||||
|
return $info['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the default parameters for the Updater.
|
||||||
|
*
|
||||||
|
* @param array $overrides
|
||||||
|
* An array of overrides for the default parameters.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of configuration parameters for an update or install operation.
|
||||||
|
*/
|
||||||
|
protected function getInstallArgs($overrides = array()) {
|
||||||
|
$args = array(
|
||||||
|
'make_backup' => FALSE,
|
||||||
|
'install_dir' => $this->getInstallDirectory(),
|
||||||
|
'backup_dir' => $this->getBackupDir(),
|
||||||
|
);
|
||||||
|
return array_merge($args, $overrides);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a Drupal project, returns a list of next actions.
|
||||||
|
*
|
||||||
|
* @param FileTransfer $filetransfer
|
||||||
|
* Object that is a child of FileTransfer. Used for moving files
|
||||||
|
* to the server.
|
||||||
|
* @param array $overrides
|
||||||
|
* An array of settings to override defaults; see self::getInstallArgs().
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of links which the user may need to complete the update
|
||||||
|
*/
|
||||||
|
public function update(&$filetransfer, $overrides = array()) {
|
||||||
|
try {
|
||||||
|
// Establish arguments with possible overrides.
|
||||||
|
$args = $this->getInstallArgs($overrides);
|
||||||
|
|
||||||
|
// Take a Backup.
|
||||||
|
if ($args['make_backup']) {
|
||||||
|
$this->makeBackup($args['install_dir'], $args['backup_dir']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->name) {
|
||||||
|
// This is bad, don't want to delete the install directory.
|
||||||
|
throw new UpdaterException(t('Fatal error in update, cowardly refusing to wipe out the install directory.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the installation parent directory exists and is writable.
|
||||||
|
$this->prepareInstallDirectory($filetransfer, $args['install_dir']);
|
||||||
|
|
||||||
|
// Note: If the project is installed in sites/all, it will not be
|
||||||
|
// deleted. It will be installed in sites/default as that will override
|
||||||
|
// the sites/all reference and not break other sites which are using it.
|
||||||
|
if (is_dir($args['install_dir'] . '/' . $this->name)) {
|
||||||
|
// Remove the existing installed file.
|
||||||
|
$filetransfer->removeDirectory($args['install_dir'] . '/' . $this->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the directory in place.
|
||||||
|
$filetransfer->copyDirectory($this->source, $args['install_dir']);
|
||||||
|
|
||||||
|
// Make sure what we just installed is readable by the web server.
|
||||||
|
$this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
|
||||||
|
|
||||||
|
// Run the updates.
|
||||||
|
// @TODO: decide if we want to implement this.
|
||||||
|
$this->postUpdate();
|
||||||
|
|
||||||
|
// For now, just return a list of links of things to do.
|
||||||
|
return $this->postUpdateTasks();
|
||||||
|
}
|
||||||
|
catch (FileTransferException $e) {
|
||||||
|
throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a Drupal project, returns a list of next actions.
|
||||||
|
*
|
||||||
|
* @param FileTransfer $filetransfer
|
||||||
|
* Object that is a child of FileTransfer.
|
||||||
|
* @param array $overrides
|
||||||
|
* An array of settings to override defaults; see self::getInstallArgs().
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of links which the user may need to complete the install.
|
||||||
|
*/
|
||||||
|
public function install(&$filetransfer, $overrides = array()) {
|
||||||
|
try {
|
||||||
|
// Establish arguments with possible overrides.
|
||||||
|
$args = $this->getInstallArgs($overrides);
|
||||||
|
|
||||||
|
// Make sure the installation parent directory exists and is writable.
|
||||||
|
$this->prepareInstallDirectory($filetransfer, $args['install_dir']);
|
||||||
|
|
||||||
|
// Copy the directory in place.
|
||||||
|
$filetransfer->copyDirectory($this->source, $args['install_dir']);
|
||||||
|
|
||||||
|
// Make sure what we just installed is readable by the web server.
|
||||||
|
$this->makeWorldReadable($filetransfer, $args['install_dir'] . '/' . $this->name);
|
||||||
|
|
||||||
|
// Potentially enable something?
|
||||||
|
// @TODO: decide if we want to implement this.
|
||||||
|
$this->postInstall();
|
||||||
|
// For now, just return a list of links of things to do.
|
||||||
|
return $this->postInstallTasks();
|
||||||
|
}
|
||||||
|
catch (FileTransferException $e) {
|
||||||
|
throw new UpdaterFileTransferException(t('File Transfer failed, reason: !reason', array('!reason' => strtr($e->getMessage(), $e->arguments))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure the installation parent directory exists and is writable.
|
||||||
|
*
|
||||||
|
* @param FileTransfer $filetransfer
|
||||||
|
* Object which is a child of FileTransfer.
|
||||||
|
* @param string $directory
|
||||||
|
* The installation directory to prepare.
|
||||||
|
*/
|
||||||
|
public function prepareInstallDirectory(&$filetransfer, $directory) {
|
||||||
|
// Make the parent dir writable if need be and create the dir.
|
||||||
|
if (!is_dir($directory)) {
|
||||||
|
$parent_dir = dirname($directory);
|
||||||
|
if (!is_writable($parent_dir)) {
|
||||||
|
@chmod($parent_dir, 0755);
|
||||||
|
// It is expected that this will fail if the directory is owned by the
|
||||||
|
// FTP user. If the FTP user == web server, it will succeed.
|
||||||
|
try {
|
||||||
|
$filetransfer->createDirectory($directory);
|
||||||
|
$this->makeWorldReadable($filetransfer, $directory);
|
||||||
|
}
|
||||||
|
catch (FileTransferException $e) {
|
||||||
|
// Probably still not writable. Try to chmod and do it again.
|
||||||
|
// @todo: Make a new exception class so we can catch it differently.
|
||||||
|
try {
|
||||||
|
$old_perms = substr(sprintf('%o', fileperms($parent_dir)), -4);
|
||||||
|
$filetransfer->chmod($parent_dir, 0755);
|
||||||
|
$filetransfer->createDirectory($directory);
|
||||||
|
$this->makeWorldReadable($filetransfer, $directory);
|
||||||
|
// Put the permissions back.
|
||||||
|
$filetransfer->chmod($parent_dir, intval($old_perms, 8));
|
||||||
|
}
|
||||||
|
catch (FileTransferException $e) {
|
||||||
|
$message = t($e->getMessage(), $e->arguments);
|
||||||
|
$throw_message = t('Unable to create %directory due to the following: %reason', array('%directory' => $directory, '%reason' => $message));
|
||||||
|
throw new UpdaterException($throw_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Put the parent directory back.
|
||||||
|
@chmod($parent_dir, 0555);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure that a given directory is world readable.
|
||||||
|
*
|
||||||
|
* @param FileTransfer $filetransfer
|
||||||
|
* Object which is a child of FileTransfer.
|
||||||
|
* @param string $path
|
||||||
|
* The file path to make world readable.
|
||||||
|
* @param bool $recursive
|
||||||
|
* If the chmod should be applied recursively.
|
||||||
|
*/
|
||||||
|
public function makeWorldReadable(&$filetransfer, $path, $recursive = TRUE) {
|
||||||
|
if (!is_executable($path)) {
|
||||||
|
// Set it to read + execute.
|
||||||
|
$new_perms = substr(sprintf('%o', fileperms($path)), -4, -1) . "5";
|
||||||
|
$filetransfer->chmod($path, intval($new_perms, 8), $recursive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform a backup.
|
||||||
|
*
|
||||||
|
* @todo Not implemented.
|
||||||
|
*/
|
||||||
|
public function makeBackup(&$filetransfer, $from, $to) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the full path to a directory where backups should be written.
|
||||||
|
*/
|
||||||
|
public function getBackupDir() {
|
||||||
|
return file_stream_wrapper_get_instance_by_scheme('temporary')->getDirectoryPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions after new code is updated.
|
||||||
|
*/
|
||||||
|
public function postUpdate() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions after installation.
|
||||||
|
*/
|
||||||
|
public function postInstall() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of links to pages that should be visited post operation.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Links which provide actions to take after the install is finished.
|
||||||
|
*/
|
||||||
|
public function postInstallTasks() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an array of links to pages that should be visited post operation.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Links which provide actions to take after the update is finished.
|
||||||
|
*/
|
||||||
|
public function postUpdateTasks() {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception class for the Updater class hierarchy.
|
||||||
|
*
|
||||||
|
* This is identical to the base Exception class, we just give it a more
|
||||||
|
* specific name so that call sites that want to tell the difference can
|
||||||
|
* specifically catch these exceptions and treat them differently.
|
||||||
|
*/
|
||||||
|
class UpdaterException extends Exception {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Child class of UpdaterException that indicates a FileTransfer exception.
|
||||||
|
*
|
||||||
|
* We have to catch FileTransfer exceptions and wrap those in t(), since
|
||||||
|
* FileTransfer is so low-level that it doesn't use any Drupal APIs and none
|
||||||
|
* of the strings are translated.
|
||||||
|
*/
|
||||||
|
class UpdaterFileTransferException extends UpdaterException {
|
||||||
|
}
|
66
includes/utility.inc
Normal file
66
includes/utility.inc
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Miscellaneous functions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drupal-friendly var_export().
|
||||||
|
*
|
||||||
|
* @param $var
|
||||||
|
* The variable to export.
|
||||||
|
* @param $prefix
|
||||||
|
* A prefix that will be added at the beginning of every lines of the output.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The variable exported in a way compatible to Drupal's coding standards.
|
||||||
|
*/
|
||||||
|
function drupal_var_export($var, $prefix = '') {
|
||||||
|
if (is_array($var)) {
|
||||||
|
if (empty($var)) {
|
||||||
|
$output = 'array()';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$output = "array(\n";
|
||||||
|
// Don't export keys if the array is non associative.
|
||||||
|
$export_keys = array_values($var) != $var;
|
||||||
|
foreach ($var as $key => $value) {
|
||||||
|
$output .= ' ' . ($export_keys ? drupal_var_export($key) . ' => ' : '') . drupal_var_export($value, ' ', FALSE) . ",\n";
|
||||||
|
}
|
||||||
|
$output .= ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (is_bool($var)) {
|
||||||
|
$output = $var ? 'TRUE' : 'FALSE';
|
||||||
|
}
|
||||||
|
elseif (is_string($var)) {
|
||||||
|
$line_safe_var = str_replace("\n", '\n', $var);
|
||||||
|
if (strpos($var, "\n") !== FALSE || strpos($var, "'") !== FALSE) {
|
||||||
|
// If the string contains a line break or a single quote, use the
|
||||||
|
// double quote export mode. Encode backslash and double quotes and
|
||||||
|
// transform some common control characters.
|
||||||
|
$var = str_replace(array('\\', '"', "\n", "\r", "\t"), array('\\\\', '\"', '\n', '\r', '\t'), $var);
|
||||||
|
$output = '"' . $var . '"';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$output = "'" . $var . "'";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elseif (is_object($var) && get_class($var) === 'stdClass') {
|
||||||
|
// var_export() will export stdClass objects using an undefined
|
||||||
|
// magic method __set_state() leaving the export broken. This
|
||||||
|
// workaround avoids this by casting the object as an array for
|
||||||
|
// export and casting it back to an object when evaluated.
|
||||||
|
$output = '(object) ' . drupal_var_export((array) $var, $prefix);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$output = var_export($var, TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($prefix) {
|
||||||
|
$output = str_replace("\n", "\n$prefix", $output);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
658
includes/xmlrpc.inc
Normal file
658
includes/xmlrpc.inc
Normal file
|
@ -0,0 +1,658 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Drupal XML-RPC library.
|
||||||
|
*
|
||||||
|
* Based on the IXR - The Incutio XML-RPC Library - (c) Incutio Ltd 2002-2005
|
||||||
|
* Version 1.7 (beta) - Simon Willison, 23rd May 2005
|
||||||
|
* Site: http://scripts.incutio.com/xmlrpc/
|
||||||
|
* Manual: http://scripts.incutio.com/xmlrpc/manual.php
|
||||||
|
* This version is made available under the GNU GPL License
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a data structure into objects with 'data' and 'type' attributes.
|
||||||
|
*
|
||||||
|
* @param $data
|
||||||
|
* The data structure.
|
||||||
|
* @param $type
|
||||||
|
* Optional type to assign to $data.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC data object containing the input $data.
|
||||||
|
*/
|
||||||
|
function xmlrpc_value($data, $type = FALSE) {
|
||||||
|
$xmlrpc_value = new stdClass();
|
||||||
|
$xmlrpc_value->data = $data;
|
||||||
|
if (!$type) {
|
||||||
|
$type = xmlrpc_value_calculate_type($xmlrpc_value);
|
||||||
|
}
|
||||||
|
$xmlrpc_value->type = $type;
|
||||||
|
if ($type == 'struct') {
|
||||||
|
// Turn all the values in the array into new xmlrpc_values
|
||||||
|
foreach ($xmlrpc_value->data as $key => $value) {
|
||||||
|
$xmlrpc_value->data[$key] = xmlrpc_value($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($type == 'array') {
|
||||||
|
for ($i = 0, $j = count($xmlrpc_value->data); $i < $j; $i++) {
|
||||||
|
$xmlrpc_value->data[$i] = xmlrpc_value($xmlrpc_value->data[$i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $xmlrpc_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a PHP type to an XML-RPC type.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_value
|
||||||
|
* Variable whose type should be mapped.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* The corresponding XML-RPC type.
|
||||||
|
*
|
||||||
|
* @see http://www.xmlrpc.com/spec#scalars
|
||||||
|
*/
|
||||||
|
function xmlrpc_value_calculate_type($xmlrpc_value) {
|
||||||
|
// http://www.php.net/gettype: Never use gettype() to test for a certain type
|
||||||
|
// [...] Instead, use the is_* functions.
|
||||||
|
if (is_bool($xmlrpc_value->data)) {
|
||||||
|
return 'boolean';
|
||||||
|
}
|
||||||
|
if (is_double($xmlrpc_value->data)) {
|
||||||
|
return 'double';
|
||||||
|
}
|
||||||
|
if (is_int($xmlrpc_value->data)) {
|
||||||
|
return 'int';
|
||||||
|
}
|
||||||
|
if (is_array($xmlrpc_value->data)) {
|
||||||
|
// empty or integer-indexed arrays are 'array', string-indexed arrays 'struct'
|
||||||
|
return empty($xmlrpc_value->data) || range(0, count($xmlrpc_value->data) - 1) === array_keys($xmlrpc_value->data) ? 'array' : 'struct';
|
||||||
|
}
|
||||||
|
if (is_object($xmlrpc_value->data)) {
|
||||||
|
if (isset($xmlrpc_value->data->is_date)) {
|
||||||
|
return 'date';
|
||||||
|
}
|
||||||
|
if (isset($xmlrpc_value->data->is_base64)) {
|
||||||
|
return 'base64';
|
||||||
|
}
|
||||||
|
$xmlrpc_value->data = get_object_vars($xmlrpc_value->data);
|
||||||
|
return 'struct';
|
||||||
|
}
|
||||||
|
// default
|
||||||
|
return 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates XML representing the given value.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_value
|
||||||
|
* A value to be represented in XML.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* XML representation of $xmlrpc_value.
|
||||||
|
*/
|
||||||
|
function xmlrpc_value_get_xml($xmlrpc_value) {
|
||||||
|
switch ($xmlrpc_value->type) {
|
||||||
|
case 'boolean':
|
||||||
|
return '<boolean>' . (($xmlrpc_value->data) ? '1' : '0') . '</boolean>';
|
||||||
|
|
||||||
|
case 'int':
|
||||||
|
return '<int>' . $xmlrpc_value->data . '</int>';
|
||||||
|
|
||||||
|
case 'double':
|
||||||
|
return '<double>' . $xmlrpc_value->data . '</double>';
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
// Note: we don't escape apostrophes because of the many blogging clients
|
||||||
|
// that don't support numerical entities (and XML in general) properly.
|
||||||
|
return '<string>' . htmlspecialchars($xmlrpc_value->data) . '</string>';
|
||||||
|
|
||||||
|
case 'array':
|
||||||
|
$return = '<array><data>' . "\n";
|
||||||
|
foreach ($xmlrpc_value->data as $item) {
|
||||||
|
$return .= ' <value>' . xmlrpc_value_get_xml($item) . "</value>\n";
|
||||||
|
}
|
||||||
|
$return .= '</data></array>';
|
||||||
|
return $return;
|
||||||
|
|
||||||
|
case 'struct':
|
||||||
|
$return = '<struct>' . "\n";
|
||||||
|
foreach ($xmlrpc_value->data as $name => $value) {
|
||||||
|
$return .= " <member><name>" . check_plain($name) . "</name><value>";
|
||||||
|
$return .= xmlrpc_value_get_xml($value) . "</value></member>\n";
|
||||||
|
}
|
||||||
|
$return .= '</struct>';
|
||||||
|
return $return;
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
return xmlrpc_date_get_xml($xmlrpc_value->data);
|
||||||
|
|
||||||
|
case 'base64':
|
||||||
|
return xmlrpc_base64_get_xml($xmlrpc_value->data);
|
||||||
|
}
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an object representing an XML-RPC message.
|
||||||
|
*
|
||||||
|
* @param $message
|
||||||
|
* A string containing an XML message.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC object containing the message.
|
||||||
|
*
|
||||||
|
* @see http://www.xmlrpc.com/spec
|
||||||
|
*/
|
||||||
|
function xmlrpc_message($message) {
|
||||||
|
$xmlrpc_message = new stdClass();
|
||||||
|
// The stack used to keep track of the current array/struct
|
||||||
|
$xmlrpc_message->array_structs = array();
|
||||||
|
// The stack used to keep track of if things are structs or array
|
||||||
|
$xmlrpc_message->array_structs_types = array();
|
||||||
|
// A stack as well
|
||||||
|
$xmlrpc_message->current_struct_name = array();
|
||||||
|
$xmlrpc_message->message = $message;
|
||||||
|
return $xmlrpc_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses an XML-RPC message.
|
||||||
|
*
|
||||||
|
* If parsing fails, the faultCode and faultString will be added to the message
|
||||||
|
* object.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_message
|
||||||
|
* An object generated by xmlrpc_message().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* TRUE if parsing succeeded; FALSE otherwise.
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_parse($xmlrpc_message) {
|
||||||
|
$xmlrpc_message->_parser = xml_parser_create();
|
||||||
|
// Set XML parser to take the case of tags into account.
|
||||||
|
xml_parser_set_option($xmlrpc_message->_parser, XML_OPTION_CASE_FOLDING, FALSE);
|
||||||
|
// Set XML parser callback functions
|
||||||
|
xml_set_element_handler($xmlrpc_message->_parser, 'xmlrpc_message_tag_open', 'xmlrpc_message_tag_close');
|
||||||
|
xml_set_character_data_handler($xmlrpc_message->_parser, 'xmlrpc_message_cdata');
|
||||||
|
xmlrpc_message_set($xmlrpc_message);
|
||||||
|
|
||||||
|
// Strip XML declaration.
|
||||||
|
$header = preg_replace('/<\?xml.*?\?'.'>/s', '', substr($xmlrpc_message->message, 0, 100), 1);
|
||||||
|
$xml = trim(substr_replace($xmlrpc_message->message, $header, 0, 100));
|
||||||
|
if ($xml == '') {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// Strip DTD.
|
||||||
|
$header = preg_replace('/^<!DOCTYPE[^>]*+>/i', '', substr($xml, 0, 200), 1);
|
||||||
|
$xml = trim(substr_replace($xml, $header, 0, 200));
|
||||||
|
if ($xml == '') {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// Confirm the XML now starts with a valid root tag. A root tag can end in [> \t\r\n]
|
||||||
|
$root_tag = substr($xml, 0, strcspn(substr($xml, 0, 20), "> \t\r\n"));
|
||||||
|
// Reject a second DTD.
|
||||||
|
if (strtoupper($root_tag) == '<!DOCTYPE') {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (!in_array($root_tag, array('<methodCall', '<methodResponse', '<fault'))) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// Skip parsing if there is an unreasonably large number of tags.
|
||||||
|
try {
|
||||||
|
$dom = new DOMDocument();
|
||||||
|
@$dom->loadXML($xml);
|
||||||
|
if ($dom->getElementsByTagName('*')->length > variable_get('xmlrpc_message_maximum_tag_count', 30000)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception $e) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!xml_parse($xmlrpc_message->_parser, $xml)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
xml_parser_free($xmlrpc_message->_parser);
|
||||||
|
|
||||||
|
// Grab the error messages, if any.
|
||||||
|
$xmlrpc_message = xmlrpc_message_get();
|
||||||
|
if (!isset($xmlrpc_message->messagetype)) {
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
elseif ($xmlrpc_message->messagetype == 'fault') {
|
||||||
|
$xmlrpc_message->fault_code = $xmlrpc_message->params[0]['faultCode'];
|
||||||
|
$xmlrpc_message->fault_string = $xmlrpc_message->params[0]['faultString'];
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a copy of the most recent XML-RPC message object temporarily.
|
||||||
|
*
|
||||||
|
* @param $value
|
||||||
|
* An XML-RPC message to store, or NULL to keep the last message.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* The most recently stored message.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_message_get()
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_set($value = NULL) {
|
||||||
|
static $xmlrpc_message;
|
||||||
|
if ($value) {
|
||||||
|
$xmlrpc_message = $value;
|
||||||
|
}
|
||||||
|
return $xmlrpc_message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the most recently stored XML-RPC message object.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* The most recently stored message.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_message_set()
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_get() {
|
||||||
|
return xmlrpc_message_set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles opening tags for XML parsing in xmlrpc_message_parse().
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_tag_open($parser, $tag, $attr) {
|
||||||
|
$xmlrpc_message = xmlrpc_message_get();
|
||||||
|
$xmlrpc_message->current_tag_contents = '';
|
||||||
|
$xmlrpc_message->last_open = $tag;
|
||||||
|
switch ($tag) {
|
||||||
|
case 'methodCall':
|
||||||
|
case 'methodResponse':
|
||||||
|
case 'fault':
|
||||||
|
$xmlrpc_message->messagetype = $tag;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Deal with stacks of arrays and structs
|
||||||
|
case 'data':
|
||||||
|
$xmlrpc_message->array_structs_types[] = 'array';
|
||||||
|
$xmlrpc_message->array_structs[] = array();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'struct':
|
||||||
|
$xmlrpc_message->array_structs_types[] = 'struct';
|
||||||
|
$xmlrpc_message->array_structs[] = array();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
xmlrpc_message_set($xmlrpc_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles character data for XML parsing in xmlrpc_message_parse().
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_cdata($parser, $cdata) {
|
||||||
|
$xmlrpc_message = xmlrpc_message_get();
|
||||||
|
$xmlrpc_message->current_tag_contents .= $cdata;
|
||||||
|
xmlrpc_message_set($xmlrpc_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles closing tags for XML parsing in xmlrpc_message_parse().
|
||||||
|
*/
|
||||||
|
function xmlrpc_message_tag_close($parser, $tag) {
|
||||||
|
$xmlrpc_message = xmlrpc_message_get();
|
||||||
|
$value_flag = FALSE;
|
||||||
|
switch ($tag) {
|
||||||
|
case 'int':
|
||||||
|
case 'i4':
|
||||||
|
$value = (int)trim($xmlrpc_message->current_tag_contents);
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'double':
|
||||||
|
$value = (double)trim($xmlrpc_message->current_tag_contents);
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'string':
|
||||||
|
$value = $xmlrpc_message->current_tag_contents;
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'dateTime.iso8601':
|
||||||
|
$value = xmlrpc_date(trim($xmlrpc_message->current_tag_contents));
|
||||||
|
// $value = $iso->getTimestamp();
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'value':
|
||||||
|
// If no type is indicated, the type is string
|
||||||
|
// We take special care for empty values
|
||||||
|
if (trim($xmlrpc_message->current_tag_contents) != '' || (isset($xmlrpc_message->last_open) && ($xmlrpc_message->last_open == 'value'))) {
|
||||||
|
$value = (string) $xmlrpc_message->current_tag_contents;
|
||||||
|
$value_flag = TRUE;
|
||||||
|
}
|
||||||
|
unset($xmlrpc_message->last_open);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
$value = (boolean)trim($xmlrpc_message->current_tag_contents);
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'base64':
|
||||||
|
$value = base64_decode(trim($xmlrpc_message->current_tag_contents));
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Deal with stacks of arrays and structs
|
||||||
|
case 'data':
|
||||||
|
case 'struct':
|
||||||
|
$value = array_pop($xmlrpc_message->array_structs);
|
||||||
|
array_pop($xmlrpc_message->array_structs_types);
|
||||||
|
$value_flag = TRUE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'member':
|
||||||
|
array_pop($xmlrpc_message->current_struct_name);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'name':
|
||||||
|
$xmlrpc_message->current_struct_name[] = trim($xmlrpc_message->current_tag_contents);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'methodName':
|
||||||
|
$xmlrpc_message->methodname = trim($xmlrpc_message->current_tag_contents);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ($value_flag) {
|
||||||
|
if (count($xmlrpc_message->array_structs) > 0) {
|
||||||
|
// Add value to struct or array
|
||||||
|
if ($xmlrpc_message->array_structs_types[count($xmlrpc_message->array_structs_types) - 1] == 'struct') {
|
||||||
|
// Add to struct
|
||||||
|
$xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][$xmlrpc_message->current_struct_name[count($xmlrpc_message->current_struct_name) - 1]] = $value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Add to array
|
||||||
|
$xmlrpc_message->array_structs[count($xmlrpc_message->array_structs) - 1][] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Just add as a parameter
|
||||||
|
$xmlrpc_message->params[] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!in_array($tag, array("data", "struct", "member"))) {
|
||||||
|
$xmlrpc_message->current_tag_contents = '';
|
||||||
|
}
|
||||||
|
xmlrpc_message_set($xmlrpc_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs an object representing an XML-RPC request.
|
||||||
|
*
|
||||||
|
* @param $method
|
||||||
|
* The name of the method to be called.
|
||||||
|
* @param $args
|
||||||
|
* An array of parameters to send with the method.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC object representing the request.
|
||||||
|
*/
|
||||||
|
function xmlrpc_request($method, $args) {
|
||||||
|
$xmlrpc_request = new stdClass();
|
||||||
|
$xmlrpc_request->method = $method;
|
||||||
|
$xmlrpc_request->args = $args;
|
||||||
|
$xmlrpc_request->xml = <<<EOD
|
||||||
|
<?xml version="1.0"?>
|
||||||
|
<methodCall>
|
||||||
|
<methodName>{$xmlrpc_request->method}</methodName>
|
||||||
|
<params>
|
||||||
|
|
||||||
|
EOD;
|
||||||
|
foreach ($xmlrpc_request->args as $arg) {
|
||||||
|
$xmlrpc_request->xml .= '<param><value>';
|
||||||
|
$v = xmlrpc_value($arg);
|
||||||
|
$xmlrpc_request->xml .= xmlrpc_value_get_xml($v);
|
||||||
|
$xmlrpc_request->xml .= "</value></param>\n";
|
||||||
|
}
|
||||||
|
$xmlrpc_request->xml .= '</params></methodCall>';
|
||||||
|
return $xmlrpc_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates, temporarily saves, and returns an XML-RPC error object.
|
||||||
|
*
|
||||||
|
* @param $code
|
||||||
|
* The error code.
|
||||||
|
* @param $message
|
||||||
|
* The error message.
|
||||||
|
* @param $reset
|
||||||
|
* TRUE to empty the temporary error storage. Ignored if $code is supplied.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC error object representing $code and $message, or the most
|
||||||
|
* recently stored error object if omitted.
|
||||||
|
*/
|
||||||
|
function xmlrpc_error($code = NULL, $message = NULL, $reset = FALSE) {
|
||||||
|
static $xmlrpc_error;
|
||||||
|
if (isset($code)) {
|
||||||
|
$xmlrpc_error = new stdClass();
|
||||||
|
$xmlrpc_error->is_error = TRUE;
|
||||||
|
$xmlrpc_error->code = $code;
|
||||||
|
$xmlrpc_error->message = $message;
|
||||||
|
}
|
||||||
|
elseif ($reset) {
|
||||||
|
$xmlrpc_error = NULL;
|
||||||
|
}
|
||||||
|
return $xmlrpc_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an XML-RPC error object into XML.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_error
|
||||||
|
* The XML-RPC error object.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* An XML representation of the error as an XML methodResponse.
|
||||||
|
*/
|
||||||
|
function xmlrpc_error_get_xml($xmlrpc_error) {
|
||||||
|
return <<<EOD
|
||||||
|
<methodResponse>
|
||||||
|
<fault>
|
||||||
|
<value>
|
||||||
|
<struct>
|
||||||
|
<member>
|
||||||
|
<name>faultCode</name>
|
||||||
|
<value><int>{$xmlrpc_error->code}</int></value>
|
||||||
|
</member>
|
||||||
|
<member>
|
||||||
|
<name>faultString</name>
|
||||||
|
<value><string>{$xmlrpc_error->message}</string></value>
|
||||||
|
</member>
|
||||||
|
</struct>
|
||||||
|
</value>
|
||||||
|
</fault>
|
||||||
|
</methodResponse>
|
||||||
|
|
||||||
|
EOD;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a PHP or ISO date/time to an XML-RPC object.
|
||||||
|
*
|
||||||
|
* @param $time
|
||||||
|
* A PHP timestamp or an ISO date-time string.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC time/date object.
|
||||||
|
*/
|
||||||
|
function xmlrpc_date($time) {
|
||||||
|
$xmlrpc_date = new stdClass();
|
||||||
|
$xmlrpc_date->is_date = TRUE;
|
||||||
|
// $time can be a PHP timestamp or an ISO one
|
||||||
|
if (is_numeric($time)) {
|
||||||
|
$xmlrpc_date->year = gmdate('Y', $time);
|
||||||
|
$xmlrpc_date->month = gmdate('m', $time);
|
||||||
|
$xmlrpc_date->day = gmdate('d', $time);
|
||||||
|
$xmlrpc_date->hour = gmdate('H', $time);
|
||||||
|
$xmlrpc_date->minute = gmdate('i', $time);
|
||||||
|
$xmlrpc_date->second = gmdate('s', $time);
|
||||||
|
$xmlrpc_date->iso8601 = gmdate('Ymd\TH:i:s', $time);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$xmlrpc_date->iso8601 = $time;
|
||||||
|
$time = str_replace(array('-', ':'), '', $time);
|
||||||
|
$xmlrpc_date->year = substr($time, 0, 4);
|
||||||
|
$xmlrpc_date->month = substr($time, 4, 2);
|
||||||
|
$xmlrpc_date->day = substr($time, 6, 2);
|
||||||
|
$xmlrpc_date->hour = substr($time, 9, 2);
|
||||||
|
$xmlrpc_date->minute = substr($time, 11, 2);
|
||||||
|
$xmlrpc_date->second = substr($time, 13, 2);
|
||||||
|
}
|
||||||
|
return $xmlrpc_date;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an XML-RPC date-time object into XML.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_date
|
||||||
|
* The XML-RPC date-time object.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* An XML representation of the date/time as XML.
|
||||||
|
*/
|
||||||
|
function xmlrpc_date_get_xml($xmlrpc_date) {
|
||||||
|
return '<dateTime.iso8601>' . $xmlrpc_date->year . $xmlrpc_date->month . $xmlrpc_date->day . 'T' . $xmlrpc_date->hour . ':' . $xmlrpc_date->minute . ':' . $xmlrpc_date->second . '</dateTime.iso8601>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an XML-RPC base 64 object.
|
||||||
|
*
|
||||||
|
* @param $data
|
||||||
|
* Base 64 data to store in returned object.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* An XML-RPC base 64 object.
|
||||||
|
*/
|
||||||
|
function xmlrpc_base64($data) {
|
||||||
|
$xmlrpc_base64 = new stdClass();
|
||||||
|
$xmlrpc_base64->is_base64 = TRUE;
|
||||||
|
$xmlrpc_base64->data = $data;
|
||||||
|
return $xmlrpc_base64;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an XML-RPC base 64 object into XML.
|
||||||
|
*
|
||||||
|
* @param $xmlrpc_base64
|
||||||
|
* The XML-RPC base 64 object.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* An XML representation of the base 64 data as XML.
|
||||||
|
*/
|
||||||
|
function xmlrpc_base64_get_xml($xmlrpc_base64) {
|
||||||
|
return '<base64>' . base64_encode($xmlrpc_base64->data) . '</base64>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs one or more XML-RPC requests.
|
||||||
|
*
|
||||||
|
* @param $url
|
||||||
|
* An absolute URL of the XML-RPC endpoint, e.g.,
|
||||||
|
* http://example.com/xmlrpc.php
|
||||||
|
* @param $args
|
||||||
|
* An associative array whose keys are the methods to call and whose values
|
||||||
|
* are the arguments to pass to the respective method. If multiple methods
|
||||||
|
* are specified, a system.multicall is performed.
|
||||||
|
* @param $options
|
||||||
|
* (optional) An array of options to pass along to drupal_http_request().
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* A single response (single request) or an array of responses (multicall
|
||||||
|
* request). Each response is the return value of the method, just as if it
|
||||||
|
* has been a local function call, on success, or FALSE on failure. If FALSE
|
||||||
|
* is returned, see xmlrpc_errno() and xmlrpc_error_msg() to get more
|
||||||
|
* information.
|
||||||
|
*/
|
||||||
|
function _xmlrpc($url, $args, $options = array()) {
|
||||||
|
xmlrpc_clear_error();
|
||||||
|
if (count($args) > 1) {
|
||||||
|
$multicall_args = array();
|
||||||
|
foreach ($args as $method => $call) {
|
||||||
|
$multicall_args[] = array('methodName' => $method, 'params' => $call);
|
||||||
|
}
|
||||||
|
$method = 'system.multicall';
|
||||||
|
$args = array($multicall_args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$method = key($args);
|
||||||
|
$args = $args[$method];
|
||||||
|
}
|
||||||
|
$xmlrpc_request = xmlrpc_request($method, $args);
|
||||||
|
// Required options which will replace any that are passed in.
|
||||||
|
$options['method'] = 'POST';
|
||||||
|
$options['headers']['Content-Type'] = 'text/xml';
|
||||||
|
$options['data'] = $xmlrpc_request->xml;
|
||||||
|
$result = drupal_http_request($url, $options);
|
||||||
|
if ($result->code != 200) {
|
||||||
|
xmlrpc_error($result->code, $result->error);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
$message = xmlrpc_message($result->data);
|
||||||
|
// Now parse what we've got back
|
||||||
|
if (!xmlrpc_message_parse($message)) {
|
||||||
|
// XML error
|
||||||
|
xmlrpc_error(-32700, t('Parse error. Not well formed'));
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// Is the message a fault?
|
||||||
|
if ($message->messagetype == 'fault') {
|
||||||
|
xmlrpc_error($message->fault_code, $message->fault_string);
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
// We now know that the message is well-formed and a non-fault result.
|
||||||
|
if ($method == 'system.multicall') {
|
||||||
|
// Return per-method results or error objects.
|
||||||
|
$return = array();
|
||||||
|
foreach ($message->params[0] as $result) {
|
||||||
|
if (array_keys($result) == array(0)) {
|
||||||
|
$return[] = $result[0];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = xmlrpc_error($result['faultCode'], $result['faultString']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return = $message->params[0];
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last XML-RPC client error number.
|
||||||
|
*/
|
||||||
|
function xmlrpc_errno() {
|
||||||
|
$error = xmlrpc_error();
|
||||||
|
return ($error != NULL ? $error->code : NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the last XML-RPC client error message.
|
||||||
|
*/
|
||||||
|
function xmlrpc_error_msg() {
|
||||||
|
$error = xmlrpc_error();
|
||||||
|
return ($error != NULL ? $error->message : NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears any previously-saved errors.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_error()
|
||||||
|
*/
|
||||||
|
function xmlrpc_clear_error() {
|
||||||
|
xmlrpc_error(NULL, NULL, TRUE);
|
||||||
|
}
|
394
includes/xmlrpcs.inc
Normal file
394
includes/xmlrpcs.inc
Normal file
|
@ -0,0 +1,394 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Provides API for defining and handling XML-RPC requests.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes XML-RPC methods on this server.
|
||||||
|
*
|
||||||
|
* @param array $callbacks
|
||||||
|
* Either an associative array of external XML-RPC method names as keys with
|
||||||
|
* the callbacks they map to as values, or a more complex structure
|
||||||
|
* describing XML-RPC callbacks as returned from hook_xmlrpc().
|
||||||
|
*/
|
||||||
|
function xmlrpc_server($callbacks) {
|
||||||
|
$xmlrpc_server = new stdClass();
|
||||||
|
// Define built-in XML-RPC method names
|
||||||
|
$defaults = array(
|
||||||
|
'system.multicall' => 'xmlrpc_server_multicall',
|
||||||
|
array(
|
||||||
|
'system.methodSignature',
|
||||||
|
'xmlrpc_server_method_signature',
|
||||||
|
array('array', 'string'),
|
||||||
|
'Returns an array describing the return type and required parameters of a method.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'system.getCapabilities',
|
||||||
|
'xmlrpc_server_get_capabilities',
|
||||||
|
array('struct'),
|
||||||
|
'Returns a struct describing the XML-RPC specifications supported by this server.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'system.listMethods',
|
||||||
|
'xmlrpc_server_list_methods',
|
||||||
|
array('array'),
|
||||||
|
'Returns an array of available methods on this server.',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'system.methodHelp',
|
||||||
|
'xmlrpc_server_method_help',
|
||||||
|
array('string', 'string'),
|
||||||
|
'Returns a documentation string for the specified method.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// We build an array of all method names by combining the built-ins
|
||||||
|
// with those defined by modules implementing the _xmlrpc hook.
|
||||||
|
// Built-in methods are overridable.
|
||||||
|
$callbacks = array_merge($defaults, (array) $callbacks);
|
||||||
|
drupal_alter('xmlrpc', $callbacks);
|
||||||
|
foreach ($callbacks as $key => $callback) {
|
||||||
|
// we could check for is_array($callback)
|
||||||
|
if (is_int($key)) {
|
||||||
|
$method = $callback[0];
|
||||||
|
$xmlrpc_server->callbacks[$method] = $callback[1];
|
||||||
|
$xmlrpc_server->signatures[$method] = $callback[2];
|
||||||
|
$xmlrpc_server->help[$method] = $callback[3];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$xmlrpc_server->callbacks[$key] = $callback;
|
||||||
|
$xmlrpc_server->signatures[$key] = '';
|
||||||
|
$xmlrpc_server->help[$key] = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = file_get_contents('php://input');
|
||||||
|
if (!$data) {
|
||||||
|
print 'XML-RPC server accepts POST requests only.';
|
||||||
|
drupal_exit();
|
||||||
|
}
|
||||||
|
$xmlrpc_server->message = xmlrpc_message($data);
|
||||||
|
if (!xmlrpc_message_parse($xmlrpc_server->message)) {
|
||||||
|
xmlrpc_server_error(-32700, t('Parse error. Request not well formed.'));
|
||||||
|
}
|
||||||
|
if ($xmlrpc_server->message->messagetype != 'methodCall') {
|
||||||
|
xmlrpc_server_error(-32600, t('Server error. Invalid XML-RPC. Request must be a methodCall.'));
|
||||||
|
}
|
||||||
|
if (!isset($xmlrpc_server->message->params)) {
|
||||||
|
$xmlrpc_server->message->params = array();
|
||||||
|
}
|
||||||
|
xmlrpc_server_set($xmlrpc_server);
|
||||||
|
$result = xmlrpc_server_call($xmlrpc_server, $xmlrpc_server->message->methodname, $xmlrpc_server->message->params);
|
||||||
|
|
||||||
|
if (is_object($result) && !empty($result->is_error)) {
|
||||||
|
xmlrpc_server_error($result);
|
||||||
|
}
|
||||||
|
// Encode the result
|
||||||
|
$r = xmlrpc_value($result);
|
||||||
|
// Create the XML
|
||||||
|
$xml = '
|
||||||
|
<methodResponse>
|
||||||
|
<params>
|
||||||
|
<param>
|
||||||
|
<value>' . xmlrpc_value_get_xml($r) . '</value>
|
||||||
|
</param>
|
||||||
|
</params>
|
||||||
|
</methodResponse>
|
||||||
|
|
||||||
|
';
|
||||||
|
// Send it
|
||||||
|
xmlrpc_server_output($xml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws an XML-RPC error.
|
||||||
|
*
|
||||||
|
* @param $error
|
||||||
|
* An error object or integer error code.
|
||||||
|
* @param $message
|
||||||
|
* (optional) The description of the error. Used only if an integer error
|
||||||
|
* code was passed in.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_error($error, $message = FALSE) {
|
||||||
|
if ($message && !is_object($error)) {
|
||||||
|
$error = xmlrpc_error($error, $message);
|
||||||
|
}
|
||||||
|
xmlrpc_server_output(xmlrpc_error_get_xml($error));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends XML-RPC output to the browser.
|
||||||
|
*
|
||||||
|
* @param string $xml
|
||||||
|
* XML to send to the browser.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_output($xml) {
|
||||||
|
$xml = '<?xml version="1.0"?>' . "\n" . $xml;
|
||||||
|
drupal_add_http_header('Content-Length', strlen($xml));
|
||||||
|
drupal_add_http_header('Content-Type', 'text/xml');
|
||||||
|
echo $xml;
|
||||||
|
drupal_exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores a copy of an XML-RPC request temporarily.
|
||||||
|
*
|
||||||
|
* @param object $xmlrpc_server
|
||||||
|
* (optional) Request object created by xmlrpc_server(). Omit to leave the
|
||||||
|
* previous server object saved.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The latest stored request.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_server_get()
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_set($xmlrpc_server = NULL) {
|
||||||
|
static $server;
|
||||||
|
if (!isset($server)) {
|
||||||
|
$server = $xmlrpc_server;
|
||||||
|
}
|
||||||
|
return $server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the latest stored XML-RPC request.
|
||||||
|
*
|
||||||
|
* @return object
|
||||||
|
* The stored request.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_server_set()
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_get() {
|
||||||
|
return xmlrpc_server_set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an XML-RPC request and any parameters to the appropriate handler.
|
||||||
|
*
|
||||||
|
* @param object $xmlrpc_server
|
||||||
|
* Object containing information about this XML-RPC server, the methods it
|
||||||
|
* provides, their signatures, etc.
|
||||||
|
* @param string $methodname
|
||||||
|
* The external XML-RPC method name; e.g., 'system.methodHelp'.
|
||||||
|
* @param array $args
|
||||||
|
* Array containing any parameters that are to be sent along with the request.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The results of the call.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_call($xmlrpc_server, $methodname, $args) {
|
||||||
|
// Make sure parameters are in an array
|
||||||
|
if ($args && !is_array($args)) {
|
||||||
|
$args = array($args);
|
||||||
|
}
|
||||||
|
// Has this method been mapped to a Drupal function by us or by modules?
|
||||||
|
if (!isset($xmlrpc_server->callbacks[$methodname])) {
|
||||||
|
return xmlrpc_error(-32601, t('Server error. Requested method @methodname not specified.', array("@methodname" => $xmlrpc_server->message->methodname)));
|
||||||
|
}
|
||||||
|
$method = $xmlrpc_server->callbacks[$methodname];
|
||||||
|
$signature = $xmlrpc_server->signatures[$methodname];
|
||||||
|
|
||||||
|
// If the method has a signature, validate the request against the signature
|
||||||
|
if (is_array($signature)) {
|
||||||
|
$ok = TRUE;
|
||||||
|
$return_type = array_shift($signature);
|
||||||
|
// Check the number of arguments
|
||||||
|
if (count($args) != count($signature)) {
|
||||||
|
return xmlrpc_error(-32602, t('Server error. Wrong number of method parameters.'));
|
||||||
|
}
|
||||||
|
// Check the argument types
|
||||||
|
foreach ($signature as $key => $type) {
|
||||||
|
$arg = $args[$key];
|
||||||
|
switch ($type) {
|
||||||
|
case 'int':
|
||||||
|
case 'i4':
|
||||||
|
if (is_array($arg) || !is_int($arg)) {
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'base64':
|
||||||
|
case 'string':
|
||||||
|
if (!is_string($arg)) {
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'boolean':
|
||||||
|
if ($arg !== FALSE && $arg !== TRUE) {
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'float':
|
||||||
|
case 'double':
|
||||||
|
if (!is_float($arg)) {
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'date':
|
||||||
|
case 'dateTime.iso8601':
|
||||||
|
if (!$arg->is_date) {
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!$ok) {
|
||||||
|
return xmlrpc_error(-32602, t('Server error. Invalid method parameters.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists($method)) {
|
||||||
|
return xmlrpc_error(-32601, t('Server error. Requested function @method does not exist.', array("@method" => $method)));
|
||||||
|
}
|
||||||
|
// Call the mapped function
|
||||||
|
return call_user_func_array($method, $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches multiple XML-RPC requests.
|
||||||
|
*
|
||||||
|
* @param array $methodcalls
|
||||||
|
* An array of XML-RPC requests to make. Each request is an array with the
|
||||||
|
* following elements:
|
||||||
|
* - methodName: Name of the method to invoke.
|
||||||
|
* - params: Parameters to pass to the method.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* An array of the results of each request.
|
||||||
|
*
|
||||||
|
* @see xmlrpc_server_call()
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_multicall($methodcalls) {
|
||||||
|
// See http://www.xmlrpc.com/discuss/msgReader$1208
|
||||||
|
// To avoid multicall expansion attacks, limit the number of duplicate method
|
||||||
|
// calls allowed with a default of 1. Set to -1 for unlimited.
|
||||||
|
$duplicate_method_limit = variable_get('xmlrpc_multicall_duplicate_method_limit', 1);
|
||||||
|
$method_count = array();
|
||||||
|
$return = array();
|
||||||
|
$xmlrpc_server = xmlrpc_server_get();
|
||||||
|
foreach ($methodcalls as $call) {
|
||||||
|
$ok = TRUE;
|
||||||
|
if (!isset($call['methodName']) || !isset($call['params'])) {
|
||||||
|
$result = xmlrpc_error(3, t('Invalid syntax for system.multicall.'));
|
||||||
|
$ok = FALSE;
|
||||||
|
}
|
||||||
|
$method = $call['methodName'];
|
||||||
|
$method_count[$method] = isset($method_count[$method]) ? $method_count[$method] + 1 : 1;
|
||||||
|
$params = $call['params'];
|
||||||
|
if ($method == 'system.multicall') {
|
||||||
|
$result = xmlrpc_error(-32600, t('Recursive calls to system.multicall are forbidden.'));
|
||||||
|
}
|
||||||
|
elseif ($duplicate_method_limit > 0 && $method_count[$method] > $duplicate_method_limit) {
|
||||||
|
$result = xmlrpc_error(-156579, t('Too many duplicate method calls in system.multicall.'));
|
||||||
|
}
|
||||||
|
elseif ($ok) {
|
||||||
|
$result = xmlrpc_server_call($xmlrpc_server, $method, $params);
|
||||||
|
}
|
||||||
|
if (is_object($result) && !empty($result->is_error)) {
|
||||||
|
$return[] = array(
|
||||||
|
'faultCode' => $result->code,
|
||||||
|
'faultString' => $result->message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$return[] = array($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the methods available on this XML-RPC server.
|
||||||
|
*
|
||||||
|
* XML-RPC method system.listMethods maps to this function.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Array of the names of methods available on this server.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_list_methods() {
|
||||||
|
$xmlrpc_server = xmlrpc_server_get();
|
||||||
|
return array_keys($xmlrpc_server->callbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of the capabilities of this server.
|
||||||
|
*
|
||||||
|
* XML-RPC method system.getCapabilities maps to this function.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* Array of server capabilities.
|
||||||
|
*
|
||||||
|
* @see http://groups.yahoo.com/group/xml-rpc/message/2897
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_get_capabilities() {
|
||||||
|
return array(
|
||||||
|
'xmlrpc' => array(
|
||||||
|
'specUrl' => 'http://www.xmlrpc.com/spec',
|
||||||
|
'specVersion' => 1,
|
||||||
|
),
|
||||||
|
'faults_interop' => array(
|
||||||
|
'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php',
|
||||||
|
'specVersion' => 20010516,
|
||||||
|
),
|
||||||
|
'system.multicall' => array(
|
||||||
|
'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208',
|
||||||
|
'specVersion' => 1,
|
||||||
|
),
|
||||||
|
'introspection' => array(
|
||||||
|
'specUrl' => 'http://scripts.incutio.com/xmlrpc/introspection.html',
|
||||||
|
'specVersion' => 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns one method signature for a function.
|
||||||
|
*
|
||||||
|
* This is the function mapped to the XML-RPC method system.methodSignature.
|
||||||
|
*
|
||||||
|
* A method signature is an array of the input and output types of a method. For
|
||||||
|
* instance, the method signature of this function is array('array', 'string'),
|
||||||
|
* because it takes an array and returns a string.
|
||||||
|
*
|
||||||
|
* @param string $methodname
|
||||||
|
* Name of method to return a method signature for.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
* An array of arrays of types, each of the arrays representing one method
|
||||||
|
* signature of the function that $methodname maps to.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_method_signature($methodname) {
|
||||||
|
$xmlrpc_server = xmlrpc_server_get();
|
||||||
|
if (!isset($xmlrpc_server->callbacks[$methodname])) {
|
||||||
|
return xmlrpc_error(-32601, t('Server error. Requested method @methodname not specified.', array("@methodname" => $methodname)));
|
||||||
|
}
|
||||||
|
if (!is_array($xmlrpc_server->signatures[$methodname])) {
|
||||||
|
return xmlrpc_error(-32601, t('Server error. Requested method @methodname signature not specified.', array("@methodname" => $methodname)));
|
||||||
|
}
|
||||||
|
// We array of types
|
||||||
|
$return = array();
|
||||||
|
foreach ($xmlrpc_server->signatures[$methodname] as $type) {
|
||||||
|
$return[] = $type;
|
||||||
|
}
|
||||||
|
return array($return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the help for an XML-RPC method.
|
||||||
|
*
|
||||||
|
* XML-RPC method system.methodHelp maps to this function.
|
||||||
|
*
|
||||||
|
* @param string $method
|
||||||
|
* Name of method for which we return a help string.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
* Help text for $method.
|
||||||
|
*/
|
||||||
|
function xmlrpc_server_method_help($method) {
|
||||||
|
$xmlrpc_server = xmlrpc_server_get();
|
||||||
|
return $xmlrpc_server->help[$method];
|
||||||
|
}
|
21
index.php
Normal file
21
index.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* The PHP page that serves all page requests on a Drupal installation.
|
||||||
|
*
|
||||||
|
* The routines here dispatch control to the appropriate handler, which then
|
||||||
|
* prints the appropriate page.
|
||||||
|
*
|
||||||
|
* All Drupal code is released under the GNU General Public License.
|
||||||
|
* See COPYRIGHT.txt and LICENSE.txt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root directory of Drupal installation.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_ROOT', getcwd());
|
||||||
|
|
||||||
|
require_once DRUPAL_ROOT . '/includes/bootstrap.inc';
|
||||||
|
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
|
||||||
|
menu_execute_active_handler();
|
26
install.php
Normal file
26
install.php
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Initiates a browser-based installation of Drupal.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the root directory of the Drupal installation.
|
||||||
|
*/
|
||||||
|
define('DRUPAL_ROOT', getcwd());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global flag to indicate the site is in installation mode.
|
||||||
|
*/
|
||||||
|
define('MAINTENANCE_MODE', 'install');
|
||||||
|
|
||||||
|
// Exit early if running an incompatible PHP version to avoid fatal errors.
|
||||||
|
if (version_compare(PHP_VERSION, '5.2.4') < 0) {
|
||||||
|
print 'Your PHP installation is too old. Drupal requires at least PHP 5.2.4. See the <a href="http://drupal.org/requirements">system requirements</a> page for more information.';
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the installer.
|
||||||
|
require_once DRUPAL_ROOT . '/includes/install.core.inc';
|
||||||
|
install_drupal();
|
677
misc/ajax.js
Normal file
677
misc/ajax.js
Normal file
|
@ -0,0 +1,677 @@
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides Ajax page updating via jQuery $.ajax (Asynchronous JavaScript and XML).
|
||||||
|
*
|
||||||
|
* Ajax is a method of making a request via JavaScript while viewing an HTML
|
||||||
|
* page. The request returns an array of commands encoded in JSON, which is
|
||||||
|
* then executed to make any changes that are necessary to the page.
|
||||||
|
*
|
||||||
|
* Drupal uses this file to enhance form elements with #ajax['path'] and
|
||||||
|
* #ajax['wrapper'] properties. If set, this file will automatically be included
|
||||||
|
* to provide Ajax capabilities.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Drupal.ajax = Drupal.ajax || {};
|
||||||
|
|
||||||
|
Drupal.settings.urlIsAjaxTrusted = Drupal.settings.urlIsAjaxTrusted || {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the Ajax behavior to each Ajax form element.
|
||||||
|
*/
|
||||||
|
Drupal.behaviors.AJAX = {
|
||||||
|
attach: function (context, settings) {
|
||||||
|
// Load all Ajax behaviors specified in the settings.
|
||||||
|
for (var base in settings.ajax) {
|
||||||
|
if (!$('#' + base + '.ajax-processed').length) {
|
||||||
|
var element_settings = settings.ajax[base];
|
||||||
|
|
||||||
|
if (typeof element_settings.selector == 'undefined') {
|
||||||
|
element_settings.selector = '#' + base;
|
||||||
|
}
|
||||||
|
$(element_settings.selector).each(function () {
|
||||||
|
element_settings.element = this;
|
||||||
|
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#' + base).addClass('ajax-processed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind Ajax behaviors to all items showing the class.
|
||||||
|
$('.use-ajax:not(.ajax-processed)').addClass('ajax-processed').each(function () {
|
||||||
|
var element_settings = {};
|
||||||
|
// Clicked links look better with the throbber than the progress bar.
|
||||||
|
element_settings.progress = { 'type': 'throbber' };
|
||||||
|
|
||||||
|
// For anchor tags, these will go to the target of the anchor rather
|
||||||
|
// than the usual location.
|
||||||
|
if ($(this).attr('href')) {
|
||||||
|
element_settings.url = $(this).attr('href');
|
||||||
|
element_settings.event = 'click';
|
||||||
|
}
|
||||||
|
var base = $(this).attr('id');
|
||||||
|
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This class means to submit the form to the action using Ajax.
|
||||||
|
$('.use-ajax-submit:not(.ajax-processed)').addClass('ajax-processed').each(function () {
|
||||||
|
var element_settings = {};
|
||||||
|
|
||||||
|
// Ajax submits specified in this manner automatically submit to the
|
||||||
|
// normal form action.
|
||||||
|
element_settings.url = $(this.form).attr('action');
|
||||||
|
// Form submit button clicks need to tell the form what was clicked so
|
||||||
|
// it gets passed in the POST request.
|
||||||
|
element_settings.setClick = true;
|
||||||
|
// Form buttons use the 'click' event rather than mousedown.
|
||||||
|
element_settings.event = 'click';
|
||||||
|
// Clicked form buttons look better with the throbber than the progress bar.
|
||||||
|
element_settings.progress = { 'type': 'throbber' };
|
||||||
|
|
||||||
|
var base = $(this).attr('id');
|
||||||
|
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajax object.
|
||||||
|
*
|
||||||
|
* All Ajax objects on a page are accessible through the global Drupal.ajax
|
||||||
|
* object and are keyed by the submit button's ID. You can access them from
|
||||||
|
* your module's JavaScript file to override properties or functions.
|
||||||
|
*
|
||||||
|
* For example, if your Ajax enabled button has the ID 'edit-submit', you can
|
||||||
|
* redefine the function that is called to insert the new content like this
|
||||||
|
* (inside a Drupal.behaviors attach block):
|
||||||
|
* @code
|
||||||
|
* Drupal.behaviors.myCustomAJAXStuff = {
|
||||||
|
* attach: function (context, settings) {
|
||||||
|
* Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) {
|
||||||
|
* new_content = $(response.data);
|
||||||
|
* $('#my-wrapper').append(new_content);
|
||||||
|
* alert('New content was appended to #my-wrapper');
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
Drupal.ajax = function (base, element, element_settings) {
|
||||||
|
var defaults = {
|
||||||
|
url: 'system/ajax',
|
||||||
|
event: 'mousedown',
|
||||||
|
keypress: true,
|
||||||
|
selector: '#' + base,
|
||||||
|
effect: 'none',
|
||||||
|
speed: 'none',
|
||||||
|
method: 'replaceWith',
|
||||||
|
progress: {
|
||||||
|
type: 'throbber',
|
||||||
|
message: Drupal.t('Please wait...')
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
'js': true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.extend(this, defaults, element_settings);
|
||||||
|
|
||||||
|
this.element = element;
|
||||||
|
this.element_settings = element_settings;
|
||||||
|
|
||||||
|
// Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
|
||||||
|
// the server detect when it needs to degrade gracefully.
|
||||||
|
// There are five scenarios to check for:
|
||||||
|
// 1. /nojs/
|
||||||
|
// 2. /nojs$ - The end of a URL string.
|
||||||
|
// 3. /nojs? - Followed by a query (with clean URLs enabled).
|
||||||
|
// E.g.: path/nojs?destination=foobar
|
||||||
|
// 4. /nojs& - Followed by a query (without clean URLs enabled).
|
||||||
|
// E.g.: ?q=path/nojs&destination=foobar
|
||||||
|
// 5. /nojs# - Followed by a fragment.
|
||||||
|
// E.g.: path/nojs#myfragment
|
||||||
|
this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
|
||||||
|
// If the 'nojs' version of the URL is trusted, also trust the 'ajax' version.
|
||||||
|
if (Drupal.settings.urlIsAjaxTrusted[element_settings.url]) {
|
||||||
|
Drupal.settings.urlIsAjaxTrusted[this.url] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.wrapper = '#' + element_settings.wrapper;
|
||||||
|
|
||||||
|
// If there isn't a form, jQuery.ajax() will be used instead, allowing us to
|
||||||
|
// bind Ajax to links as well.
|
||||||
|
if (this.element.form) {
|
||||||
|
this.form = $(this.element.form);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the options for the ajaxSubmit function.
|
||||||
|
// The 'this' variable will not persist inside of the options object.
|
||||||
|
var ajax = this;
|
||||||
|
ajax.options = {
|
||||||
|
url: ajax.url,
|
||||||
|
data: ajax.submit,
|
||||||
|
beforeSerialize: function (element_settings, options) {
|
||||||
|
return ajax.beforeSerialize(element_settings, options);
|
||||||
|
},
|
||||||
|
beforeSubmit: function (form_values, element_settings, options) {
|
||||||
|
ajax.ajaxing = true;
|
||||||
|
return ajax.beforeSubmit(form_values, element_settings, options);
|
||||||
|
},
|
||||||
|
beforeSend: function (xmlhttprequest, options) {
|
||||||
|
ajax.ajaxing = true;
|
||||||
|
return ajax.beforeSend(xmlhttprequest, options);
|
||||||
|
},
|
||||||
|
success: function (response, status, xmlhttprequest) {
|
||||||
|
// Sanity check for browser support (object expected).
|
||||||
|
// When using iFrame uploads, responses must be returned as a string.
|
||||||
|
if (typeof response == 'string') {
|
||||||
|
response = $.parseJSON(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prior to invoking the response's commands, verify that they can be
|
||||||
|
// trusted by checking for a response header. See
|
||||||
|
// ajax_set_verification_header() for details.
|
||||||
|
// - Empty responses are harmless so can bypass verification. This avoids
|
||||||
|
// an alert message for server-generated no-op responses that skip Ajax
|
||||||
|
// rendering.
|
||||||
|
// - Ajax objects with trusted URLs (e.g., ones defined server-side via
|
||||||
|
// #ajax) can bypass header verification. This is especially useful for
|
||||||
|
// Ajax with multipart forms. Because IFRAME transport is used, the
|
||||||
|
// response headers cannot be accessed for verification.
|
||||||
|
if (response !== null && !Drupal.settings.urlIsAjaxTrusted[ajax.url]) {
|
||||||
|
if (xmlhttprequest.getResponseHeader('X-Drupal-Ajax-Token') !== '1') {
|
||||||
|
var customMessage = Drupal.t("The response failed verification so will not be processed.");
|
||||||
|
return ajax.error(xmlhttprequest, ajax.url, customMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ajax.success(response, status);
|
||||||
|
},
|
||||||
|
complete: function (xmlhttprequest, status) {
|
||||||
|
ajax.ajaxing = false;
|
||||||
|
if (status == 'error' || status == 'parsererror') {
|
||||||
|
return ajax.error(xmlhttprequest, ajax.url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
type: 'POST'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bind the ajaxSubmit function to the element event.
|
||||||
|
$(ajax.element).bind(element_settings.event, function (event) {
|
||||||
|
if (!Drupal.settings.urlIsAjaxTrusted[ajax.url] && !Drupal.urlIsLocal(ajax.url)) {
|
||||||
|
throw new Error(Drupal.t('The callback URL is not local and not trusted: !url', {'!url': ajax.url}));
|
||||||
|
}
|
||||||
|
return ajax.eventResponse(this, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If necessary, enable keyboard submission so that Ajax behaviors
|
||||||
|
// can be triggered through keyboard input as well as e.g. a mousedown
|
||||||
|
// action.
|
||||||
|
if (element_settings.keypress) {
|
||||||
|
$(ajax.element).keypress(function (event) {
|
||||||
|
return ajax.keypressResponse(this, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If necessary, prevent the browser default action of an additional event.
|
||||||
|
// For example, prevent the browser default action of a click, even if the
|
||||||
|
// AJAX behavior binds to mousedown.
|
||||||
|
if (element_settings.prevent) {
|
||||||
|
$(ajax.element).bind(element_settings.prevent, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle a key press.
|
||||||
|
*
|
||||||
|
* The Ajax object will, if instructed, bind to a key press response. This
|
||||||
|
* will test to see if the key press is valid to trigger this event and
|
||||||
|
* if it is, trigger it for us and prevent other keypresses from triggering.
|
||||||
|
* In this case we're handling RETURN and SPACEBAR keypresses (event codes 13
|
||||||
|
* and 32. RETURN is often used to submit a form when in a textfield, and
|
||||||
|
* SPACE is often used to activate an element without submitting.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.keypressResponse = function (element, event) {
|
||||||
|
// Create a synonym for this to reduce code confusion.
|
||||||
|
var ajax = this;
|
||||||
|
|
||||||
|
// Detect enter key and space bar and allow the standard response for them,
|
||||||
|
// except for form elements of type 'text' and 'textarea', where the
|
||||||
|
// spacebar activation causes inappropriate activation if #ajax['keypress'] is
|
||||||
|
// TRUE. On a text-type widget a space should always be a space.
|
||||||
|
if (event.which == 13 || (event.which == 32 && element.type != 'text' && element.type != 'textarea')) {
|
||||||
|
$(ajax.element_settings.element).trigger(ajax.element_settings.event);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an event that triggers an Ajax response.
|
||||||
|
*
|
||||||
|
* When an event that triggers an Ajax response happens, this method will
|
||||||
|
* perform the actual Ajax call. It is bound to the event using
|
||||||
|
* bind() in the constructor, and it uses the options specified on the
|
||||||
|
* ajax object.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.eventResponse = function (element, event) {
|
||||||
|
// Create a synonym for this to reduce code confusion.
|
||||||
|
var ajax = this;
|
||||||
|
|
||||||
|
// Do not perform another ajax command if one is already in progress.
|
||||||
|
if (ajax.ajaxing) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ajax.form) {
|
||||||
|
// If setClick is set, we must set this to ensure that the button's
|
||||||
|
// value is passed.
|
||||||
|
if (ajax.setClick) {
|
||||||
|
// Mark the clicked button. 'form.clk' is a special variable for
|
||||||
|
// ajaxSubmit that tells the system which element got clicked to
|
||||||
|
// trigger the submit. Without it there would be no 'op' or
|
||||||
|
// equivalent.
|
||||||
|
element.form.clk = element;
|
||||||
|
}
|
||||||
|
|
||||||
|
ajax.form.ajaxSubmit(ajax.options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ajax.beforeSerialize(ajax.element, ajax.options);
|
||||||
|
$.ajax(ajax.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// Unset the ajax.ajaxing flag here because it won't be unset during
|
||||||
|
// the complete response.
|
||||||
|
ajax.ajaxing = false;
|
||||||
|
alert("An error occurred while attempting to process " + ajax.options.url + ": " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For radio/checkbox, allow the default event. On IE, this means letting
|
||||||
|
// it actually check the box.
|
||||||
|
if (typeof element.type != 'undefined' && (element.type == 'checkbox' || element.type == 'radio')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the form serialization.
|
||||||
|
*
|
||||||
|
* Runs before the beforeSend() handler (see below), and unlike that one, runs
|
||||||
|
* before field data is collected.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.beforeSerialize = function (element, options) {
|
||||||
|
// Allow detaching behaviors to update field values before collecting them.
|
||||||
|
// This is only needed when field values are added to the POST data, so only
|
||||||
|
// when there is a form such that this.form.ajaxSubmit() is used instead of
|
||||||
|
// $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
|
||||||
|
// isn't called, but don't rely on that: explicitly check this.form.
|
||||||
|
if (this.form) {
|
||||||
|
var settings = this.settings || Drupal.settings;
|
||||||
|
Drupal.detachBehaviors(this.form, settings, 'serialize');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate HTML ids in the returned markup.
|
||||||
|
// @see drupal_html_id()
|
||||||
|
options.data['ajax_html_ids[]'] = [];
|
||||||
|
$('[id]').each(function () {
|
||||||
|
options.data['ajax_html_ids[]'].push(this.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Allow Drupal to return new JavaScript and CSS files to load without
|
||||||
|
// returning the ones already loaded.
|
||||||
|
// @see ajax_base_page_theme()
|
||||||
|
// @see drupal_get_css()
|
||||||
|
// @see drupal_get_js()
|
||||||
|
options.data['ajax_page_state[theme]'] = Drupal.settings.ajaxPageState.theme;
|
||||||
|
options.data['ajax_page_state[theme_token]'] = Drupal.settings.ajaxPageState.theme_token;
|
||||||
|
for (var key in Drupal.settings.ajaxPageState.css) {
|
||||||
|
options.data['ajax_page_state[css][' + key + ']'] = 1;
|
||||||
|
}
|
||||||
|
for (var key in Drupal.settings.ajaxPageState.js) {
|
||||||
|
options.data['ajax_page_state[js][' + key + ']'] = 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify form values prior to form submission.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
|
||||||
|
// This function is left empty to make it simple to override for modules
|
||||||
|
// that wish to add functionality here.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare the Ajax request before it is sent.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
|
||||||
|
// For forms without file inputs, the jQuery Form plugin serializes the form
|
||||||
|
// values, and then calls jQuery's $.ajax() function, which invokes this
|
||||||
|
// handler. In this circumstance, options.extraData is never used. For forms
|
||||||
|
// with file inputs, the jQuery Form plugin uses the browser's normal form
|
||||||
|
// submission mechanism, but captures the response in a hidden IFRAME. In this
|
||||||
|
// circumstance, it calls this handler first, and then appends hidden fields
|
||||||
|
// to the form to submit the values in options.extraData. There is no simple
|
||||||
|
// way to know which submission mechanism will be used, so we add to extraData
|
||||||
|
// regardless, and allow it to be ignored in the former case.
|
||||||
|
if (this.form) {
|
||||||
|
options.extraData = options.extraData || {};
|
||||||
|
|
||||||
|
// Let the server know when the IFRAME submission mechanism is used. The
|
||||||
|
// server can use this information to wrap the JSON response in a TEXTAREA,
|
||||||
|
// as per http://jquery.malsup.com/form/#file-upload.
|
||||||
|
options.extraData.ajax_iframe_upload = '1';
|
||||||
|
|
||||||
|
// The triggering element is about to be disabled (see below), but if it
|
||||||
|
// contains a value (e.g., a checkbox, textfield, select, etc.), ensure that
|
||||||
|
// value is included in the submission. As per above, submissions that use
|
||||||
|
// $.ajax() are already serialized prior to the element being disabled, so
|
||||||
|
// this is only needed for IFRAME submissions.
|
||||||
|
var v = $.fieldValue(this.element);
|
||||||
|
if (v !== null) {
|
||||||
|
options.extraData[this.element.name] = Drupal.checkPlain(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable the element that received the change to prevent user interface
|
||||||
|
// interaction while the Ajax request is in progress. ajax.ajaxing prevents
|
||||||
|
// the element from triggering a new request, but does not prevent the user
|
||||||
|
// from changing its value.
|
||||||
|
$(this.element).addClass('progress-disabled').attr('disabled', true);
|
||||||
|
|
||||||
|
// Insert progressbar or throbber.
|
||||||
|
if (this.progress.type == 'bar') {
|
||||||
|
var progressBar = new Drupal.progressBar('ajax-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
|
||||||
|
if (this.progress.message) {
|
||||||
|
progressBar.setProgress(-1, this.progress.message);
|
||||||
|
}
|
||||||
|
if (this.progress.url) {
|
||||||
|
progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
|
||||||
|
}
|
||||||
|
this.progress.element = $(progressBar.element).addClass('ajax-progress ajax-progress-bar');
|
||||||
|
this.progress.object = progressBar;
|
||||||
|
$(this.element).after(this.progress.element);
|
||||||
|
}
|
||||||
|
else if (this.progress.type == 'throbber') {
|
||||||
|
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber"> </div></div>');
|
||||||
|
if (this.progress.message) {
|
||||||
|
$('.throbber', this.progress.element).after('<div class="message">' + this.progress.message + '</div>');
|
||||||
|
}
|
||||||
|
$(this.element).after(this.progress.element);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the form redirection completion.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.success = function (response, status) {
|
||||||
|
// Remove the progress element.
|
||||||
|
if (this.progress.element) {
|
||||||
|
$(this.progress.element).remove();
|
||||||
|
}
|
||||||
|
if (this.progress.object) {
|
||||||
|
this.progress.object.stopMonitoring();
|
||||||
|
}
|
||||||
|
$(this.element).removeClass('progress-disabled').removeAttr('disabled');
|
||||||
|
|
||||||
|
Drupal.freezeHeight();
|
||||||
|
|
||||||
|
for (var i in response) {
|
||||||
|
if (response.hasOwnProperty(i) && response[i]['command'] && this.commands[response[i]['command']]) {
|
||||||
|
this.commands[response[i]['command']](this, response[i], status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reattach behaviors, if they were detached in beforeSerialize(). The
|
||||||
|
// attachBehaviors() called on the new content from processing the response
|
||||||
|
// commands is not sufficient, because behaviors from the entire form need
|
||||||
|
// to be reattached.
|
||||||
|
if (this.form) {
|
||||||
|
var settings = this.settings || Drupal.settings;
|
||||||
|
Drupal.attachBehaviors(this.form, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
Drupal.unfreezeHeight();
|
||||||
|
|
||||||
|
// Remove any response-specific settings so they don't get used on the next
|
||||||
|
// call by mistake.
|
||||||
|
this.settings = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an effect object which tells us how to apply the effect when adding new HTML.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.getEffect = function (response) {
|
||||||
|
var type = response.effect || this.effect;
|
||||||
|
var speed = response.speed || this.speed;
|
||||||
|
|
||||||
|
var effect = {};
|
||||||
|
if (type == 'none') {
|
||||||
|
effect.showEffect = 'show';
|
||||||
|
effect.hideEffect = 'hide';
|
||||||
|
effect.showSpeed = '';
|
||||||
|
}
|
||||||
|
else if (type == 'fade') {
|
||||||
|
effect.showEffect = 'fadeIn';
|
||||||
|
effect.hideEffect = 'fadeOut';
|
||||||
|
effect.showSpeed = speed;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
effect.showEffect = type + 'Toggle';
|
||||||
|
effect.hideEffect = type + 'Toggle';
|
||||||
|
effect.showSpeed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return effect;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the form redirection error.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.error = function (xmlhttprequest, uri, customMessage) {
|
||||||
|
Drupal.displayAjaxError(Drupal.ajaxError(xmlhttprequest, uri, customMessage));
|
||||||
|
// Remove the progress element.
|
||||||
|
if (this.progress.element) {
|
||||||
|
$(this.progress.element).remove();
|
||||||
|
}
|
||||||
|
if (this.progress.object) {
|
||||||
|
this.progress.object.stopMonitoring();
|
||||||
|
}
|
||||||
|
// Undo hide.
|
||||||
|
$(this.wrapper).show();
|
||||||
|
// Re-enable the element.
|
||||||
|
$(this.element).removeClass('progress-disabled').removeAttr('disabled');
|
||||||
|
// Reattach behaviors, if they were detached in beforeSerialize().
|
||||||
|
if (this.form) {
|
||||||
|
var settings = this.settings || Drupal.settings;
|
||||||
|
Drupal.attachBehaviors(this.form, settings);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide a series of commands that the server can request the client perform.
|
||||||
|
*/
|
||||||
|
Drupal.ajax.prototype.commands = {
|
||||||
|
/**
|
||||||
|
* Command to insert new content into the DOM.
|
||||||
|
*/
|
||||||
|
insert: function (ajax, response, status) {
|
||||||
|
// Get information from the response. If it is not there, default to
|
||||||
|
// our presets.
|
||||||
|
var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
|
||||||
|
var method = response.method || ajax.method;
|
||||||
|
var effect = ajax.getEffect(response);
|
||||||
|
|
||||||
|
// We don't know what response.data contains: it might be a string of text
|
||||||
|
// without HTML, so don't rely on jQuery correctly iterpreting
|
||||||
|
// $(response.data) as new HTML rather than a CSS selector. Also, if
|
||||||
|
// response.data contains top-level text nodes, they get lost with either
|
||||||
|
// $(response.data) or $('<div></div>').replaceWith(response.data).
|
||||||
|
var new_content_wrapped = $('<div></div>').html(response.data);
|
||||||
|
var new_content = new_content_wrapped.contents();
|
||||||
|
|
||||||
|
// For legacy reasons, the effects processing code assumes that new_content
|
||||||
|
// consists of a single top-level element. Also, it has not been
|
||||||
|
// sufficiently tested whether attachBehaviors() can be successfully called
|
||||||
|
// with a context object that includes top-level text nodes. However, to
|
||||||
|
// give developers full control of the HTML appearing in the page, and to
|
||||||
|
// enable Ajax content to be inserted in places where DIV elements are not
|
||||||
|
// allowed (e.g., within TABLE, TR, and SPAN parents), we check if the new
|
||||||
|
// content satisfies the requirement of a single top-level element, and
|
||||||
|
// only use the container DIV created above when it doesn't. For more
|
||||||
|
// information, please see http://drupal.org/node/736066.
|
||||||
|
if (new_content.length != 1 || new_content.get(0).nodeType != 1) {
|
||||||
|
new_content = new_content_wrapped;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If removing content from the wrapper, detach behaviors first.
|
||||||
|
switch (method) {
|
||||||
|
case 'html':
|
||||||
|
case 'replaceWith':
|
||||||
|
case 'replaceAll':
|
||||||
|
case 'empty':
|
||||||
|
case 'remove':
|
||||||
|
var settings = response.settings || ajax.settings || Drupal.settings;
|
||||||
|
Drupal.detachBehaviors(wrapper, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new content to the page.
|
||||||
|
wrapper[method](new_content);
|
||||||
|
|
||||||
|
// Immediately hide the new content if we're using any effects.
|
||||||
|
if (effect.showEffect != 'show') {
|
||||||
|
new_content.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine which effect to use and what content will receive the
|
||||||
|
// effect, then show the new content.
|
||||||
|
if ($('.ajax-new-content', new_content).length > 0) {
|
||||||
|
$('.ajax-new-content', new_content).hide();
|
||||||
|
new_content.show();
|
||||||
|
$('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed);
|
||||||
|
}
|
||||||
|
else if (effect.showEffect != 'show') {
|
||||||
|
new_content[effect.showEffect](effect.showSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach all JavaScript behaviors to the new content, if it was successfully
|
||||||
|
// added to the page, this if statement allows #ajax['wrapper'] to be
|
||||||
|
// optional.
|
||||||
|
if (new_content.parents('html').length > 0) {
|
||||||
|
// Apply any settings from the returned JSON if available.
|
||||||
|
var settings = response.settings || ajax.settings || Drupal.settings;
|
||||||
|
Drupal.attachBehaviors(new_content, settings);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to remove a chunk from the page.
|
||||||
|
*/
|
||||||
|
remove: function (ajax, response, status) {
|
||||||
|
var settings = response.settings || ajax.settings || Drupal.settings;
|
||||||
|
Drupal.detachBehaviors($(response.selector), settings);
|
||||||
|
$(response.selector).remove();
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to mark a chunk changed.
|
||||||
|
*/
|
||||||
|
changed: function (ajax, response, status) {
|
||||||
|
if (!$(response.selector).hasClass('ajax-changed')) {
|
||||||
|
$(response.selector).addClass('ajax-changed');
|
||||||
|
if (response.asterisk) {
|
||||||
|
$(response.selector).find(response.asterisk).append(' <span class="ajax-changed">*</span> ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to provide an alert.
|
||||||
|
*/
|
||||||
|
alert: function (ajax, response, status) {
|
||||||
|
alert(response.text, response.title);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to provide the jQuery css() function.
|
||||||
|
*/
|
||||||
|
css: function (ajax, response, status) {
|
||||||
|
$(response.selector).css(response.argument);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to set the settings that will be used for other commands in this response.
|
||||||
|
*/
|
||||||
|
settings: function (ajax, response, status) {
|
||||||
|
if (response.merge) {
|
||||||
|
$.extend(true, Drupal.settings, response.settings);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ajax.settings = response.settings;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to attach data using jQuery's data API.
|
||||||
|
*/
|
||||||
|
data: function (ajax, response, status) {
|
||||||
|
$(response.selector).data(response.name, response.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to apply a jQuery method.
|
||||||
|
*/
|
||||||
|
invoke: function (ajax, response, status) {
|
||||||
|
var $element = $(response.selector);
|
||||||
|
$element[response.method].apply($element, response.arguments);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to restripe a table.
|
||||||
|
*/
|
||||||
|
restripe: function (ajax, response, status) {
|
||||||
|
// :even and :odd are reversed because jQuery counts from 0 and
|
||||||
|
// we count from 1, so we're out of sync.
|
||||||
|
// Match immediate children of the parent element to allow nesting.
|
||||||
|
$('> tbody > tr:visible, > tr:visible', $(response.selector))
|
||||||
|
.removeClass('odd even')
|
||||||
|
.filter(':even').addClass('odd').end()
|
||||||
|
.filter(':odd').addClass('even');
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to add css.
|
||||||
|
*
|
||||||
|
* Uses the proprietary addImport method if available as browsers which
|
||||||
|
* support that method ignore @import statements in dynamically added
|
||||||
|
* stylesheets.
|
||||||
|
*/
|
||||||
|
add_css: function (ajax, response, status) {
|
||||||
|
// Add the styles in the normal way.
|
||||||
|
$('head').prepend(response.data);
|
||||||
|
// Add imports in the styles using the addImport method if available.
|
||||||
|
var match, importMatch = /^@import url\("(.*)"\);$/igm;
|
||||||
|
if (document.styleSheets[0].addImport && importMatch.test(response.data)) {
|
||||||
|
importMatch.lastIndex = 0;
|
||||||
|
while (match = importMatch.exec(response.data)) {
|
||||||
|
document.styleSheets[0].addImport(match[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command to update a form's build ID.
|
||||||
|
*/
|
||||||
|
updateBuildId: function(ajax, response, status) {
|
||||||
|
$('input[name="form_build_id"][value="' + response['old'] + '"]').val(response['new']);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
BIN
misc/arrow-asc.png
Normal file
BIN
misc/arrow-asc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 B |
BIN
misc/arrow-desc.png
Normal file
BIN
misc/arrow-desc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 118 B |
27
misc/authorize.js
Normal file
27
misc/authorize.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file
|
||||||
|
* Conditionally hide or show the appropriate settings and saved defaults
|
||||||
|
* on the file transfer connection settings form used by authorize.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
Drupal.behaviors.authorizeFileTransferForm = {
|
||||||
|
attach: function(context) {
|
||||||
|
$('#edit-connection-settings-authorize-filetransfer-default').change(function() {
|
||||||
|
$('.filetransfer').hide().filter('.filetransfer-' + $(this).val()).show();
|
||||||
|
});
|
||||||
|
$('.filetransfer').hide().filter('.filetransfer-' + $('#edit-connection-settings-authorize-filetransfer-default').val()).show();
|
||||||
|
|
||||||
|
// Removes the float on the select box (used for non-JS interface).
|
||||||
|
if ($('.connection-settings-update-filetransfer-default-wrapper').length > 0) {
|
||||||
|
$('.connection-settings-update-filetransfer-default-wrapper').css('float', 'none');
|
||||||
|
}
|
||||||
|
// Hides the submit button for non-js users.
|
||||||
|
$('#edit-submit-connection').hide();
|
||||||
|
$('#edit-submit-process').show();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
328
misc/autocomplete.js
Normal file
328
misc/autocomplete.js
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the autocomplete behavior to all required fields.
|
||||||
|
*/
|
||||||
|
Drupal.behaviors.autocomplete = {
|
||||||
|
attach: function (context, settings) {
|
||||||
|
var acdb = [];
|
||||||
|
$('input.autocomplete', context).once('autocomplete', function () {
|
||||||
|
var uri = this.value;
|
||||||
|
if (!acdb[uri]) {
|
||||||
|
acdb[uri] = new Drupal.ACDB(uri);
|
||||||
|
}
|
||||||
|
var $input = $('#' + this.id.substr(0, this.id.length - 13))
|
||||||
|
.attr('autocomplete', 'OFF')
|
||||||
|
.attr('aria-autocomplete', 'list');
|
||||||
|
$($input[0].form).submit(Drupal.autocompleteSubmit);
|
||||||
|
$input.parent()
|
||||||
|
.attr('role', 'application')
|
||||||
|
.append($('<span class="element-invisible" aria-live="assertive"></span>')
|
||||||
|
.attr('id', $input.attr('id') + '-autocomplete-aria-live')
|
||||||
|
);
|
||||||
|
new Drupal.jsAC($input, acdb[uri]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the form from submitting if the suggestions popup is open
|
||||||
|
* and closes the suggestions popup when doing so.
|
||||||
|
*/
|
||||||
|
Drupal.autocompleteSubmit = function () {
|
||||||
|
return $('#autocomplete').each(function () {
|
||||||
|
this.owner.hidePopup();
|
||||||
|
}).length == 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AutoComplete object.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC = function ($input, db) {
|
||||||
|
var ac = this;
|
||||||
|
this.input = $input[0];
|
||||||
|
this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
|
||||||
|
this.db = db;
|
||||||
|
|
||||||
|
$input
|
||||||
|
.keydown(function (event) { return ac.onkeydown(this, event); })
|
||||||
|
.keyup(function (event) { ac.onkeyup(this, event); })
|
||||||
|
.blur(function () { ac.hidePopup(); ac.db.cancel(); });
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the "keydown" event.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.onkeydown = function (input, e) {
|
||||||
|
if (!e) {
|
||||||
|
e = window.event;
|
||||||
|
}
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 40: // down arrow.
|
||||||
|
this.selectDown();
|
||||||
|
return false;
|
||||||
|
case 38: // up arrow.
|
||||||
|
this.selectUp();
|
||||||
|
return false;
|
||||||
|
default: // All other keys.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler for the "keyup" event.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.onkeyup = function (input, e) {
|
||||||
|
if (!e) {
|
||||||
|
e = window.event;
|
||||||
|
}
|
||||||
|
switch (e.keyCode) {
|
||||||
|
case 16: // Shift.
|
||||||
|
case 17: // Ctrl.
|
||||||
|
case 18: // Alt.
|
||||||
|
case 20: // Caps lock.
|
||||||
|
case 33: // Page up.
|
||||||
|
case 34: // Page down.
|
||||||
|
case 35: // End.
|
||||||
|
case 36: // Home.
|
||||||
|
case 37: // Left arrow.
|
||||||
|
case 38: // Up arrow.
|
||||||
|
case 39: // Right arrow.
|
||||||
|
case 40: // Down arrow.
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 9: // Tab.
|
||||||
|
case 13: // Enter.
|
||||||
|
case 27: // Esc.
|
||||||
|
this.hidePopup(e.keyCode);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default: // All other keys.
|
||||||
|
if (input.value.length > 0 && !input.readOnly) {
|
||||||
|
this.populatePopup();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.hidePopup(e.keyCode);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts the currently highlighted suggestion into the autocomplete field.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.select = function (node) {
|
||||||
|
this.input.value = $(node).data('autocompleteValue');
|
||||||
|
$(this.input).trigger('autocompleteSelect', [node]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the next suggestion.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.selectDown = function () {
|
||||||
|
if (this.selected && this.selected.nextSibling) {
|
||||||
|
this.highlight(this.selected.nextSibling);
|
||||||
|
}
|
||||||
|
else if (this.popup) {
|
||||||
|
var lis = $('li', this.popup);
|
||||||
|
if (lis.length > 0) {
|
||||||
|
this.highlight(lis.get(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the previous suggestion.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.selectUp = function () {
|
||||||
|
if (this.selected && this.selected.previousSibling) {
|
||||||
|
this.highlight(this.selected.previousSibling);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights a suggestion.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.highlight = function (node) {
|
||||||
|
if (this.selected) {
|
||||||
|
$(this.selected).removeClass('selected');
|
||||||
|
}
|
||||||
|
$(node).addClass('selected');
|
||||||
|
this.selected = node;
|
||||||
|
$(this.ariaLive).html($(this.selected).html());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unhighlights a suggestion.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.unhighlight = function (node) {
|
||||||
|
$(node).removeClass('selected');
|
||||||
|
this.selected = false;
|
||||||
|
$(this.ariaLive).empty();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the autocomplete suggestions.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.hidePopup = function (keycode) {
|
||||||
|
// Select item if the right key or mousebutton was pressed.
|
||||||
|
if (this.selected && ((keycode && keycode != 46 && keycode != 8 && keycode != 27) || !keycode)) {
|
||||||
|
this.select(this.selected);
|
||||||
|
}
|
||||||
|
// Hide popup.
|
||||||
|
var popup = this.popup;
|
||||||
|
if (popup) {
|
||||||
|
this.popup = null;
|
||||||
|
$(popup).fadeOut('fast', function () { $(popup).remove(); });
|
||||||
|
}
|
||||||
|
this.selected = false;
|
||||||
|
$(this.ariaLive).empty();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Positions the suggestions popup and starts a search.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.populatePopup = function () {
|
||||||
|
var $input = $(this.input);
|
||||||
|
var position = $input.position();
|
||||||
|
// Show popup.
|
||||||
|
if (this.popup) {
|
||||||
|
$(this.popup).remove();
|
||||||
|
}
|
||||||
|
this.selected = false;
|
||||||
|
this.popup = $('<div id="autocomplete"></div>')[0];
|
||||||
|
this.popup.owner = this;
|
||||||
|
$(this.popup).css({
|
||||||
|
top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
|
||||||
|
left: parseInt(position.left, 10) + 'px',
|
||||||
|
width: $input.innerWidth() + 'px',
|
||||||
|
display: 'none'
|
||||||
|
});
|
||||||
|
$input.before(this.popup);
|
||||||
|
|
||||||
|
// Do search.
|
||||||
|
this.db.owner = this;
|
||||||
|
this.db.search(this.input.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fills the suggestion popup with any matches received.
|
||||||
|
*/
|
||||||
|
Drupal.jsAC.prototype.found = function (matches) {
|
||||||
|
// If no value in the textfield, do not show the popup.
|
||||||
|
if (!this.input.value.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare matches.
|
||||||
|
var ul = $('<ul></ul>');
|
||||||
|
var ac = this;
|
||||||
|
for (key in matches) {
|
||||||
|
$('<li></li>')
|
||||||
|
.html($('<div></div>').html(matches[key]))
|
||||||
|
.mousedown(function () { ac.hidePopup(this); })
|
||||||
|
.mouseover(function () { ac.highlight(this); })
|
||||||
|
.mouseout(function () { ac.unhighlight(this); })
|
||||||
|
.data('autocompleteValue', key)
|
||||||
|
.appendTo(ul);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show popup with matches, if any.
|
||||||
|
if (this.popup) {
|
||||||
|
if (ul.children().length) {
|
||||||
|
$(this.popup).empty().append(ul).show();
|
||||||
|
$(this.ariaLive).html(Drupal.t('Autocomplete popup'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(this.popup).css({ visibility: 'hidden' });
|
||||||
|
this.hidePopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drupal.jsAC.prototype.setStatus = function (status) {
|
||||||
|
switch (status) {
|
||||||
|
case 'begin':
|
||||||
|
$(this.input).addClass('throbbing');
|
||||||
|
$(this.ariaLive).html(Drupal.t('Searching for matches...'));
|
||||||
|
break;
|
||||||
|
case 'cancel':
|
||||||
|
case 'error':
|
||||||
|
case 'found':
|
||||||
|
$(this.input).removeClass('throbbing');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An AutoComplete DataBase object.
|
||||||
|
*/
|
||||||
|
Drupal.ACDB = function (uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.delay = 300;
|
||||||
|
this.cache = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a cached and delayed search.
|
||||||
|
*/
|
||||||
|
Drupal.ACDB.prototype.search = function (searchString) {
|
||||||
|
var db = this;
|
||||||
|
this.searchString = searchString;
|
||||||
|
|
||||||
|
// See if this string needs to be searched for anyway. The pattern ../ is
|
||||||
|
// stripped since it may be misinterpreted by the browser.
|
||||||
|
searchString = searchString.replace(/^\s+|\.{2,}\/|\s+$/g, '');
|
||||||
|
// Skip empty search strings, or search strings ending with a comma, since
|
||||||
|
// that is the separator between search terms.
|
||||||
|
if (searchString.length <= 0 ||
|
||||||
|
searchString.charAt(searchString.length - 1) == ',') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if this key has been searched for before.
|
||||||
|
if (this.cache[searchString]) {
|
||||||
|
return this.owner.found(this.cache[searchString]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initiate delayed search.
|
||||||
|
if (this.timer) {
|
||||||
|
clearTimeout(this.timer);
|
||||||
|
}
|
||||||
|
this.timer = setTimeout(function () {
|
||||||
|
db.owner.setStatus('begin');
|
||||||
|
|
||||||
|
// Ajax GET request for autocompletion. We use Drupal.encodePath instead of
|
||||||
|
// encodeURIComponent to allow autocomplete search terms to contain slashes.
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: db.uri + '/' + Drupal.encodePath(searchString),
|
||||||
|
dataType: 'json',
|
||||||
|
success: function (matches) {
|
||||||
|
if (typeof matches.status == 'undefined' || matches.status != 0) {
|
||||||
|
db.cache[searchString] = matches;
|
||||||
|
// Verify if these are still the matches the user wants to see.
|
||||||
|
if (db.searchString == searchString) {
|
||||||
|
db.owner.found(matches);
|
||||||
|
}
|
||||||
|
db.owner.setStatus('found');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function (xmlhttp) {
|
||||||
|
Drupal.displayAjaxError(Drupal.ajaxError(xmlhttp, db.uri));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, this.delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the current autocomplete request.
|
||||||
|
*/
|
||||||
|
Drupal.ACDB.prototype.cancel = function () {
|
||||||
|
if (this.owner) this.owner.setStatus('cancel');
|
||||||
|
if (this.timer) clearTimeout(this.timer);
|
||||||
|
this.searchString = '';
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
32
misc/batch.js
Normal file
32
misc/batch.js
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the batch behavior to progress bars.
|
||||||
|
*/
|
||||||
|
Drupal.behaviors.batch = {
|
||||||
|
attach: function (context, settings) {
|
||||||
|
$('#progress', context).once('batch', function () {
|
||||||
|
var holder = $(this);
|
||||||
|
|
||||||
|
// Success: redirect to the summary.
|
||||||
|
var updateCallback = function (progress, status, pb) {
|
||||||
|
if (progress == 100) {
|
||||||
|
pb.stopMonitoring();
|
||||||
|
window.location = settings.batch.uri + '&op=finished';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var errorCallback = function (pb) {
|
||||||
|
holder.prepend($('<p class="error"></p>').html(settings.batch.errorMessage));
|
||||||
|
$('#wait').hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
var progress = new Drupal.progressBar('updateprogress', updateCallback, 'POST', errorCallback);
|
||||||
|
progress.setProgress(-1, settings.batch.initMessage);
|
||||||
|
holder.append(progress.element);
|
||||||
|
progress.startMonitoring(settings.batch.uri + '&op=do', 10);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
103
misc/collapse.js
Normal file
103
misc/collapse.js
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the visibility of a fieldset using smooth animations.
|
||||||
|
*/
|
||||||
|
Drupal.toggleFieldset = function (fieldset) {
|
||||||
|
var $fieldset = $(fieldset);
|
||||||
|
if ($fieldset.is('.collapsed')) {
|
||||||
|
var $content = $('> .fieldset-wrapper', fieldset).hide();
|
||||||
|
$fieldset
|
||||||
|
.removeClass('collapsed')
|
||||||
|
.trigger({ type: 'collapsed', value: false })
|
||||||
|
.find('> legend span.fieldset-legend-prefix').html(Drupal.t('Hide'));
|
||||||
|
$content.slideDown({
|
||||||
|
duration: 'fast',
|
||||||
|
easing: 'linear',
|
||||||
|
complete: function () {
|
||||||
|
Drupal.collapseScrollIntoView(fieldset);
|
||||||
|
fieldset.animating = false;
|
||||||
|
},
|
||||||
|
step: function () {
|
||||||
|
// Scroll the fieldset into view.
|
||||||
|
Drupal.collapseScrollIntoView(fieldset);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$fieldset.trigger({ type: 'collapsed', value: true });
|
||||||
|
$('> .fieldset-wrapper', fieldset).slideUp('fast', function () {
|
||||||
|
$fieldset
|
||||||
|
.addClass('collapsed')
|
||||||
|
.find('> legend span.fieldset-legend-prefix').html(Drupal.t('Show'));
|
||||||
|
fieldset.animating = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll a given fieldset into view as much as possible.
|
||||||
|
*/
|
||||||
|
Drupal.collapseScrollIntoView = function (node) {
|
||||||
|
var h = document.documentElement.clientHeight || document.body.clientHeight || 0;
|
||||||
|
var offset = document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||||
|
var posY = $(node).offset().top;
|
||||||
|
var fudge = 55;
|
||||||
|
if (posY + node.offsetHeight + fudge > h + offset) {
|
||||||
|
if (node.offsetHeight > h) {
|
||||||
|
window.scrollTo(0, posY);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.scrollTo(0, posY + node.offsetHeight - h + fudge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Drupal.behaviors.collapse = {
|
||||||
|
attach: function (context, settings) {
|
||||||
|
$('fieldset.collapsible', context).once('collapse', function () {
|
||||||
|
var $fieldset = $(this);
|
||||||
|
// Expand fieldset if there are errors inside, or if it contains an
|
||||||
|
// element that is targeted by the URI fragment identifier.
|
||||||
|
var anchor = location.hash && location.hash != '#' ? ', ' + location.hash : '';
|
||||||
|
if ($fieldset.find('.error' + anchor).length) {
|
||||||
|
$fieldset.removeClass('collapsed');
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = $('<span class="summary"></span>');
|
||||||
|
$fieldset.
|
||||||
|
bind('summaryUpdated', function () {
|
||||||
|
var text = $.trim($fieldset.drupalGetSummary());
|
||||||
|
summary.html(text ? ' (' + text + ')' : '');
|
||||||
|
})
|
||||||
|
.trigger('summaryUpdated');
|
||||||
|
|
||||||
|
// Turn the legend into a clickable link, but retain span.fieldset-legend
|
||||||
|
// for CSS positioning.
|
||||||
|
var $legend = $('> legend .fieldset-legend', this);
|
||||||
|
|
||||||
|
$('<span class="fieldset-legend-prefix element-invisible"></span>')
|
||||||
|
.append($fieldset.hasClass('collapsed') ? Drupal.t('Show') : Drupal.t('Hide'))
|
||||||
|
.prependTo($legend)
|
||||||
|
.after(' ');
|
||||||
|
|
||||||
|
// .wrapInner() does not retain bound events.
|
||||||
|
var $link = $('<a class="fieldset-title" href="#"></a>')
|
||||||
|
.prepend($legend.contents())
|
||||||
|
.appendTo($legend)
|
||||||
|
.click(function () {
|
||||||
|
var fieldset = $fieldset.get(0);
|
||||||
|
// Don't animate multiple times.
|
||||||
|
if (!fieldset.animating) {
|
||||||
|
fieldset.animating = true;
|
||||||
|
Drupal.toggleFieldset(fieldset);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$legend.append(summary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
BIN
misc/configure.png
Normal file
BIN
misc/configure.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 248 B |
BIN
misc/draggable.png
Normal file
BIN
misc/draggable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 268 B |
578
misc/drupal.js
Normal file
578
misc/drupal.js
Normal file
|
@ -0,0 +1,578 @@
|
||||||
|
|
||||||
|
var Drupal = Drupal || { 'settings': {}, 'behaviors': {}, 'locale': {} };
|
||||||
|
|
||||||
|
// Allow other JavaScript libraries to use $.
|
||||||
|
jQuery.noConflict();
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override jQuery.fn.init to guard against XSS attacks.
|
||||||
|
*
|
||||||
|
* See http://bugs.jquery.com/ticket/9521
|
||||||
|
*/
|
||||||
|
var jquery_init = $.fn.init;
|
||||||
|
$.fn.init = function (selector, context, rootjQuery) {
|
||||||
|
// If the string contains a "#" before a "<", treat it as invalid HTML.
|
||||||
|
if (selector && typeof selector === 'string') {
|
||||||
|
var hash_position = selector.indexOf('#');
|
||||||
|
if (hash_position >= 0) {
|
||||||
|
var bracket_position = selector.indexOf('<');
|
||||||
|
if (bracket_position > hash_position) {
|
||||||
|
throw 'Syntax error, unrecognized expression: ' + selector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return jquery_init.call(this, selector, context, rootjQuery);
|
||||||
|
};
|
||||||
|
$.fn.init.prototype = jquery_init.prototype;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach all registered behaviors to a page element.
|
||||||
|
*
|
||||||
|
* Behaviors are event-triggered actions that attach to page elements, enhancing
|
||||||
|
* default non-JavaScript UIs. Behaviors are registered in the Drupal.behaviors
|
||||||
|
* object using the method 'attach' and optionally also 'detach' as follows:
|
||||||
|
* @code
|
||||||
|
* Drupal.behaviors.behaviorName = {
|
||||||
|
* attach: function (context, settings) {
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* detach: function (context, settings, trigger) {
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* };
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* Drupal.attachBehaviors is added below to the jQuery ready event and so
|
||||||
|
* runs on initial page load. Developers implementing AHAH/Ajax in their
|
||||||
|
* solutions should also call this function after new page content has been
|
||||||
|
* loaded, feeding in an element to be processed, in order to attach all
|
||||||
|
* behaviors to the new content.
|
||||||
|
*
|
||||||
|
* Behaviors should use
|
||||||
|
* @code
|
||||||
|
* $(selector).once('behavior-name', function () {
|
||||||
|
* ...
|
||||||
|
* });
|
||||||
|
* @endcode
|
||||||
|
* to ensure the behavior is attached only once to a given element. (Doing so
|
||||||
|
* enables the reprocessing of given elements, which may be needed on occasion
|
||||||
|
* despite the ability to limit behavior attachment to a particular element.)
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* An element to attach behaviors to. If none is given, the document element
|
||||||
|
* is used.
|
||||||
|
* @param settings
|
||||||
|
* An object containing settings for the current context. If none given, the
|
||||||
|
* global Drupal.settings object is used.
|
||||||
|
*/
|
||||||
|
Drupal.attachBehaviors = function (context, settings) {
|
||||||
|
context = context || document;
|
||||||
|
settings = settings || Drupal.settings;
|
||||||
|
// Execute all of them.
|
||||||
|
$.each(Drupal.behaviors, function () {
|
||||||
|
if ($.isFunction(this.attach)) {
|
||||||
|
this.attach(context, settings);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach registered behaviors from a page element.
|
||||||
|
*
|
||||||
|
* Developers implementing AHAH/Ajax in their solutions should call this
|
||||||
|
* function before page content is about to be removed, feeding in an element
|
||||||
|
* to be processed, in order to allow special behaviors to detach from the
|
||||||
|
* content.
|
||||||
|
*
|
||||||
|
* Such implementations should look for the class name that was added in their
|
||||||
|
* corresponding Drupal.behaviors.behaviorName.attach implementation, i.e.
|
||||||
|
* behaviorName-processed, to ensure the behavior is detached only from
|
||||||
|
* previously processed elements.
|
||||||
|
*
|
||||||
|
* @param context
|
||||||
|
* An element to detach behaviors from. If none is given, the document element
|
||||||
|
* is used.
|
||||||
|
* @param settings
|
||||||
|
* An object containing settings for the current context. If none given, the
|
||||||
|
* global Drupal.settings object is used.
|
||||||
|
* @param trigger
|
||||||
|
* A string containing what's causing the behaviors to be detached. The
|
||||||
|
* possible triggers are:
|
||||||
|
* - unload: (default) The context element is being removed from the DOM.
|
||||||
|
* - move: The element is about to be moved within the DOM (for example,
|
||||||
|
* during a tabledrag row swap). After the move is completed,
|
||||||
|
* Drupal.attachBehaviors() is called, so that the behavior can undo
|
||||||
|
* whatever it did in response to the move. Many behaviors won't need to
|
||||||
|
* do anything simply in response to the element being moved, but because
|
||||||
|
* IFRAME elements reload their "src" when being moved within the DOM,
|
||||||
|
* behaviors bound to IFRAME elements (like WYSIWYG editors) may need to
|
||||||
|
* take some action.
|
||||||
|
* - serialize: When an Ajax form is submitted, this is called with the
|
||||||
|
* form as the context. This provides every behavior within the form an
|
||||||
|
* opportunity to ensure that the field elements have correct content
|
||||||
|
* in them before the form is serialized. The canonical use-case is so
|
||||||
|
* that WYSIWYG editors can update the hidden textarea to which they are
|
||||||
|
* bound.
|
||||||
|
*
|
||||||
|
* @see Drupal.attachBehaviors
|
||||||
|
*/
|
||||||
|
Drupal.detachBehaviors = function (context, settings, trigger) {
|
||||||
|
context = context || document;
|
||||||
|
settings = settings || Drupal.settings;
|
||||||
|
trigger = trigger || 'unload';
|
||||||
|
// Execute all of them.
|
||||||
|
$.each(Drupal.behaviors, function () {
|
||||||
|
if ($.isFunction(this.detach)) {
|
||||||
|
this.detach(context, settings, trigger);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode special characters in a plain-text string for display as HTML.
|
||||||
|
*
|
||||||
|
* @ingroup sanitization
|
||||||
|
*/
|
||||||
|
Drupal.checkPlain = function (str) {
|
||||||
|
var character, regex,
|
||||||
|
replace = { '&': '&', '"': '"', '<': '<', '>': '>' };
|
||||||
|
str = String(str);
|
||||||
|
for (character in replace) {
|
||||||
|
if (replace.hasOwnProperty(character)) {
|
||||||
|
regex = new RegExp(character, 'g');
|
||||||
|
str = str.replace(regex, replace[character]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace placeholders with sanitized values in a string.
|
||||||
|
*
|
||||||
|
* @param str
|
||||||
|
* A string with placeholders.
|
||||||
|
* @param args
|
||||||
|
* An object of replacements pairs to make. Incidences of any key in this
|
||||||
|
* array are replaced with the corresponding value. Based on the first
|
||||||
|
* character of the key, the value is escaped and/or themed:
|
||||||
|
* - !variable: inserted as is
|
||||||
|
* - @variable: escape plain text to HTML (Drupal.checkPlain)
|
||||||
|
* - %variable: escape text and theme as a placeholder for user-submitted
|
||||||
|
* content (checkPlain + Drupal.theme('placeholder'))
|
||||||
|
*
|
||||||
|
* @see Drupal.t()
|
||||||
|
* @ingroup sanitization
|
||||||
|
*/
|
||||||
|
Drupal.formatString = function(str, args) {
|
||||||
|
// Transform arguments before inserting them.
|
||||||
|
for (var key in args) {
|
||||||
|
if (args.hasOwnProperty(key)) {
|
||||||
|
switch (key.charAt(0)) {
|
||||||
|
// Escaped only.
|
||||||
|
case '@':
|
||||||
|
args[key] = Drupal.checkPlain(args[key]);
|
||||||
|
break;
|
||||||
|
// Pass-through.
|
||||||
|
case '!':
|
||||||
|
break;
|
||||||
|
// Escaped and placeholder.
|
||||||
|
default:
|
||||||
|
args[key] = Drupal.theme('placeholder', args[key]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Drupal.stringReplace(str, args, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace substring.
|
||||||
|
*
|
||||||
|
* The longest keys will be tried first. Once a substring has been replaced,
|
||||||
|
* its new value will not be searched again.
|
||||||
|
*
|
||||||
|
* @param {String} str
|
||||||
|
* A string with placeholders.
|
||||||
|
* @param {Object} args
|
||||||
|
* Key-value pairs.
|
||||||
|
* @param {Array|null} keys
|
||||||
|
* Array of keys from the "args". Internal use only.
|
||||||
|
*
|
||||||
|
* @return {String}
|
||||||
|
* Returns the replaced string.
|
||||||
|
*/
|
||||||
|
Drupal.stringReplace = function (str, args, keys) {
|
||||||
|
if (str.length === 0) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the array of keys is not passed then collect the keys from the args.
|
||||||
|
if (!$.isArray(keys)) {
|
||||||
|
keys = [];
|
||||||
|
for (var k in args) {
|
||||||
|
if (args.hasOwnProperty(k)) {
|
||||||
|
keys.push(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Order the keys by the character length. The shortest one is the first.
|
||||||
|
keys.sort(function (a, b) { return a.length - b.length; });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keys.length === 0) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take next longest one from the end.
|
||||||
|
var key = keys.pop();
|
||||||
|
var fragments = str.split(key);
|
||||||
|
|
||||||
|
if (keys.length) {
|
||||||
|
for (var i = 0; i < fragments.length; i++) {
|
||||||
|
// Process each fragment with a copy of remaining keys.
|
||||||
|
fragments[i] = Drupal.stringReplace(fragments[i], args, keys.slice(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fragments.join(args[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translate strings to the page language or a given language.
|
||||||
|
*
|
||||||
|
* See the documentation of the server-side t() function for further details.
|
||||||
|
*
|
||||||
|
* @param str
|
||||||
|
* A string containing the English string to translate.
|
||||||
|
* @param args
|
||||||
|
* An object of replacements pairs to make after translation. Incidences
|
||||||
|
* of any key in this array are replaced with the corresponding value.
|
||||||
|
* See Drupal.formatString().
|
||||||
|
*
|
||||||
|
* @param options
|
||||||
|
* - 'context' (defaults to the empty context): The context the source string
|
||||||
|
* belongs to.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The translated string.
|
||||||
|
*/
|
||||||
|
Drupal.t = function (str, args, options) {
|
||||||
|
options = options || {};
|
||||||
|
options.context = options.context || '';
|
||||||
|
|
||||||
|
// Fetch the localized version of the string.
|
||||||
|
if (Drupal.locale.strings && Drupal.locale.strings[options.context] && Drupal.locale.strings[options.context][str]) {
|
||||||
|
str = Drupal.locale.strings[options.context][str];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args) {
|
||||||
|
str = Drupal.formatString(str, args);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a string containing a count of items.
|
||||||
|
*
|
||||||
|
* This function ensures that the string is pluralized correctly. Since Drupal.t() is
|
||||||
|
* called by this function, make sure not to pass already-localized strings to it.
|
||||||
|
*
|
||||||
|
* See the documentation of the server-side format_plural() function for further details.
|
||||||
|
*
|
||||||
|
* @param count
|
||||||
|
* The item count to display.
|
||||||
|
* @param singular
|
||||||
|
* The string for the singular case. Please make sure it is clear this is
|
||||||
|
* singular, to ease translation (e.g. use "1 new comment" instead of "1 new").
|
||||||
|
* Do not use @count in the singular string.
|
||||||
|
* @param plural
|
||||||
|
* The string for the plural case. Please make sure it is clear this is plural,
|
||||||
|
* to ease translation. Use @count in place of the item count, as in "@count
|
||||||
|
* new comments".
|
||||||
|
* @param args
|
||||||
|
* An object of replacements pairs to make after translation. Incidences
|
||||||
|
* of any key in this array are replaced with the corresponding value.
|
||||||
|
* See Drupal.formatString().
|
||||||
|
* Note that you do not need to include @count in this array.
|
||||||
|
* This replacement is done automatically for the plural case.
|
||||||
|
* @param options
|
||||||
|
* The options to pass to the Drupal.t() function.
|
||||||
|
* @return
|
||||||
|
* A translated string.
|
||||||
|
*/
|
||||||
|
Drupal.formatPlural = function (count, singular, plural, args, options) {
|
||||||
|
args = args || {};
|
||||||
|
args['@count'] = count;
|
||||||
|
// Determine the index of the plural form.
|
||||||
|
var index = Drupal.locale.pluralFormula ? Drupal.locale.pluralFormula(args['@count']) : ((args['@count'] == 1) ? 0 : 1);
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
return Drupal.t(singular, args, options);
|
||||||
|
}
|
||||||
|
else if (index == 1) {
|
||||||
|
return Drupal.t(plural, args, options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args['@count[' + index + ']'] = args['@count'];
|
||||||
|
delete args['@count'];
|
||||||
|
return Drupal.t(plural.replace('@count', '@count[' + index + ']'), args, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the passed in URL as an absolute URL.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The URL string to be normalized to an absolute URL.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The normalized, absolute URL.
|
||||||
|
*
|
||||||
|
* @see https://github.com/angular/angular.js/blob/v1.4.4/src/ng/urlUtils.js
|
||||||
|
* @see https://grack.com/blog/2009/11/17/absolutizing-url-in-javascript
|
||||||
|
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L53
|
||||||
|
*/
|
||||||
|
Drupal.absoluteUrl = function (url) {
|
||||||
|
var urlParsingNode = document.createElement('a');
|
||||||
|
|
||||||
|
// Decode the URL first; this is required by IE <= 6. Decoding non-UTF-8
|
||||||
|
// strings may throw an exception.
|
||||||
|
try {
|
||||||
|
url = decodeURIComponent(url);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
urlParsingNode.setAttribute('href', url);
|
||||||
|
|
||||||
|
// IE <= 7 normalizes the URL when assigned to the anchor node similar to
|
||||||
|
// the other browsers.
|
||||||
|
return urlParsingNode.cloneNode(false).href;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the URL is within Drupal's base path.
|
||||||
|
*
|
||||||
|
* @param url
|
||||||
|
* The URL string to be tested.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* Boolean true if local.
|
||||||
|
*
|
||||||
|
* @see https://github.com/jquery/jquery-ui/blob/1.11.4/ui/tabs.js#L58
|
||||||
|
*/
|
||||||
|
Drupal.urlIsLocal = function (url) {
|
||||||
|
// Always use browser-derived absolute URLs in the comparison, to avoid
|
||||||
|
// attempts to break out of the base path using directory traversal.
|
||||||
|
var absoluteUrl = Drupal.absoluteUrl(url);
|
||||||
|
var protocol = location.protocol;
|
||||||
|
|
||||||
|
// Consider URLs that match this site's base URL but use HTTPS instead of HTTP
|
||||||
|
// as local as well.
|
||||||
|
if (protocol === 'http:' && absoluteUrl.indexOf('https:') === 0) {
|
||||||
|
protocol = 'https:';
|
||||||
|
}
|
||||||
|
var baseUrl = protocol + '//' + location.host + Drupal.settings.basePath.slice(0, -1);
|
||||||
|
|
||||||
|
// Decoding non-UTF-8 strings may throw an exception.
|
||||||
|
try {
|
||||||
|
absoluteUrl = decodeURIComponent(absoluteUrl);
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
baseUrl = decodeURIComponent(baseUrl);
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// The given URL matches the site's base URL, or has a path under the site's
|
||||||
|
// base URL.
|
||||||
|
return absoluteUrl === baseUrl || absoluteUrl.indexOf(baseUrl + '/') === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the themed representation of a Drupal object.
|
||||||
|
*
|
||||||
|
* All requests for themed output must go through this function. It examines
|
||||||
|
* the request and routes it to the appropriate theme function. If the current
|
||||||
|
* theme does not provide an override function, the generic theme function is
|
||||||
|
* called.
|
||||||
|
*
|
||||||
|
* For example, to retrieve the HTML for text that should be emphasized and
|
||||||
|
* displayed as a placeholder inside a sentence, call
|
||||||
|
* Drupal.theme('placeholder', text).
|
||||||
|
*
|
||||||
|
* @param func
|
||||||
|
* The name of the theme function to call.
|
||||||
|
* @param ...
|
||||||
|
* Additional arguments to pass along to the theme function.
|
||||||
|
* @return
|
||||||
|
* Any data the theme function returns. This could be a plain HTML string,
|
||||||
|
* but also a complex object.
|
||||||
|
*/
|
||||||
|
Drupal.theme = function (func) {
|
||||||
|
var args = Array.prototype.slice.apply(arguments, [1]);
|
||||||
|
|
||||||
|
return (Drupal.theme[func] || Drupal.theme.prototype[func]).apply(this, args);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freeze the current body height (as minimum height). Used to prevent
|
||||||
|
* unnecessary upwards scrolling when doing DOM manipulations.
|
||||||
|
*/
|
||||||
|
Drupal.freezeHeight = function () {
|
||||||
|
Drupal.unfreezeHeight();
|
||||||
|
$('<div id="freeze-height"></div>').css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '0px',
|
||||||
|
left: '0px',
|
||||||
|
width: '1px',
|
||||||
|
height: $('body').css('height')
|
||||||
|
}).appendTo('body');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unfreeze the body height.
|
||||||
|
*/
|
||||||
|
Drupal.unfreezeHeight = function () {
|
||||||
|
$('#freeze-height').remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes a Drupal path for use in a URL.
|
||||||
|
*
|
||||||
|
* For aesthetic reasons slashes are not escaped.
|
||||||
|
*/
|
||||||
|
Drupal.encodePath = function (item, uri) {
|
||||||
|
uri = uri || location.href;
|
||||||
|
return encodeURIComponent(item).replace(/%2F/g, '/');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text selection in a textarea.
|
||||||
|
*/
|
||||||
|
Drupal.getSelection = function (element) {
|
||||||
|
if (typeof element.selectionStart != 'number' && document.selection) {
|
||||||
|
// The current selection.
|
||||||
|
var range1 = document.selection.createRange();
|
||||||
|
var range2 = range1.duplicate();
|
||||||
|
// Select all text.
|
||||||
|
range2.moveToElementText(element);
|
||||||
|
// Now move 'dummy' end point to end point of original range.
|
||||||
|
range2.setEndPoint('EndToEnd', range1);
|
||||||
|
// Now we can calculate start and end points.
|
||||||
|
var start = range2.text.length - range1.text.length;
|
||||||
|
var end = start + range1.text.length;
|
||||||
|
return { 'start': start, 'end': end };
|
||||||
|
}
|
||||||
|
return { 'start': element.selectionStart, 'end': element.selectionEnd };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a global variable which determines if the window is being unloaded.
|
||||||
|
*
|
||||||
|
* This is primarily used by Drupal.displayAjaxError().
|
||||||
|
*/
|
||||||
|
Drupal.beforeUnloadCalled = false;
|
||||||
|
$(window).bind('beforeunload pagehide', function () {
|
||||||
|
Drupal.beforeUnloadCalled = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a JavaScript error from an Ajax response when appropriate to do so.
|
||||||
|
*/
|
||||||
|
Drupal.displayAjaxError = function (message) {
|
||||||
|
// Skip displaying the message if the user deliberately aborted (for example,
|
||||||
|
// by reloading the page or navigating to a different page) while the Ajax
|
||||||
|
// request was still ongoing. See, for example, the discussion at
|
||||||
|
// http://stackoverflow.com/questions/699941/handle-ajax-error-when-a-user-clicks-refresh.
|
||||||
|
if (!Drupal.beforeUnloadCalled) {
|
||||||
|
alert(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an error message from an Ajax response.
|
||||||
|
*/
|
||||||
|
Drupal.ajaxError = function (xmlhttp, uri, customMessage) {
|
||||||
|
var statusCode, statusText, pathText, responseText, readyStateText, message;
|
||||||
|
if (xmlhttp.status) {
|
||||||
|
statusCode = "\n" + Drupal.t("An AJAX HTTP error occurred.") + "\n" + Drupal.t("HTTP Result Code: !status", {'!status': xmlhttp.status});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
statusCode = "\n" + Drupal.t("An AJAX HTTP request terminated abnormally.");
|
||||||
|
}
|
||||||
|
statusCode += "\n" + Drupal.t("Debugging information follows.");
|
||||||
|
pathText = "\n" + Drupal.t("Path: !uri", {'!uri': uri} );
|
||||||
|
statusText = '';
|
||||||
|
// In some cases, when statusCode == 0, xmlhttp.statusText may not be defined.
|
||||||
|
// Unfortunately, testing for it with typeof, etc, doesn't seem to catch that
|
||||||
|
// and the test causes an exception. So we need to catch the exception here.
|
||||||
|
try {
|
||||||
|
statusText = "\n" + Drupal.t("StatusText: !statusText", {'!statusText': $.trim(xmlhttp.statusText)});
|
||||||
|
}
|
||||||
|
catch (e) {}
|
||||||
|
|
||||||
|
responseText = '';
|
||||||
|
// Again, we don't have a way to know for sure whether accessing
|
||||||
|
// xmlhttp.responseText is going to throw an exception. So we'll catch it.
|
||||||
|
try {
|
||||||
|
responseText = "\n" + Drupal.t("ResponseText: !responseText", {'!responseText': $.trim(xmlhttp.responseText) } );
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
// Make the responseText more readable by stripping HTML tags and newlines.
|
||||||
|
responseText = responseText.replace(/<("[^"]*"|'[^']*'|[^'">])*>/gi,"");
|
||||||
|
responseText = responseText.replace(/[\n]+\s+/g,"\n");
|
||||||
|
|
||||||
|
// We don't need readyState except for status == 0.
|
||||||
|
readyStateText = xmlhttp.status == 0 ? ("\n" + Drupal.t("ReadyState: !readyState", {'!readyState': xmlhttp.readyState})) : "";
|
||||||
|
|
||||||
|
// Additional message beyond what the xmlhttp object provides.
|
||||||
|
customMessage = customMessage ? ("\n" + Drupal.t("CustomMessage: !customMessage", {'!customMessage': customMessage})) : "";
|
||||||
|
|
||||||
|
message = statusCode + pathText + statusText + customMessage + responseText + readyStateText;
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Class indicating that JS is enabled; used for styling purpose.
|
||||||
|
$('html').addClass('js');
|
||||||
|
|
||||||
|
// 'js enabled' cookie.
|
||||||
|
document.cookie = 'has_js=1; path=/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additions to jQuery.support.
|
||||||
|
*/
|
||||||
|
$(function () {
|
||||||
|
/**
|
||||||
|
* Boolean indicating whether or not position:fixed is supported.
|
||||||
|
*/
|
||||||
|
if (jQuery.support.positionFixed === undefined) {
|
||||||
|
var el = $('<div style="position:fixed; top:10px" />').appendTo(document.body);
|
||||||
|
jQuery.support.positionFixed = el[0].offsetTop === 10;
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Attach all behaviors.
|
||||||
|
$(function () {
|
||||||
|
Drupal.attachBehaviors(document, Drupal.settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default themes.
|
||||||
|
*/
|
||||||
|
Drupal.theme.prototype = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats text for emphasized display in a placeholder inside a sentence.
|
||||||
|
*
|
||||||
|
* @param str
|
||||||
|
* The text to format (plain-text).
|
||||||
|
* @return
|
||||||
|
* The formatted text (html).
|
||||||
|
*/
|
||||||
|
placeholder: function (str) {
|
||||||
|
return '<em class="placeholder">' + Drupal.checkPlain(str) + '</em>';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
})(jQuery);
|
BIN
misc/druplicon.png
Normal file
BIN
misc/druplicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
36
misc/farbtastic/farbtastic.css
Normal file
36
misc/farbtastic/farbtastic.css
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
.farbtastic {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.farbtastic * {
|
||||||
|
position: absolute;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.farbtastic,
|
||||||
|
.farbtastic .wheel {
|
||||||
|
width: 195px;
|
||||||
|
height: 195px;
|
||||||
|
}
|
||||||
|
.farbtastic .color,
|
||||||
|
.farbtastic .overlay {
|
||||||
|
top: 47px;
|
||||||
|
left: 47px;
|
||||||
|
width: 101px;
|
||||||
|
height: 101px;
|
||||||
|
}
|
||||||
|
.farbtastic .wheel {
|
||||||
|
background: url(wheel.png) no-repeat;
|
||||||
|
width: 195px;
|
||||||
|
height: 195px;
|
||||||
|
}
|
||||||
|
.farbtastic .overlay {
|
||||||
|
background: url(mask.png) no-repeat;
|
||||||
|
}
|
||||||
|
.farbtastic .marker {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
margin: -8px 0 0 -8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: url(marker.png) no-repeat;
|
||||||
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue