Compare commits

..
56 Commits
Author SHA1 Message Date
Bachir Soussi Chiadmi e36279d8b1 changed materio_serach_api, query configuration by query 2016-03-16 12:35:55 +01:00
Bachir Soussi Chiadmi 0b6e0c10cb updated search feature 2016-03-02 10:15:50 +01:00
Bachir Soussi Chiadmi ba1ec7113e updated search_api and search_api_solr 2016-03-01 16:23:45 +01:00
Bachir Soussi Chiadmi deb2bff572 print fine tuning 2016-01-21 12:02:55 +01:00
Bachir Soussi Chiadmi 538c791bae fine tunning print fiche 2016-01-20 18:15:38 +01:00
Bachir Soussi Chiadmi df107a0443 fixed printed material
WARNING : we are nox using gulp instead of grunt, watch for bugs
2016-01-19 19:23:16 +01:00
Bachir Soussi Chiadmi c9b16289ef added remove duplicates modules 2016-01-19 15:10:33 +01:00
Bachir Soussi Chiadmi b4acd7d8bb updated video filter module to handle https on embeded videos 2015-12-20 10:56:50 +01:00
Bachir Soussi Chiadmi 3af02617b4 added html5 boiler plate mime headers to htaccess 2015-12-19 21:36:40 +01:00
Bachir Soussi Chiadmi 0fdb4abd71 ajax autocomplete fixe 2015-12-03 12:23:53 +01:00
Bachir Soussi Chiadmi 90be34eef1 updated htaccess 2015-12-02 17:17:06 +01:00
Bachir Soussi Chiadmi 3e984dc6c5 updated htaccess 2015-12-02 17:13:13 +01:00
Bachir Soussi Chiadmi a0cb0b2171 updated htaccess 2015-12-02 16:57:17 +01:00
Bachir Soussi Chiadmi f6346b0195 updated htaccess 2015-12-02 16:52:56 +01:00
Bachir Soussi Chiadmi 300f698989 updated htaccess 2015-12-02 16:46:36 +01:00
Bachir Soussi Chiadmi 8cbb5f8c4c updated htaccess 2015-12-02 16:45:51 +01:00
Bachir Soussi Chiadmi a106b98209 updated htaccess 2015-12-02 16:30:59 +01:00
Bachir Soussi Chiadmi 8fe92dbcc7 updated htaccess 2015-12-02 16:29:02 +01:00
Bachir Soussi Chiadmi 60f2c5c7fa updated htaccess 2015-12-02 15:20:02 +01:00
Bachir Soussi Chiadmi d96c17454b updated htaccess 2015-12-02 11:40:34 +01:00
Bachir Soussi Chiadmi ed6ae84181 added ubercart ssl module 2015-12-02 11:36:55 +01:00
Bachir Soussi Chiadmi 35e914df58 created admon taxo view 2015-11-25 11:34:49 +01:00
Bachir Soussi Chiadmi 6b46869a16 htaccess : all redirects are handle by claranet 2015-11-25 09:47:27 +01:00
Bachir Soussi Chiadmi 892dba801f added entity_translation_export_import module 2015-11-25 00:53:33 +01:00
Bachir Soussi Chiadmi 50dfd2d89b removed i18n_taxonomy module from features dependencies
fixed some bugs
2015-11-24 23:50:19 +01:00
Bachir Soussi Chiadmi 9a18821b7e fixed search api materio settings 2015-11-24 18:17:34 +01:00
Bachir Soussi Chiadmi 82bf36a5eb recreated features 2015-11-24 18:05:48 +01:00
Bachir Soussi Chiadmi 3eb20676ca search api autocomplet t() for transmlate term name 2015-11-24 16:51:48 +01:00
Bachir Soussi Chiadmi 2155a66eb1 added special menu items module 2015-11-24 15:29:54 +01:00
Bachir Soussi Chiadmi 62fef652fc removed redirects from htaccess, claranet will handle this on vhosts 2015-11-24 15:15:10 +01:00
Bachir Soussi Chiadmi 951c9114a2 added code for remove all http on file address 2015-11-24 13:42:37 +01:00
Bachir Soussi Chiadmi edba04f103 htaccess fixe 2015-11-24 13:23:18 +01:00
Bachir Soussi Chiadmi 26ab9aa56b htaccess fixe 2015-11-24 13:22:11 +01:00
Bachir Soussi Chiadmi 5716acf5bb htaccess fixe 2015-11-24 13:20:51 +01:00
Bachir Soussi Chiadmi 12fe570fbd htaccess fixe 2015-11-24 13:20:11 +01:00
Bachir Soussi Chiadmi 66ee9c6657 htaccess fixe 2015-11-24 13:19:01 +01:00
Bachir Soussi Chiadmi ca5231f1fe htaccess fixe 2015-11-24 13:09:04 +01:00
Bachir Soussi Chiadmi b9a8db8140 htaccess fixe 2015-11-24 13:08:22 +01:00
Bachir Soussi Chiadmi d25e7428bc htaccess fixe 2015-11-24 12:55:37 +01:00
Bachir Soussi Chiadmi 3027b3985b htaccess fixe 2015-11-24 12:55:09 +01:00
Bachir Soussi Chiadmi d214a44291 htaccess fixe 2015-11-24 12:49:06 +01:00
Bachir Soussi Chiadmi fc7753f7a0 htaccess fixe 2015-11-24 12:48:26 +01:00
Bachir Soussi Chiadmi 7c51b255fb htaccess fixe 2015-11-24 12:47:26 +01:00
Bachir Soussi Chiadmi 5e099d48de htaccess fixe 2015-11-24 12:35:16 +01:00
Bachir Soussi Chiadmi bf84ac06cd htaccess fixe 2015-11-24 12:34:56 +01:00
Bachir Soussi Chiadmi 87beabee92 htaccess fixe 2015-11-24 12:29:51 +01:00
Bachir Soussi Chiadmi f6ee366a71 htaccess fixe 2015-11-24 12:29:32 +01:00
Bachir Soussi Chiadmi b216e30f28 htaccess fixe 2015-11-24 12:28:04 +01:00
Bachir Soussi Chiadmi 0a863fc1bb htaccess ssl 2015-11-24 12:26:56 +01:00
Bachir Soussi Chiadmi a5edc41323 htaccess fixe 2015-11-24 12:12:57 +01:00
Bachir Soussi Chiadmi 1cc2c9af75 updated term csv files 2015-11-24 11:17:12 +01:00
Bachir Soussi Chiadmi cc653b73be bug fixe 2015-11-24 11:02:29 +01:00
Bachir Soussi Chiadmi 73993a539b added fr2en batch function 2015-11-24 10:59:13 +01:00
Bachir Soussi Chiadmi d7e7410950 added term_merge module 2015-11-24 10:56:30 +01:00
Bachir Soussi Chiadmi 7298235d4f added taxonomy wrangler module 2015-11-23 14:47:44 +01:00
Bachir Soussi Chiadmi fc0a779214 fixed bookmarks for students 2015-10-21 20:22:58 +02:00
155 changed files with 22523 additions and 16166 deletions
+149 -106
View File
@@ -3,7 +3,7 @@
#
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(\..*|Entries.*|Repository|Root|Tag|Template)$">
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\..*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig\.save)$">
Order allow,deny
</FilesMatch>
@@ -20,7 +20,7 @@ ErrorDocument 404 /index.php
DirectoryIndex index.php index.html index.htm
# Override PHP settings that cannot be changed at runtime. See
# sites/default/default.settings.php and drupal_initialize_variables() in
# 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.
@@ -56,6 +56,17 @@ DirectoryIndex index.php index.html index.htm
<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
@@ -71,18 +82,6 @@ DirectoryIndex index.php index.html index.htm
# downloaded.
RewriteRule "(^|/)\." - [F]
# custom
RewriteCond %{HTTP_HOST} ^basebeta\.materio\.com [OR]
RewriteCond %{HTTP_HOST} ^materio\.com [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.fr [OR]
RewriteCond %{HTTP_HOST} ^materio\.fr [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.net [OR]
RewriteCond %{HTTP_HOST} ^materio\.net [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.eu [OR]
RewriteCond %{HTTP_HOST} ^materio\.eu [NC]
RewriteRule ^ http://www.materio.com%{REQUEST_URI} [L,R=301]
# 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:
@@ -90,14 +89,32 @@ DirectoryIndex index.php index.html index.htm
# 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://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
#
# 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://%1%{REQUEST_URI} [L,R=301]
# RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301]
# redirect all materiau domains to materio.com
RewriteCond %{HTTP_HOST} ^basebeta\.materio\.com [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.fr [OR]
RewriteCond %{HTTP_HOST} ^materio\.fr [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.net [OR]
RewriteCond %{HTTP_HOST} ^materio\.net [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.eu [OR]
RewriteCond %{HTTP_HOST} ^materio\.eu [OR]
RewriteCond %{HTTP_HOST} ^www\.materio\.com
# RewriteRule ^ http%{ENV:protossl}://materio.com%{REQUEST_URI} [L,R=301]
RewriteRule ^ https://materio.com%{REQUEST_URI} [L,R=301]
# Redirect to HTTPS
# RewriteCond %{HTTPS} off
# RewriteCond %{HTTP:X-Forwarded-Proto} !https
# RewriteRule ^(.*)$ https://%{HTTP_HOST}%{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.
@@ -116,24 +133,6 @@ DirectoryIndex index.php index.html index.htm
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^ index.php [L]
## local
# RewriteCond %{REMOTE_ADDR} !^127\.0\.0\.1
## maison quentin
# RewriteCond %{REMOTE_ADDR} !^86\.68\.188\.175
## materio
# RewriteCond %{REMOTE_ADDR} !^212\.198\.136\.215
## maison bach
# RewriteCond %{REMOTE_ADDR} !^82\.230\.62\.145
# RewriteRule !maintenance http://www.materio.com/maintenance [L,R=302,E=nocache:1]
## http://mark.koli.ch/2010/12/set-cache-control-and-expires-headers-on-a-redirect-with-mod-rewrite.html
## Set the response header if the "nocache" environment variable is set
## in the RewriteRule above.
# Header always set Cache-Control "no-store, no-cache, must-revalidate" env=nocache
## Set Expires too ...
# Header always set Expires "Thu, 01 Jan 1970 00:00:00 GMT" env=nocache
# Rules to correctly serve gzip compressed CSS and JS files.
# Requires both mod_rewrite and mod_headers to be enabled.
<IfModule mod_headers.c>
@@ -160,94 +159,138 @@ DirectoryIndex index.php index.html index.htm
</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>
# from boiler plate V5.02
# FROM Html5 BoilerPlate 4.0
# ######################################################################
# # MEDIA TYPES AND CHARACTER ENCODINGS #
# ######################################################################
# ----------------------------------------------------------------------
# Proper MIME type for all files
# | Media types |
# ----------------------------------------------------------------------
# JavaScript
# Normalize to standard type (it's sniffed in IE anyways)
# tools.ietf.org/html/rfc4329#section-7.2
AddType application/javascript js jsonp
AddType application/json json
# Serve resources with the proper media types (f.k.a. MIME types).
#
# https://www.iana.org/assignments/media-types/media-types.xhtml
# https://httpd.apache.org/docs/current/mod/mod_mime.html#addtype
# Audio
AddType audio/ogg oga ogg
AddType audio/mp4 m4a f4a f4b
<IfModule mod_mime.c>
# Video
AddType video/ogg ogv
AddType video/mp4 mp4 m4v f4v f4p
AddType video/webm webm
AddType video/x-flv flv
# Data interchange
# SVG
# Required for svg webfonts on iPad
# twitter.com/FontSquirrel/status/14855840545
AddType image/svg+xml svg svgz
AddEncoding gzip svgz
# Webfonts
AddType application/vnd.ms-fontobject eot
AddType application/x-font-ttf ttf ttc
AddType font/opentype otf
AddType application/x-font-woff woff
# Assorted types
AddType image/x-icon ico
AddType image/webp webp
AddType text/cache-manifest appcache manifest
AddType text/x-component htc
AddType application/xml rss atom xml rdf
AddType application/x-chrome-extension crx
AddType application/x-opera-extension oex
AddType application/x-xpinstall xpi
AddType application/octet-stream safariextz
AddType application/x-web-app-manifest+json webapp
AddType text/x-vcard vcf
AddType application/x-shockwave-flash swf
AddType text/vtt vtt
AddType application/atom+xml atom
AddType application/json json map topojson
AddType application/ld+json jsonld
AddType application/rss+xml rss
AddType application/vnd.geo+json geojson
AddType application/xml rdf xml
# JavaScript
# Normalize to standard type.
# https://tools.ietf.org/html/rfc4329#section-7.2
AddType application/javascript js
# Manifest files
AddType application/manifest+json webmanifest
AddType application/x-web-app-manifest+json webapp
AddType text/cache-manifest appcache
# Media files
AddType audio/mp4 f4a f4b m4a
AddType audio/ogg oga ogg opus
AddType image/bmp bmp
AddType image/svg+xml svg svgz
AddType image/webp webp
AddType video/mp4 f4v f4p m4v mp4
AddType video/ogg ogv
AddType video/webm webm
AddType video/x-flv flv
# Serving `.ico` image files with a different media type
# prevents Internet Explorer from displaying then as images:
# https://github.com/h5bp/html5-boilerplate/commit/37b5fec090d00f38de64b591bcddcb205aadf8ee
AddType image/x-icon cur ico
# Web fonts
AddType application/font-woff woff
AddType application/font-woff2 woff2
AddType application/vnd.ms-fontobject eot
# Browsers usually ignore the font media types and simply sniff
# the bytes to figure out the font type.
# https://mimesniff.spec.whatwg.org/#matching-a-font-type-pattern
#
# However, Blink and WebKit based browsers will show a warning
# in the console if the following font types are served with any
# other media types.
AddType application/x-font-ttf ttc ttf
AddType font/opentype otf
# Other
AddType application/octet-stream safariextz
AddType application/x-bb-appworld bbaw
AddType application/x-chrome-extension crx
AddType application/x-opera-extension oex
AddType application/x-xpinstall xpi
AddType text/vcard vcard vcf
AddType text/vnd.rim.location.xloc xloc
AddType text/vtt vtt
AddType text/x-component htc
</IfModule>
# ----------------------------------------------------------------------
# UTF-8 encoding
# | Character encodings |
# ----------------------------------------------------------------------
# Use UTF-8 encoding for anything served text/plain or text/html
# Serve all resources labeled as `text/html` or `text/plain`
# with the media type `charset` parameter set to `UTF-8`.
#
# https://httpd.apache.org/docs/current/mod/core.html#adddefaultcharset
AddDefaultCharset utf-8
# Force UTF-8 for a number of file formats
AddCharset utf-8 .atom .css .js .json .rss .vtt .xml
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Serve the following file types with the media type `charset`
# parameter set to `UTF-8`.
#
# https://httpd.apache.org/docs/current/mod/mod_mime.html#addcharset
# ----------------------------------------------------------------------
# A little more security
# ----------------------------------------------------------------------
# Block access to "hidden" directories or files whose names begin with a
# period. This includes directories used by version control systems such as
# Subversion or Git.
<IfModule mod_rewrite.c>
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
# Block access to backup and source files. These files may be left by some
# text/html editors and pose a great security danger, when anyone can access
# them.
<FilesMatch "(\.(bak|config|sql|fla|psd|ini|log|sh|inc|swp|dist)|~)$">
Order allow,deny
Deny from all
Satisfy All
</FilesMatch>
# Increase cookie security
<IfModule php5_module>
php_value session.cookie_httponly true
<IfModule mod_mime.c>
AddCharset utf-8 .atom \
.bbaw \
.css \
.geojson \
.js \
.json \
.jsonld \
.manifest \
.rdf \
.rss \
.topojson \
.vtt \
.webapp \
.webmanifest \
.xloc \
.xml
</IfModule>
+2 -3
View File
@@ -17,9 +17,9 @@ feedback
filter_path_alias
flag_lists
//nodeformcols
search_api
//search_api
search_api_page
search_api_solr
//search_api_solr
termreferencetree
views
wysiwyg
@@ -27,4 +27,3 @@ views_rss_media
node_export :
- https://www.drupal.org/node/1869918
- https://www.drupal.org/node/1911638
@@ -0,0 +1 @@
.DS_Store
@@ -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.
@@ -0,0 +1,539 @@
<?php
/**
* Menu Callback. Handles the export of the translatable entities.
*/
function entity_translation_export_import_admin_export_form($form, $form_state){
$form = array('#tree' => TRUE);
// Get translatable entity types.
$enabled = variable_get('entity_translation_entity_types', array());
$info = entity_get_info();
$entities_options = array();
$bundles_options = array();
$fields_options = array();
// Get the translatable fields foreach translatable entity_type.
foreach ($enabled as $entity_type) {
if (entity_translation_enabled($entity_type)) {
$bundles = !empty($info[$entity_type]['bundles']) ? array_keys($info[$entity_type]['bundles']) : array($entity_type);
foreach ($bundles as $bundle) {
$settings = entity_translation_settings($entity_type, $bundle);
if (entity_translation_enabled_bundle($entity_type, $bundle) && ($handler = entity_translation_get_handler($entity_type, $bundle))) {
$fields_info = field_info_instances($entity_type, $bundle);
foreach ($fields_info as $field_name => $value) {
$field = field_info_field($field_name);
if(field_is_translatable($entity_type, $field) and in_array($field['type'], array('text', 'text_long', 'text_with_summary')) and $field['cardinality'] == 1){
if(!array_key_exists($entity_type, $entities_options)){
$entities_options[$entity_type] = $info[$entity_type]['label'];
}
if(!isset($bundles_options[$entity_type][$bundle])){
$bundles_options[$entity_type][$bundle] = $info[$entity_type]['bundles'][$bundle]['label'];
}
$field_value = $entity_type . ':' . $bundle . ':' . $field_name;
if(!isset($fields_options[$entity_type][$bundle][$field_value])){
$fields_options[$entity_type][$bundle][$field_value] = $value['label'];
}
}
}
}
}
}
}
// If there is no translatable field yet.
if(empty($fields_options)){
$form['no_result'] = array('#markup' => '<p>' . t('You need to enable entity translation for at least one field.') . '</p>');
}
else{
drupal_set_title(t('Export entity translations'));
$form['export'] = array(
'#type' => 'fieldset',
'#title' => t('Export Settings'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
);
$form['export']['entity_type'] = array(
'#type' => 'checkboxes',
'#options' => $entities_options,
'#ajax' => array(
'callback' => 'entity_translation_export_import_admin_export_bundle_callback',
'wrapper' => 'entity-type-wrapper',
),
);
$form['export']['entity_type_wrapper'] = array(
'#type' => 'container',
'#id' => 'entity-type-wrapper',
);
if(isset($form_state['values']['export']['entity_type'])){
foreach($form_state['values']['export']['entity_type'] as $entity_type => $entity_type_value){
if($entity_type_value){
$form['export']['entity_type_wrapper'][$entity_type] = array(
'#type' => 'fieldset',
'#title' => $entities_options[$entity_type],
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
foreach($bundles_options[$entity_type] as $bundle_option){
$form['export']['entity_type_wrapper'][$entity_type]['bundle'] = array(
'#type' => 'checkboxes',
'#options' => $bundles_options[$entity_type],
'#ajax' => array(
'callback' => 'entity_translation_export_import_admin_export_fields_callback',
'wrapper' => drupal_html_class($entity_type . '-bundle-wrapper'),
),
);
$form['export']['entity_type_wrapper'][$entity_type]['bundle_wrapper'] = array(
'#type' => 'container',
'#id' => drupal_html_class($entity_type . '-bundle-wrapper'),
);
if(isset($form_state['values']['export']['entity_type_wrapper'][$entity_type]['bundle'])){
foreach($form_state['values']['export']['entity_type_wrapper'][$entity_type]['bundle'] as $bundle => $bundle_value){
if($bundle_value){
$form['export']['entity_type_wrapper'][$entity_type]['bundle_wrapper'][$bundle] = array(
'#type' => 'fieldset',
'#title' => $bundles_options[$entity_type][$bundle],
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
foreach($bundles_options[$entity_type] as $bundle_option){
$form['export']['entity_type_wrapper'][$entity_type]['bundle_wrapper'][$bundle]['field'] = array(
'#type' => 'checkboxes',
'#options' => $fields_options[$entity_type][$bundle],
);
}
}
}
}
}
}
}
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => t('Export Translations'),
);
}
return $form;
}
/**
* Ajax callback.
*/
function entity_translation_export_import_admin_export_bundle_callback($form, $form_state){
$parents = array_slice($form_state['triggering_element']['#array_parents'], 0, -2);
$element = drupal_array_get_nested_value($form, $parents);
return $element['entity_type_wrapper'];
}
/**
* Ajax callback.
*/
function entity_translation_export_import_admin_export_fields_callback($form, $form_state){
$parents = array_slice($form_state['triggering_element']['#array_parents'], 0, -2);
$element = drupal_array_get_nested_value($form, $parents);
return $element['bundle_wrapper'];
}
/**
* Submit callback.
*/
function entity_translation_export_import_admin_export_form_validate($form, $form_state){
// Get all entities to export.
$at_least_one_field = FALSE;
if(isset($form_state['values']['export']['entity_type_wrapper'])){
foreach($form_state['values']['export']['entity_type_wrapper'] as $entity_type => $entity_type_value) {
if($entity_type_value and isset($entity_type_value['bundle_wrapper'])){
foreach($entity_type_value['bundle_wrapper'] as $bundle => $bundle_value) {
if($bundle_value){
foreach($bundle_value['field'] as $field_value){
if($field_value){
$at_least_one_field = TRUE;
}
}
}
}
}
}
}
if(!$at_least_one_field){
form_set_error('', t('You need to select at least one field.'));
}
}
/**
* Submit callback.
*/
function entity_translation_export_import_admin_export_form_submit($form, $form_state){
// Get all entities to export.
$entities = array();
foreach($form_state['values']['export']['entity_type_wrapper'] as $entity_type => $entity_type_value) {
if($entity_type_value){
foreach($entity_type_value['bundle_wrapper'] as $bundle => $bundle_value) {
if($bundle_value){
foreach($bundle_value['field'] as $field_value){
if($field_value){
$parts = explode(':', $field_value);
$entities[$parts[0]][$parts[1]][] = $parts[2];
}
}
}
}
}
}
$limit = 500;
$filename = 'public://entity_translation_export.csv';
variable_set('entity_translation_export_import_file', '');
$batch = array(
'operations' => array(
array('entity_translation_export_import_export_process', array($entities, $limit, $filename)),
),
'finished' => 'entity_translation_export_import_export_finished',
'title' => t('Export Translations'),
'init_message' => t('Retrieving Entities...'),
'progress_message' => t('Processed @current out of @total.'),
'error_message' => t('Exporting has encountered an error.'),
'file' => drupal_get_path('module', 'entity_translation_export_import') . '/entity_translation_export_import.admin.inc',
);
batch_set($batch);
batch_process('admin/config/regional/entity_translation/export');
}
/**
* Batch process callback.
*/
function entity_translation_export_import_export_process($entities, $limit, $filename, &$context){
// Set the default progress data.
if (!isset($context['sandbox']['progress'])) {
$context['results']['file'] = file_save_data('', $filename);
$context['sandbox']['progress'] = array();
$context['sandbox']['progress_total']['max'] = 0;
$context['sandbox']['progress_total']['current'] = 0;
foreach($entities as $entity_type => $bundles){
foreach($bundles as $bundle => $fields){
$query = new EntityFieldQuery();
$context['sandbox']['progress'][$entity_type][$bundle]['max'] = $query->entityCondition('entity_type', $entity_type)->entityCondition('bundle', $bundle)->count()->execute();
$context['sandbox']['progress'][$entity_type][$bundle]['current'] = 0;
$context['sandbox']['progress_total']['max'] += $context['sandbox']['progress'][$entity_type][$bundle]['max'];
}
}
}
if(!empty($context['sandbox']['progress'])){
$entity_type = key($context['sandbox']['progress']);
$bundle = key(current($context['sandbox']['progress']));
$progress = current(current($context['sandbox']['progress']));
// Receive items.
$items = _entity_translation_export_import_export_entity($entity_type, $bundle, $progress['current'], $limit, $entities[$entity_type][$bundle]);
$is_header = ($context['sandbox']['progress_total']['current'] === 0);
$data = _entity_translation_export_import_csv($items, $is_header);
file_put_contents($context['results']['file']->uri, $data, FILE_APPEND);
$progress['current'] += $limit;
$context['sandbox']['progress_total']['current'] += ($progress['current'] > $progress['max'] ? ($progress['max'] - ($progress['current']-$limit)) : $limit);
$context['sandbox']['progress'][$entity_type][$bundle]['current'] = $progress['current'];
if($progress['current'] >= $progress['max']){
unset($context['sandbox']['progress'][$entity_type][$bundle]);
if(empty($context['sandbox']['progress'][$entity_type])){
unset($context['sandbox']['progress'][$entity_type]);
}
}
if(!empty($context['sandbox']['progress'])){
reset($context['sandbox']['progress']);
$entity_type = key($context['sandbox']['progress']);
$bundle = key(current($context['sandbox']['progress']));
$progress = current(current($context['sandbox']['progress']));
$percentage = '';
if(isset($progress['current']) and $progress['current'] > 0){
$percentage = ' (' . $progress['current'] . '/' . $progress['max'] . ')';
}
$info = entity_get_info();
$context['message'] = t('Exporting ' . $info[$entity_type]['label'] . ' - ' . $info[$entity_type]['bundles'][$bundle]['label'] . $percentage);
}
}
$context['finished'] = ($context['sandbox']['progress_total']['current']/$context['sandbox']['progress_total']['max']);
}
/**
* batch process finish callback.
*/
function entity_translation_export_import_export_finished($success, $results){
$link = file_create_url($results['file']->uri);
drupal_set_message(t('Export has been created! Download the <a href="@link">file</a>.', array('@link' => $link)));
}
/**
* Get the field values of an entity.
*/
function _entity_translation_export_import_export_entity($entity_type, $bundle, $limitstart, $limit, $fields){
$query = new EntityFieldQuery();
$result = $query->entityCondition('entity_type', $entity_type)
->entityCondition('bundle', $bundle)
->entityOrderBy('entity_id')
->range($limitstart, $limit)
->execute();
// Force Default Language to be first.
$languages = language_list();
$language_default = language_default();
unset($languages[$language_default->language]);
$languages = array_merge(array($language_default->language => $language_default), $languages);
$export = array();
foreach($result as $entity_type => $items){
foreach($items as $entity_id => $item){
foreach($languages as $language){
$entity = current(entity_load($entity_type, array($entity_id)));
$export[$entity_id]['Entity Type (Not Editable)'] = $entity_type;
$export[$entity_id]['Bundle (Not Editable)'] = $bundle;
$export[$entity_id]['Entity ID (Not Editable)'] = $entity_id;
$handler = entity_translation_get_handler($entity_type, $entity, TRUE);
$translations = $handler->getTranslations();
foreach($fields as $field){
$value = '';
if(isset($translations->data[$language->language])){
$value = $entity->{$field}[$language->language][0]['value'];
}
$export[$entity_id][$field][$language->language] = $value;
}
}
}
}
return $export;
}
/**
* Format entity values as csv.
*/
function _entity_translation_export_import_csv($items, $is_header = FALSE){
$data = '';
// Render Header.
if($is_header){
$titles = array();
foreach(reset($items) as $field_name => $value){
if(is_array($value)){
foreach($value as $lang_code => $language_value){
$titles[] = $field_name . '_' . $lang_code;
}
}
else{
$titles[] = $field_name;
}
}
$data .= implode(",", $titles) . "\n";
}
foreach($items as $item){
$row = array();
foreach($item as $field_name => $value){
if(is_array($value)){
foreach($value as $language_value){
$row[] = '"' . str_replace('"', '""', $language_value) . '"';
}
}
else{
$row[] = '"' . str_replace('"', '""', $value) . '"';
}
}
$data .= implode(",", $row) . "\n";
}
return $data;
}
/**
* Import form callback.
*/
function entity_translation_export_import_admin_import_form($form, $form_state){
$form = array('#attributes' => array('enctype' => "multipart/form-data"));
$form['fid'] = array(
'#type' => 'managed_file',
'#title' => 'Translation File for Import',
'#upload_validators' => array('file_validate_extensions' => array('csv')),
'#description' => t('WARNING! Do not upload any file that is not formated as needed. Please try to export the translations first.'),
);
$fid = variable_get('entity_translation_export_import_file', '');
if(file_load($fid)){
$form['fid']['#default_value'] = $fid;
}
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Import Translations'
);
return $form;
}
/**
* Import form submit callback.
*/
function entity_translation_export_import_admin_import_form_submit($form, $form_state){
$file = file_load($form_state['values']['fid']);
variable_set('entity_translation_export_import_file', $file->fid);
$batch = array(
'operations' => array(
array('entity_translation_export_import_import_process', array($file)),
),
'finished' => 'entity_translation_export_import_import_finished',
'title' => t('Import Translations'),
'init_message' => t('Retrieving Entities...'),
'progress_message' => t('Processed @current out of @total.'),
'error_message' => t('Importing has encountered an error.'),
'file' => drupal_get_path('module', 'entity_translation_export_import') . '/entity_translation_export_import.admin.inc',
);
batch_set($batch);
batch_process('admin/config/regional/entity_translation/import');
}
/**
* Batch process callback.
*/
function entity_translation_export_import_import_process($file, &$context){
// Initialize Sandbox Data.
if (!isset($context['sandbox']['current'])) {
$context['sandbox']['total'] = filesize($file->uri);
$context['sandbox']['current'] = 0;
}
if($context['sandbox']['current'] < $context['sandbox']['total']){
$fp = @fopen($file->uri, "r");
// Set the current csv pointer.
@fseek($fp, $context['sandbox']['current']);
if($data = fgetcsv($fp)){
if(!isset($context['sandbox']['header'])){
$context['sandbox']['header'] = $data;
}
else{
entity_translation_export_import_import_translation(array_combine($context['sandbox']['header'], $data));
}
}
// Get the current csv pointer.
$context['sandbox']['current'] = @ftell($fp);
}
$context['finished'] = ($context['sandbox']['current']/$context['sandbox']['total']);
}
/**
* Batch process finish callback.
*/
function entity_translation_export_import_import_finished($success, $results){
drupal_set_message(t('Entity Translations have been imported successfully!'));
}
/**
* Imports entity translations.
*/
function entity_translation_export_import_import_translation($data){
$entity_type = $data['Entity Type (Not Editable)'];
$bundle = $data['Bundle (Not Editable)'];
$entity_id = $data['Entity ID (Not Editable)'];
$entities = entity_load($entity_type, array($entity_id));
// If cannot load entity.
if(empty($entities)){
drupal_set_message(t('Entity with id @id could not be found in database.', array('@id' => $entity_id)));
return FALSE;
}
// Get the entity.
$entity = current($entities);
// Get the default entity's language.
$source_language = entity_language($entity_type, $entity);
// Get the current entity's field info.
$fields_info = field_info_instances($entity_type, $bundle);
$values = array();
$languages = array();
foreach($data as $key => $value){
if(strstr($key, '_') and !empty($value)){
$parts = explode('_', $key);
$field_language = array_pop($parts);
$field_name = implode('_', $parts);
// If field is translatable.
if(array_key_exists($field_name, $fields_info)){
$field = field_info_field($field_name);
if(field_is_translatable($entity_type, $field) and in_array($field['type'], array('text', 'text_long', 'text_with_summary')) and $field['cardinality'] == 1){
if(!in_array($field_language, $languages)){
$languages[] = $field_language;
}
if(!isset($entity->{$field_name}[$field_language])){
$entity->{$field_name}[$field_language] = $entity->{$field_name}[$source_language];
}
$entity->{$field_name}[$field_language][0]['value'] = $value;
}
}
}
}
$handler = entity_translation_get_handler($entity_type, $entity, TRUE);
if($handler){
foreach($languages as $langcode){
$translation = array(
'translate' => 0,
'status' => 1,
'source' => ($source_language != $langcode) ? $source_language : '',
'language' => $langcode,
);
$handler->setTranslation($translation);
// If original entity and module title exists then do the replacements.
if($source_language == $langcode and module_exists('title')){
$fr_info = title_field_replacement_info($entity_type);
foreach($fr_info as $legacy_field => $info) {
if(isset($entity->{$info['field']['field_name']})){
drupal_static_reset('field_language');
title_field_sync_get($entity_type, $entity, $legacy_field, $info, $langcode);
}
}
}
}
}
entity_save($entity_type, $entity);
}
@@ -0,0 +1,13 @@
name = Entity Translation Export/Import
description = Exports/Imports entity translation fields. Very useful for fast updating of taxonomy terms, nodes etc.
core = 7.x
package = Multilingual - Entity Translation
dependencies[] = entity
dependencies[] = entity_translation
; Information added by drupal.org packaging script on 2013-09-30
version = "7.x-1.0-beta3+0-dev"
core = "7.x"
project = "entity_translation_export_import"
datestamp = "1380577025"
@@ -0,0 +1,37 @@
<?php
/**
* Implements hook_menu().
*/
function entity_translation_export_import_menu() {
$items['admin/config/regional/entity_translation/export'] = array(
'title' => 'Export',
'page callback' => 'drupal_get_form',
'page arguments' => array('entity_translation_export_import_admin_export_form'),
'access arguments' => array('administer entity translation'),
'type' => MENU_LOCAL_TASK,
'file' => 'entity_translation_export_import.admin.inc',
'file path' => drupal_get_path('module', 'entity_translation_export_import'),
);
$items['admin/config/regional/entity_translation/import'] = array(
'title' => 'Import',
'page callback' => 'drupal_get_form',
'page arguments' => array('entity_translation_export_import_admin_import_form'),
'access arguments' => array('administer entity translation'),
'type' => MENU_LOCAL_TASK,
'file' => 'entity_translation_export_import.admin.inc',
'file path' => drupal_get_path('module', 'entity_translation_export_import'),
);
return $items;
}
/**
* Implements hook_menu_alter().
*/
function entity_translation_export_import_menu_alter(&$items) {
$items['admin/config/regional/entity_translation/settings'] = $items['admin/config/regional/entity_translation'];
$items['admin/config/regional/entity_translation/settings']['type'] = MENU_DEFAULT_LOCAL_TASK;
}
@@ -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.
@@ -0,0 +1,95 @@
CONTENTS OF THIS FILE
---------------------
* Introduction
* Installing
* Uninstalling
* Frequently Asked Questions (FAQ)
* Known Issues
* More Information
* How Can You Contribute?
INTRODUCTION
------------
Current Maintainer: Sami Radi a.k.a VirtuoWorks <http://drupal.org/user/656630>
Remove Duplicates removes duplicate nodes
according to any selected content field.
This can be very useful with Drupal feeds module :
- http://drupal.org/project/feeds
This module was inspired by these threads :
- http://drupal.org/node/720190 : Remove Duplicate Nodes based on title
- http://drupal.org/node/1211922 : remove duplicated node base on title problem
THANKS
------
Thanks to dman <https://drupal.org/user/33240> for endorsing this project.
He wrote a sandbox module "Deduplicate Nodes" for D6 intended to deduplicate
nodes according to titles only : https://drupal.org/sandbox/dman/1422586
INSTALLING
----------
See http://drupal.org/getting-started/install-contrib
for instructions on how to install or update Drupal modules.
Once Remove Duplicates is installed and enabled,
you can use it at admin/config/content/remove_duplicates.
It is highly recommended that you backup your database before using this module.
UNINSTALLING
------------
Because Drupal does not uninstall modules in reverse
order of their dependencies, if you want to uninstall
all the Remove Duplicates modules, be sure to disable
and uninstall all the sub-modules before the base Remove
Duplicates module.
To help fix this bug in Drupal core, visit http://drupal.org/node/151452.
FREQUENTLY ASKED QUESTIONS (FAQ)
--------------------------------
- There are no frequently asked questions at this time.
KNOWN ISSUES
------------
- See http://drupal.org/project/issues/2005296
for a list of the current known issues.
MORE INFORMATION
----------------
- To issue any bug reports, feature or support requests
see the module issue queue at
http://drupal.org/project/issues/2005296
- For additional documentation, see the online module handbook at
https://drupal.org/project/remove_duplicates
HOW CAN YOU CONTRIBUTE?
-----------------------
- Report any bugs, feature requests, etc. in the issue tracker.
http://drupal.org/node/add/project-issue/2005296
- Help translate this module.
- Write a review for this module at drupalmodules.com.
http://drupalmodules.com/
- Help keep development active by donating to the developers.
http://www.virtuoworks.com/
@@ -0,0 +1,12 @@
name = Remove Duplicates
description = "Remove duplicate nodes according to node fields or CCK fields."
core = 7.x
package = Content management
configure = admin/config/content/remove_duplicates
; Information added by Drupal.org packaging script on 2015-04-13
version = "7.x-1.4"
core = "7.x"
project = "remove_duplicates"
datestamp = "1428944507"
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
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.
@@ -0,0 +1,41 @@
Special Menu Items Module
------------------------
Written by Tamir Al Zoubi and Karim Djelid - Servit Open Source Solutions - www.servit.ch
Description
-----------
Special Menu Items is module that enables placeholder and separator menu items.Placeholder is a menu item which is
actually not a link. Something like this is useful with drop down menus where we want to have a parent link which
is actually not linking to a page but which is just acting as a parent grouping some children below it.
A separator menu item is something like "-------" which is also not linking anywhere but merely a mean to structure menus.
This module depends on the Menu module. It is recommended that the SimpleMenu module or another drop down menu module
is used, or you will not be able to acess children of nolink menu items.
Features
--------
- User can create a new menu item and place either "<nolink>" or "<separator>" in the Path field, without quotes.
- When the menu is rendered the "nolink" item will be rendered similar to a normal menu link item, but there will
be no link, just the title. Since version 1.3 you can change HTML tag used for menu item.
- When the menu is rendered the "separator" item will be rendered as an item which has no link,
and the default title will be "-------". Since version 1.3 it is possible to change both the HTML tag and title.
- Breadcrumb of "<nolink>" will be rendered same as "<nolink>" menu item.
- CSS class "nolink" is added to "<nolink>" menu item.
- CSS class "seperator" is added to "<seperator>" menu item.
- Compatible with the Sitemap module.
Installation
------------
1. Copy the special_menu_items folder to your sites/all/modules directory.
2. At Administer -> Site building -> Modules (admin/modules) enable the module.
3. Configure the module settings at Administer -> Site configuration -> Special Menu Items (admin/config/system/special_menu_items).
Upgrading
---------
Just overwrite (or replace) the older special_menu_items folder with the newer version.
Contact
-------
This module is developed by Servit Open Source Solutions - http://servit.ch
and maintained by Khaled Zaidan - zaidan@servit.ch
@@ -0,0 +1,13 @@
name = Special menu items
description = "Allow users to add placeholder and/or separator menu items."
core = 7.x
dependencies[] = menu
configure = admin/config/system/special_menu_items
; Information added by drupal.org packaging script on 2012-09-04
version = "7.x-2.0"
core = "7.x"
project = "special_menu_items"
datestamp = "1346788411"
@@ -0,0 +1,38 @@
<?php
/**
* Implementation of hook_uninstall().
*/
function special_menu_items_uninstall() {
variable_del('special_menu_items_nolink_tag');
variable_del('special_menu_items_seperator_tag');
variable_del('special_menu_items_seperator_value');
variable_del('special_menu_items_menu_item_link');
}
/**
* "nolink" to <nolink> and "separator" to <separator>
*/
function special_menu_items_update_7000(&$sandbox){
//change "nolink" to <nolink>
db_update('menu_links')
->fields(array(
'link_path' => '<nolink>',
'router_path' => '<nolink>',
))
->condition('router_path', 'nolink', '=')
->execute();
//change "separator" to <separator>
db_update('menu_links')
->fields(array(
'link_path' => '<separator>',
'router_path' => '<separator>',
))
->condition('router_path', 'separator', '=')
->execute();
//we don't need this variable anymore
variable_del('special_menu_items_menu_item_link');
}
@@ -0,0 +1,199 @@
<?php
/**
* @file
* Module to enable placeholder or separator menu items.Placeholder is a menu item which is
* actually not a link. Something like this is useful with drop down menus where we want to
* have a parent link which is actually not linking to a page but which is just acting as a
* parent and grouping some children below it.
* A separator menu item is something like "-------" which is also not linking anywhere but
* merely a mean to structure menus.
*
* Written by Tamir Al Zoubi and Karim Djelid - Servit Open Source Solutions - www.servit.ch
*/
/**
*Implementation of hook_menu()
*/
function special_menu_items_menu() {
$items['<nolink>'] = array(
'page callback' => 'drupal_not_found',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['<separator>'] = array(
'page callback' => 'drupal_not_found',
'access callback' => TRUE,
'type' => MENU_CALLBACK,
);
$items['admin/config/system/special_menu_items'] = array(
'title' => 'Special Menu Items',
'description' => 'Configure Special Menu Items.',
'page callback' => 'drupal_get_form',
'page arguments' => array('special_menu_items_admin_settings_form'),
'access arguments' => array('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
);
return $items;
}
/**
* Override of theme_link()
* This function will render link if it is "nolink" or "separator". Otherwise it will call originally
* overwritten menu_item_link function.
*/
function special_menu_items_link(array $variables) {
if (in_array($variables['path'], array('<nolink>', '<separator>'))) {
switch ($variables['path']) {
case '<nolink>':
$tag = variable_get('special_menu_items_nolink_tag', '<span>');
$title = $variables['options']['html'] ? $variables['text'] : check_plain($variables['text']);
$variables['options']['attributes']['class'][] = 'nolink';
break;
case '<separator>':
$tag = variable_get('special_menu_items_separator_tag', '<span>');
$title = variable_get('special_menu_items_separator_value', '<hr>');
$variables['options']['attributes']['class'][] = 'separator';
break;
}
$attributes = drupal_attributes($variables['options']['attributes']);
if ($tag != '<a>') {
// <a> tags can have these but a <span> cannot, so we remove them.
foreach (array('accesskey', 'target', 'rel', 'name') as $attribute) {
$attributes = preg_replace("/ $attribute=\".*\"/i", "", $attributes);
}
}
return special_menu_items_render_menu_item($tag, $title, $attributes);
}
// Call the original theme function for normal menu link.
return theme('special_menu_items_link_default', $variables);
}
/**
* Returns menu item rendered.
*/
function special_menu_items_render_menu_item($tag, $value, $attrs = array()) {
// $attrs may be a string already or an array
if (is_array($attrs)) {
$attrs = drupal_attributes($attrs);
}
$length = strlen($tag);
if ($tag[0] == '<' && $tag[$length - 1] == '>') {
$tag = substr($tag, 1, $length-2);
}
$closingtag = explode(' ', $tag,2);
$closingtag = '</' . $closingtag[0] . '>';
$tag = '<' . $tag . $attrs . '>';
return $tag . $value . $closingtag;
}
/**
* Implementation of hook_theme_registry_alter()
* We replace theme_menu_item_link with our own function.
*/
function special_menu_items_theme_registry_alter(&$registry) {
// Save previous value from registry in case another theme overwrites menu_item_link
$registry['special_menu_items_link_default'] = $registry['link'];
$registry['link']['function'] = 'special_menu_items_link';
}
/**
* Implementation of hook_form_FROM_ID_alter()
* Description changed, added nolink and separator as path types.
*/
function special_menu_items_form_menu_edit_item_alter(&$form, &$form_state) {
// Some menu items have a pre-defined path which cannot be modified hence no default_value
if (isset($form['link_path']['#default_value'])) {
$default_value = $form['link_path']['#default_value'];
if (preg_match('/^<nolink>\/[0-9]+$/', $default_value)) {
$default_value = '<nolink>';
}
elseif (preg_match('/^<separator>\/[0-9]+$/', $default_value)) {
$default_value = '<separator>';
}
$form['link_path']['#default_value'] = $default_value;
$form['link_path']['#description'] .= ' ' . t('Enter "%nolink" to generate non-linkable item, enter "%separator" to generate separator item.', array('%nolink' => '<nolink>', '%separator' => '<separator>'));
}
}
/**
* Implementation of hook_init().
*/
function special_menu_items_init() {
// Make breadcrumb of nolink menu item nonlinkable.
$breadcrumb = drupal_get_breadcrumb();
foreach($breadcrumb as $key => $crumb){
if (strlen(strstr($crumb,'<nolink>')) > 0) {
$crumb = strip_tags($crumb);
$tag = variable_get('special_menu_items_nolink_tag', '<span>');
$breadcrumb[$key] = special_menu_items_render_menu_item($tag, $crumb);
}
}
drupal_set_breadcrumb($breadcrumb);
}
/**
* Special Menu Items admin settings form.
*
* @return
* The settings form used by Special Menu Items.
*/
function special_menu_items_admin_settings_form() {
$form['special_menu_items_nolink_tag'] = array(
'#type' => 'textfield',
'#title' => t('HTML tag for "nolink"'),
'#description' => t('By default, Special Menu Items will use a span tag for the nolink menu item. Here you can specify your own tag.'),
'#default_value' => variable_get('special_menu_items_nolink_tag', '<span>'),
);
$form['special_menu_items_separator_tag'] = array(
'#type' => 'textfield',
'#title' => t('HTML tag for "separator"'),
'#description' => t('By default, Special Menu Items will use a span tag for the separator menu item. Here you can specify your own tag.'),
'#default_value' => variable_get('special_menu_items_separator_tag', '<span>'),
);
$form['special_menu_items_separator_value'] = array(
'#type' => 'textfield',
'#title' => t('Value to be displayed for the "separator"'),
'#description' => t('By default, Special Menu Items will use a "&lt;hr&gt;" value for the separator. You can specify your own value for the separator.'),
'#default_value' => variable_get('special_menu_items_separator_value', '<hr>'),
);
return system_settings_form($form);
}
/**
* Implements hook_menu_link_update()
*
*/
/*
function special_menu_items_menu_link_update($link) {
//do all links in db
global $db_type;
if ($db_type == 'pgsql') {
db_query("UPDATE {menu_links} SET link_path=link_path||'/'||mlid WHERE (link_path='<nolink>' OR link_path='<separator>') AND hidden != -1");
}
else {
db_query("UPDATE {menu_links} SET link_path=CONCAT(CONCAT(link_path,'/'),mlid) WHERE (link_path='<nolink>' OR link_path='<separator>') AND hidden!=-1");
}
}
*
*/
@@ -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.
@@ -0,0 +1,21 @@
/**
* @file
* Styles for taxonomy wrangler
*/
.taxonomy-wrangler-accordion-item {
float: left;
font-size: 1.35em;
min-width: 15px;
padding-right: 5px;
padding-left: 5px;
color: #656565;
}
.taxonomy-wrangler-accordion-item:hover {
text-decoration: none;
}
.taxonomy-wrangler-accordion-item--parent span:after {
content: "\25be"; /*right pointing small triangle*/
}
.taxonomy-wrangler-accordion-item--parent.is-collapsed span:after {
content: "\25b8"; /*down pointing small triangle*/
}
@@ -0,0 +1,591 @@
/**
* @file
* js for taxonomy ui tweaks
*/
(function ($, Drupal, window, document, undefined) {
function TaxonomyWranglerUID() {
var prefix = 'taxonomy-wrangler-';
var randomId = prefix + this.count;
while($('#' + randomId).length) {
this.count++;
randomId = prefix + this.count;
}
return randomId;
}
TaxonomyWranglerUID.count = 0;
function TaxonomyWranglerAccordion(table) {
this.table = table;
this.rows = $('tbody tr', table);
this.init();
}
TaxonomyWranglerAccordion.instances = {};
var ctrlDown = false;
$(window).keydown(function(e) {
if (e.keyCode == 17) {
ctrlDown = true;
}
}).keyup(function(e) {
if (e.keyCode == 17) {
ctrlDown = false;
}
});
TaxonomyWranglerAccordion.prototype.init = function() {
var accordion = this;
$('tbody tr').not(':eq(0)').each(function(){
var $row = $(this);
accordion.collapseTree($row);
accordion.attachAccordionEvents($row);
});
};
TaxonomyWranglerAccordion.prototype.attachAccordionEvents = function($row) {
var accordion = this;
$row.find('.taxonomy-wrangler-accordion-item').bind('click.tweaksAccordion', function(e){
e.preventDefault();
if (ctrlDown) {
accordion.toggleTree($row);
}
else {
accordion.toggleRow($row);
}
return false;
});
}
TaxonomyWranglerAccordion.prototype.refreshRows = function(){
var accordion = this;
this.rows.not(':eq(0)').each(function(){
var $this = $(this);
var hasChildren = accordion.rows.filter('[data-parent="'+ $this.data('tid') +'"]').length > 0 ? true : false;
var $collapseLink = $this.find('.taxonomy-wrangler-accordion-item--parent');
//create a new parent
if (hasChildren && !$collapseLink.length) {
$this.find('.tabledrag-handle').after('<a href="#" class="taxonomy-wrangler-accordion-item taxonomy-wrangler-accordion-item--parent"><span></span></a>');
accordion.attachAccordionEvents($this);
}
else if (!hasChildren && $collapseLink.length) {
$collapseLink.remove();
}
});
};
/**
* This is a bit different than a typical accordion in that the
* data structure is hierarchical, but its representation in the DOM is flat
*/
TaxonomyWranglerAccordion.prototype.showRow = function($row) {
$row.css('display', 'table-row').removeClass('tabledrag-hidden');
}
TaxonomyWranglerAccordion.prototype.hideRow = function($row) {
$row.css('display', 'none').addClass('tabledrag-hidden');
}
TaxonomyWranglerAccordion.prototype.setCollapsed = function($row, collapsed, recurse) {
var accordion = this;
var $children = this.rows.filter('[data-parent=' + $row.data('tid') + ']');
if (collapsed && $children.length) {
$row.data('is-collapsed', true);
$row.find('.taxonomy-wrangler-accordion-item').addClass('is-collapsed');
if ($children.length) {
$row.addClass('tabledrag-leaf');
}
}
else {
$row.data('is-collapsed', false).removeClass('tabledrag-leaf');
$row.find('.taxonomy-wrangler-accordion-item').removeClass('is-collapsed');
}
if (recurse) {
accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
accordion.setCollapsed($(this), collapsed, true);
});
}
}
/*
* Hide all children
*/
TaxonomyWranglerAccordion.prototype.hideChildren = function($row) {
var $item = $row.find('.taxonomy-wrangler-accordion-item');
var accordion = this;
this.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
accordion.hideRow($(this));
accordion.hideChildren($(this));
});
};
/**
* Show children if they are not collapsed
*/
TaxonomyWranglerAccordion.prototype.showChildren = function($row) {
var accordion = this;
accordion.rows.filter('[data-parent=' + $row.data('tid') + ']').each(function(){
accordion.showRow($(this));
if (!$(this).data('is-collapsed')) {
accordion.showChildren($(this));
}
});
};
TaxonomyWranglerAccordion.prototype.toggleRow = function($row) {
if ($row.data('is-collapsed')) {
this.setCollapsed($row, false);
this.showChildren($row);
}
else {
this.setCollapsed($row, true);
this.hideChildren($row);
}
};
TaxonomyWranglerAccordion.prototype.expandTree = function($row) {
this.setCollapsed($row, false, true);
this.showChildren($row);
};
TaxonomyWranglerAccordion.prototype.collapseTree = function($row) {
this.setCollapsed($row, true, true);
this.hideChildren($row);
};
TaxonomyWranglerAccordion.prototype.toggleTree = function($row) {
if ($row.data('is-collapsed')) {
this.expandTree($row);
}
else {
this.collapseTree($row);
}
}
Drupal.behaviors.taxonomyWranglerAccordion = {
attach: function(context, settings) {
idCount = 0;
$('.taxonomy-wrangler-accordion', context)
.not('.taxonomy-wrangler-accordion-processed')
.addClass('taxonomy-wrangler-accordion-processed')
.each(function(){
var id = $(this).attr('id')
if (!!!id || id == '') {
$(this).attr('id', TaxonomyWranglerUID());
}
TaxonomyWranglerAccordion.instances[id] = new TaxonomyWranglerAccordion(this);
});
}
};
function TaxonomyWranglerCollection() {
this.updatedTerms = {};
};
TaxonomyWranglerCollection.prototype.updateTerm = function(row) {
var $row = $(row);
var tid = $row.data('tid');
var now = Date.now();
this.updatedTerms[tid] = {
"tid": $row.data('tid'),
"parent": $row.data('parent'),
"depth": $row.data('depth'),
"weight": $row.data('weight'),
"updated": now
}
//el.attr('data-{key}') & el.data('key') are not the same!
$row.attr('data-parent', $row.data('parent'))
.attr('data-depth', $row.data('depth'))
.attr('data-weight', $row.data('weight'))
.data('updated', now);
};
/**
* New TableDrag "class" that inherits Drupal.tableDrag
*/
function TaxonomyWranglerDrag(table, tableSettings) {
Drupal.tableDrag.call(this, table, tableSettings);
}
TaxonomyWranglerDrag.prototype = new Drupal.tableDrag();
TaxonomyWranglerDrag.instances = {};
/**
* Find the target used within a particular row and group.
* This isn't strictly necessary in this case because the data
* is stored on the row element itself.
* Copied and modified from misc/tabledrag.js
*/
TaxonomyWranglerDrag.prototype.rowSettings = function (group, row) {
for (var delta in this.tableSettings[group]) {
// Return a copy of the row settings.
var rowSettings = {};
for (var n in this.tableSettings[group][delta]) {
rowSettings[n] = this.tableSettings[group][delta][n];
}
return rowSettings;
}
};
/**
* After the row is dropped, update the table fields according to the settings
* set for this table.
*
* @param changedRow
* DOM object for the row that was just dropped.
*/
TaxonomyWranglerDrag.prototype.updateFields = function (changedRow) {
var $c = $(changedRow);
for (var group in this.tableSettings) {
// Each group may have a different setting for relationship, so we find
// the source rows for each separately.
this.updateField(changedRow, group);
}
};
/**
* After the row is dropped, update a single table field according to specific
* settings. This code is cribbed from tabledrag.js and modified to
* use the data properties on the row object rather than input fields in the row.
*
* @param changedRow
* DOM object for the row that was just dropped.
* @param group
* The settings group on which field updates will occur.
*/
TaxonomyWranglerDrag.prototype.updateField = function (changedRow, group) {
var rowSettings = this.rowSettings(group, changedRow);
// Set the row as its own target.
if (rowSettings.relationship == 'self' || rowSettings.relationship == 'group') {
var sourceRow = changedRow;
}
// Siblings are easy, check previous and next rows.
else if (rowSettings.relationship == 'sibling') {
var previousRow = $(changedRow).prev('tr').get(0);
var nextRow = $(changedRow).next('tr').get(0);
var sourceRow = changedRow;
if ($(previousRow).is('.draggable') && $('.' + group, previousRow).length) {
if (this.indentEnabled) {
if ($('.indentations', previousRow).length == $('.indentations', changedRow)) {
sourceRow = previousRow;
}
}
else {
sourceRow = previousRow;
}
}
else if ($(nextRow).is('.draggable') && $('.' + group, nextRow).length) {
if (this.indentEnabled) {
if ($('.indentations', nextRow).length == $('.indentations', changedRow)) {
sourceRow = nextRow;
}
}
else {
sourceRow = nextRow;
}
}
}
// Parents, look up the tree until we find a field not in this group.
// Go up as many parents as indentations in the changed row.
else if (rowSettings.relationship == 'parent') {
var previousRow = $(changedRow).prev('tr');
while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) {
previousRow = previousRow.prev('tr');
}
// If we found a row.
if (previousRow.length) {
sourceRow = previousRow[0];
}
// Otherwise we went all the way to the left of the table without finding
// a parent, meaning this item has been placed at the root level.
else {
// Use the first row in the table as source, because it's guaranteed to
// be at the root level. Find the first item, then compare this row
// against it as a sibling.
sourceRow = $(this.table).find('tr.draggable:first').get(0);
if (sourceRow == this.rowObject.element) {
sourceRow = $(this.rowObject.group[this.rowObject.group.length - 1]).next('tr.draggable').get(0);
}
var useSibling = true;
}
}
// Because we may have moved the row from one category to another,
// take a look at our sibling and borrow its sources and targets.
this.copyDragClasses(sourceRow, changedRow, group);
rowSettings = this.rowSettings(group, changedRow);
// In the case that we're looking for a parent, but the row is at the top
// of the tree, copy our sibling's values.
if (useSibling) {
rowSettings.relationship = 'sibling';
rowSettings.source = rowSettings.target;
}
var $changedRow = $(changedRow);
// Check if data exists in this row.
if (typeof $(sourceRow).data(rowSettings.target) !== 'undefined') {
switch (rowSettings.action) {
case 'depth':
// Get the depth of the target row.
$changedRow.data(rowSettings.target, $('.indentation', $(sourceRow)).length);
break;
case 'match':
// Update the value.
$changedRow.data(rowSettings.target, $(sourceRow).data(rowSettings.source));
break;
case 'order':
var siblings = this.rowObject.findSiblings(rowSettings);
// We're not using fields for this so we just assume
// weight starting from 0
var weight = 0;
$(siblings).each(function () {
$(this).data('weight', weight);
weight++;
$(this).trigger('taxonomyWranglerTermDrag.rowDataChange', [this]);
});
break;
}
}
$changedRow.trigger('taxonomyWranglerTermDrag.rowDataChange', [changedRow]);
};
TaxonomyWranglerDrag.prototype.dropRow = function (event, self) {
// Drop row functionality shared between mouseup and blur events.
if (self.rowObject != null) {
var droppedRow = self.rowObject.element;
// The row is already in the right place so we just release it.
if (self.rowObject.changed == true) {
// Update the fields in the dropped row.
self.updateFields(droppedRow);
// If a setting exists for affecting the entire group, update all the
// fields in the entire dragged group.
for (var group in self.tableSettings) {
var rowSettings = self.rowSettings(group, droppedRow);
if (rowSettings.relationship == 'group') {
for (var n in self.rowObject.children) {
self.updateField(self.rowObject.children[n], group);
}
}
}
self.rowObject.markChanged();
if (self.changed == false) {
self.changed = true;
}
}
if (self.indentEnabled) {
self.rowObject.removeIndentClasses();
}
if (self.oldRowElement) {
$(self.oldRowElement).removeClass('drag-previous');
}
$(droppedRow).removeClass('drag').addClass('drag-previous');
self.oldRowElement = droppedRow;
self.onDrop();
self.rowObject = null;
}
// Functionality specific only to mouseup event.
if (self.dragObject != null) {
$('.tabledrag-handle', droppedRow).removeClass('tabledrag-handle-hover');
self.dragObject = null;
$('body').removeClass('drag');
clearInterval(self.scrollInterval);
// Hack for IE6 that flickers uncontrollably if select lists are moved.
if (navigator.userAgent.indexOf('MSIE 6.') != -1) {
$('select', this.table).css('display', 'block');
}
}
};
/**
* Indent a row within the legal bounds of the table.
*
* @param indentDiff
* The number of additional indentations proposed for the row (can be
* positive or negative). This number will be adjusted to nearest valid
* indentation level for the row.
*/
TaxonomyWranglerDrag.prototype.row.prototype.indent = function (indentDiff) {
// Determine the valid indentations interval if not available yet.
if (!this.interval) {
var prevRow = $(this.element).prevAll('tr:not(.tabledrag-hidden)').get(0);
var nextRow = $(this.group).filter(':last').nextAll('tr:not(.tabledrag-hidden)').get(0);
this.interval = this.validIndentInterval(prevRow, nextRow);
}
// Adjust to the nearest valid indentation.
var indent = this.indents + indentDiff;
indent = Math.max(indent, this.interval.min);
indent = Math.min(indent, this.interval.max);
indentDiff = indent - this.indents;
for (var n = 1; n <= Math.abs(indentDiff); n++) {
// Add or remove indentations.
if (indentDiff < 0) {
$('.indentation:first', this.group).remove();
this.indents--;
}
else {
$('td:first', this.group).prepend(Drupal.theme('tableDragIndentation'));
this.indents++;
}
}
if (indentDiff) {
// Update indentation for this row.
this.changed = true;
this.groupDepth += indentDiff;
this.onIndent();
}
return indentDiff;
};
/**
* Perform the swap between two rows. Take into account the collapsed
* accordion rows when swapping after.
*
* @param position
* Whether the swap will occur 'before' or 'after' the given row.
* @param row
* DOM element what will be swapped with the row group.
*/
TaxonomyWranglerDrag.prototype.row.prototype.swap = function (position, row) {
var swapRow = row;
if (position == 'after') {
var $row = $(row);
if ($row.data('is-collapsed')) {
var $target = $row.nextUntil(":visible");
if ($target.length) {
swapRow = $target.get($target.length - 1);
}
else {
// If there are no other rows visible, we must be at the bottom
// of the table, get the last row.
swapRow = $row.nextAll(":last").get(0);
}
}
}
Drupal.detachBehaviors(this.group, Drupal.settings, 'move');
$(swapRow)[position](this.group);
Drupal.attachBehaviors(this.group, Drupal.settings);
this.changed = true;
this.onSwap(row);
};
/**
* Ensure that two rows are allowed to be swapped.
*
* @param row
* DOM object for the row being considered for swapping.
*/
TaxonomyWranglerDrag.prototype.row.prototype.isValidSwap = function (row) {
if (this.indentEnabled) {
var prevRow, nextRow;
if (this.direction == 'down') {
prevRow = row;
nextRow = $(row).nextAll('tr:not(:hidden)').get(0);
}
else {
prevRow = $(row).prevAll('tr:not(:hidden)').get(0);
nextRow = row;
}
this.interval = this.validIndentInterval(prevRow, nextRow);
// We have an invalid swap if the valid indentations interval is empty.
if (this.interval.min > this.interval.max) {
return false;
}
}
// Do not let an un-draggable first row have anything put before it.
if (this.table.tBodies[0].rows[0] == row && $(row).is(':not(.draggable)')) {
return false;
}
return true;
};
/**
* Move a block in the blocks table from one region to another via select list.
*
* This behavior is dependent on the tableDrag behavior, since it uses the
* objects initialized in that behavior to update the row.
*/
Drupal.behaviors.taxonomyWranglerTermDrag = {
attach: function (context, settings) {
for (var base in settings.taxonomyWrangler.tables) {
if (typeof TaxonomyWranglerDrag.instances[base] == 'undefined') {
$('#' + base, context).once('tabledrag', function () {
// Create the new tableDrag instance. Save in the Drupal variable
// to allow other scripts access to the object.
TaxonomyWranglerDrag.instances[base] = new TaxonomyWranglerDrag(this, settings.taxonomyWrangler.tables[base]);
});
}
}
var table = $('#taxonomy-wrangler', context);
var tableDrag = TaxonomyWranglerDrag.instances['taxonomy-wrangler']; // Get the blocks tableDrag object.
var $messages = $('#taxonomy-wrangler-messages-js', context);
// Compare server timestamp with javascript changed stamp and remove changed classes.
if (settings.taxonomyWrangler && settings.taxonomyWrangler.updatedTimestamps) {
$.each(settings.taxonomyWrangler.updatedTimestamps, function (tid, stamp) {
var $row = table.find('tr[data-tid="'+ tid +'"]');
if ($row.data('updated') == stamp) {
$row.removeClass('drag-previous').find('span.tabledrag-changed').remove();
}
});
}
if (tableDrag.tm === null) {
$messages.removeClass('is-busy').empty();
}
if (!table.hasClass('taxonomy-wrangler-processed')) {
table.addClass('taxonomy-wrangler-processed');
var rows = $('tr', table).length;
var $termData = $('input[name="term_data"]', context);
var $submit = $('input[name="op"][value="Save"]', context)
var $rows = $('tr', table);
tableDrag.collection = new TaxonomyWranglerCollection();
var updatedTerms = {};
$submit.attr('disabled', 'disabled');
var accordion = !!TaxonomyWranglerAccordion.instances[table.attr('id')] ? TaxonomyWranglerAccordion.instances[table.attr('id')] : null;
$rows.bind('taxonomyWranglerTermDrag.rowDataChange', function(e, row){
tableDrag.collection.updateTerm(row);
var termsArray = [];
var tids = [];
accordion.refreshRows();
$.each(tableDrag.collection.updatedTerms, function(i, val) {
termsArray.push(val);
tids.push(val.tid + ':' + val.weight);
});
$termData.val(JSON.stringify({ "termData": termsArray }));
if (tableDrag.tm) {
clearTimeout(tableDrag.tm);
tableDrag.tm = null;
}
tableDrag.tm = setTimeout(function(){
$submit.removeAttr('disabled').trigger('mousedown').attr('disabled', 'disabled');
tableDrag.tm = null;
}, 1000);
if (!$messages.hasClass('is-busy')) {
$messages.addClass('is-busy').append('<div class="ajax-progress ajax-progress-throbber ajax-progress-throbber--taxonomy-wrangler"><div class="throbber">&nbsp;</div> Updating terms</div>');
}
});
}
}
};
})(jQuery, Drupal, this, this.document);
@@ -0,0 +1,291 @@
<?php
/**
* @file
* Administrative page callbacks for the taxonomy ui tweaks module.
*/
/**
* Form builder for the taxonomy tweaks terms overview.
*
* Display a tree of all the terms in a vocabulary, with options to edit
* each one. The form is made drag and drop by the theme function.
*
* @ingroup forms
* @see taxonomy_overview_terms_submit()
* @see theme_taxonomy_overview_terms()
*/
function taxonomy_wrangler_overview_terms($form, &$form_state, $vocabulary) {
global $pager_page_array, $pager_total, $pager_total_items;
// Check for confirmation forms.
if (isset($form_state['confirm_reset_alphabetical'])) {
module_load_include('inc', 'taxonomy', 'taxonomy.admin');
return taxonomy_vocabulary_confirm_reset_alphabetical($form, $form_state, $vocabulary->vid);
}
$form['#vocabulary'] = $vocabulary;
$form['#tree'] = TRUE;
$form['#parent_fields'] = FALSE;
$form['header']['info'] = array(
'#markup' => '<p>' . t('Drag and drop taxonomy terms below. Ctrl + click on an arrow will expand the term and all its children') . '</p>',
);
$form['header']['messages'] = array(
'#markup' => '<div id="taxonomy-wrangler-messages"><div id="taxonomy-wrangler-messages-server"></div><div id="taxonomy-wrangler-messages-js"></div></div>',
);
$root_entries = 0; // Elements at the root level on this page.
$delta = 0;
$term_deltas = array();
$tree = taxonomy_get_tree($vocabulary->vid);
$form_state['storage']['tree'] = $tree;
$term = current($tree);
$form['taxonomy_wrangler_messages'] = array(
'#type' => 'markup',
'#markup' => '',
);
// The original form handled setting the order after a validation error.
// Since we aren't supporting the non-ajax form, it has been removed.
// Build the actual form.
foreach ($tree as $orig_key => $term) {
$term_deltas[$term->tid] = isset($term_deltas[$term->tid]) ? $term_deltas[$term->tid] + 1 : 0;
$key = 'tid:' . $term->tid . ':' . $term_deltas[$term->tid];
// Save the term for the current page so we don't have to load it a second time.
$form[$key]['#term'] = (array) $term;
if (isset($term->parents)) {
$form[$key]['#term']['parent'] = $term->parent = $term->parents[0];
unset($form[$key]['#term']['parents'], $term->parents);
}
$form[$key]['view'] = array('#type' => 'link', '#title' => $term->name, '#href' => "taxonomy/term/$term->tid");
if ($vocabulary->hierarchy < 2 && count($tree) > 1) {
$form['#parent_fields'] = TRUE;
$form[$key]['view']['#options']['attributes']['data-tid'] = $term->tid;
$form[$key]['view']['#options']['attributes']['data-parent'] = $term->tid;
$form[$key]['view']['#options']['attributes']['data-depth'] = $term->depth;
$form[$key]['view']['#options']['attributes']['data-weight'] = $term->weight;
}
$form[$key]['edit'] = array('#type' => 'link', '#title' => t('edit'), '#href' => 'taxonomy/term/' . $term->tid . '/edit', '#options' => array('query' => drupal_get_destination()));
}
$form['#empty_text'] = t('No terms available. <a href="@link">Add term</a>.', array('@link' => url('admin/structure/taxonomy/' . $vocabulary->machine_name . '/add')));
// Hidden input field for ajax.
$form['term_data'] = array(
'#type' => 'hidden',
'#default_value' => '{}',
);
if ($vocabulary->hierarchy < 2 && count($tree) > 1) {
$form['actions'] = array('#type' => 'actions', '#tree' => FALSE);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Save'),
'#ajax' => array(
'callback' => 'taxonomy_wrangler_terms_overview_ajax_handler',
),
);
$form['actions']['reset_alphabetical'] = array(
'#type' => 'submit',
'#value' => t('Reset to alphabetical'),
);
}
$module_path = drupal_get_path('module', 'taxonomy_wrangler');
$form['#attached']['css'][] = $module_path . '/css/taxonomy_wrangler.css';
$form['#attached']['js'][] = $module_path . '/js/taxonomy_wrangler.js';
return $form;
}
/**
* Validate callback for taxonomy_wrangler_overview_terms.
*/
function taxonomy_wrangler_overview_terms_validate($form, &$form_state) {
if ($form_state['triggering_element']['#value'] != t('Reset to alphabetical')) {
if (isset($form_state['values']['term_data'])) {
$updated_terms_json = json_decode($form_state['values']['term_data']);
if ($updated_terms_json == NULL) {
drupal_set_message(t('an error occurred parsing submitted values.'), 'error');
}
else {
$form_state['storage']['updated_terms'] = array();
$values_to_update = array('parent', 'weight');
if (!empty($updated_terms_json->termData)) {
$form_state['storage']['raw_term_data'] = $updated_terms_json->termData;
foreach ($updated_terms_json->termData as $updated_term) {
$term = taxonomy_term_load($updated_term->tid);
foreach ($values_to_update as $prop) {
if (isset($updated_term->{$prop})) {
$term->{$prop} = (int) $updated_term->{$prop};
}
}
$form_state['storage']['updated_terms'][$term->tid] = $term;
}
}
}
}
}
}
/**
* Submit callback for taxonomy_wrangler_overview_terms.
*/
function taxonomy_wrangler_overview_terms_submit($form, &$form_state) {
if ($form_state['triggering_element']['#value'] == t('Reset to alphabetical')) {
module_load_include('inc', 'taxonomy', 'taxonomy.admin');
// Execute the reset action.
if ($form_state['values']['reset_alphabetical'] === TRUE) {
return taxonomy_vocabulary_confirm_reset_alphabetical_submit($form, $form_state);
}
// Rebuild the form to confirm the reset action.
$form_state['rebuild'] = TRUE;
$form_state['confirm_reset_alphabetical'] = TRUE;
return;
}
else {
if (!empty($form_state['storage']['updated_terms'])) {
foreach ($form_state['storage']['updated_terms'] as $term) {
taxonomy_term_save($term);
}
}
}
}
/**
* Ajax handler for taxonomy_wrangler_overview_terms.
*/
function taxonomy_wrangler_terms_overview_ajax_handler($form, &$form_state) {
$commands = array();
$messages = '<div id="taxonomy-wrangler-messages-server">';
$messages .= theme('status_messages');
$messages .= '</div>';
$commands[] = ajax_command_replace('#taxonomy-wrangler-messages-server', $messages);
// Send timestamps back to browser so it can update the row status.
$timestamps = array();
if (!empty($form_state['storage']['raw_term_data'])) {
foreach($form_state['storage']['raw_term_data'] as $term) {
if (isset($term->updated)) {
$timestamps[$term->tid] = (float) $term->updated;
}
}
}
$commands[] = ajax_command_settings(array(
'taxonomyWrangler' => array('updatedTimestamps' => $timestamps),
));
return array('#type' => 'ajax', '#commands' => $commands);
}
/**
* Returns HTML for a terms overview form as a sortable list of terms.
*
* @param $variables
* An associative array containing:
* - form: A render element representing the form.
*
* @see taxonomy_overview_terms()
* @ingroup themeable
*/
function theme_taxonomy_wrangler_overview_terms($variables) {
$form = $variables['form'];
// Add drag and drop if parent fields are present in the form.
if ($form['#parent_fields']) {
taxonomy_wrangler_add_tabledrag('taxonomy-wrangler', 'match', 'parent', 'parent', 'parent', 'tid', FALSE);
taxonomy_wrangler_add_tabledrag('taxonomy-wrangler', 'depth', 'group', 'depth', NULL, NULL, FALSE);
drupal_add_css(drupal_get_path('module', 'taxonomy') . '/taxonomy.css');
}
taxonomy_wrangler_add_tabledrag('taxonomy-wrangler', 'order', 'sibling', 'weight');
$errors = form_get_errors() != FALSE ? form_get_errors() : array();
$rows = array();
$element_children = element_children($form);
$is_root = TRUE;
foreach ($element_children as $i => $key) {
if (isset($form[$key]['#term'])) {
$term = &$form[$key];
$next_key = isset($element_children[$i + 1]) ? $element_children[$i + 1] : FALSE;
$is_parent = FALSE;
if (isset($form[$next_key]['#term']) && $form[$next_key]['#term']['depth'] > $term['#term']['depth'] && !$is_root) {
$is_parent = TRUE;
}
$row = array();
$row[] = (isset($term['#term']['depth']) && $term['#term']['depth'] > 0 ? theme('indentation', array('size' => $term['#term']['depth'])) : '');
$accordion_classes = array('taxonomy-wrangler-accordion-item');
if ($is_root) {
$accordion_classes[] = 'taxonomy-wrangler-accordion-item--root';
$is_root = FALSE;
}
elseif ($is_parent) {
$accordion_classes[] = 'taxonomy-wrangler-accordion-item--parent';
}
else {
$accordion_classes[] = 'taxonomy-wrangler-accordion-item--child';
}
$row[0] .= '<a href="#" class="' . implode(' ', $accordion_classes) . '"><span></span></a>';
$row[0] .= drupal_render($term['view']);
if ($form['#parent_fields']) {
$term['tid']['#attributes']['class'] = array('term-id');
$term['parent']['#attributes']['class'] = array('term-parent');
$term['depth']['#attributes']['class'] = array('term-depth');
$row[0] .= drupal_render($term['parent']) . drupal_render($term['tid']) . drupal_render($term['depth']);
}
$term['weight']['#attributes']['class'] = array('term-weight');
$row[] = drupal_render($term['weight']);
$row[] = drupal_render($term['edit']);
$row = array(
'data' => $row,
'data-tid' => $term['#term']['tid'],
'data-parent' => $term['#term']['parent'],
'data-depth' => $term['#term']['depth'],
'data-weight' => $term['#term']['weight'],
'data-haschildren' => $is_parent,
);
$rows[$key] = $row;
}
}
// Add necessary classes to rows.
$row_position = 0;
foreach ($rows as $key => $row) {
$rows[$key]['class'] = array();
if (isset($form['#parent_fields'])) {
$rows[$key]['class'][] = 'draggable';
}
// Add an error class if this row contains a form error.
foreach ($errors as $error_key => $error) {
if (strpos($error_key, $key) === 0) {
$rows[$key]['class'][] = 'error';
}
}
$rows[$key]['class'][] = 'taxonomy-wrangler-term-row';
$rows[$key]['class'][] = 'term-weight-row';
$row_position++;
}
if (empty($rows)) {
$rows[] = array(array('data' => $form['#empty_text'], 'colspan' => '3'));
}
$output = drupal_render($form['header']);
//unset($form['header']);
$header = array(t('Name'), t('Weight'), t('Operations'));
$output .= theme('table', array(
'header' => $header,
'rows' => $rows,
'attributes' => array(
'id' => 'taxonomy-wrangler',
'class' => array('taxonomy-wrangler', 'taxonomy-wrangler-accordion'),
),
));
$output .= drupal_render_children($form);
return $output;
}
@@ -0,0 +1,12 @@
name = Taxonomy Wrangler
description = Provides a better UI for the taxonomy overview page
core = 7.x
dependencies[] = taxonomy
package = Taxonomy
; Information added by Drupal.org packaging script on 2015-02-17
version = "7.x-1.x-dev"
core = "7.x"
project = "taxonomy_wrangler"
datestamp = "1424205783"
@@ -0,0 +1,59 @@
<?php
/**
* @file
* Taxonomy wrangler main module file
*/
/**
* Implements hook_menu_alter().
*
* Override the main taxonomy overview page.
*/
function taxonomy_wrangler_menu_alter(&$items) {
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name']['page arguments'] = array('taxonomy_wrangler_overview_terms', 3);
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name']['file'] = 'taxonomy_wrangler.admin.inc';
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name']['file path'] = drupal_get_path('module', 'taxonomy_wrangler');
}
/**
* Implements hook_theme().
*/
function taxonomy_wrangler_theme() {
return array(
'taxonomy_wrangler_overview_terms' => array(
'render element' => 'form',
),
);
}
/**
* Copy of drupal_add_tabledrag().
*
* This is a copy of drupal_add_tabledrag used just to instantiate
* our own tabledrag class vs Drupal's.
* @see drupal_add_tabledrag
*/
function taxonomy_wrangler_add_tabledrag($table_id, $action, $relationship, $group, $subgroup = NULL, $source = NULL, $hidden = TRUE, $limit = 0) {
$js_added = &drupal_static(__FUNCTION__, FALSE);
if (!$js_added) {
// Add the table drag JavaScript to the page before the module JavaScript
// to ensure that table drag behaviors are registered before any module
// uses it.
drupal_add_library('system', 'jquery.cookie');
drupal_add_js('misc/tabledrag.js', array('weight' => -1));
$js_added = TRUE;
}
// If a subgroup or source isn't set, assume it is the same as the group.
$target = isset($subgroup) ? $subgroup : $group;
$source = isset($source) ? $source : $target;
$settings['taxonomyWrangler']['tables'][$table_id][$group][] = array(
'target' => $target,
'source' => $source,
'relationship' => $relationship,
'action' => $action,
'hidden' => $hidden,
'limit' => $limit,
);
drupal_add_js($settings, 'setting');
}
@@ -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.
@@ -0,0 +1,78 @@
<?php
function uc_ssl_admin()
{
$form = array();
$form['uc_ssl_ssl_domain'] = array(
'#type' => 'textfield',
'#title' => t('Secure Domain Name'),
'#default_value' => variable_get('uc_ssl_ssl_domain', ''),
//'#size' => 2,
//'#maxlength' => 2,
'#description' => t("This field accepts domain names in the following format: https://something.com OR https://something.com:5938. <br><br><b>IPMORTANT DRUPAL 7 NOTICE ABOUT DOMAIN NAMES:</b> This module DOES support using port numbers and different domains for SSL and normal HTTP like it's Drupal 6 predecessor HOWEVER, this is NOT meant for a production environment!!!! RFC restrictions PROHIBIT passing cookie data between sites with different port numbers (80 to 5933 for example. The ONLY exception is for the default ports of 80 and 443 which are never specified to begin with and thus pass RFC requirements for sharing data!!) and it treats them as separate sites. So if you use a port number drupal WILL require you to login BEFORE that url will work (drupal 6 has the same limitation btw, I just never documented it). In Drupal 7 this results in the 'Store' configuration panel NOT showing up. You must login to the store config panel by going to the https://yoursite.com:PORTNUMBER/admin/store url FIRST and login. Once you have done that, you can access store/admin. The administration panel overlay code for drupal 7 is NOT compatible with this module because it is NOT a secure method to access data!!!! ALWAYS go DIRECTLY to the /store/admin link DIRECTLY. The same goes for /cart. /cart WILL ask you to login. Once your site is ready for production you must absolutely use the SAME url for both secure AND non-secure pages. So your site should always be accessed from http://yoursite.com and https://yoursite.com OR http://www.yoursite.com and https://www.yoursite.com. It should NEVER be accessed like this: http://yoursite.com http://ssl.yoursite.com. Or any other sub domain for that matter. It will result in drupal asking you to login!"),
'#required' => TRUE,
);
$form['uc_ssl_non_ssl_domain'] = array(
'#type' => 'textfield',
'#title' => t('NON-Secure Domain Name'),
'#default_value' => variable_get('uc_ssl_non_ssl_domain', ''),
//'#size' => 2,
//'#maxlength' => 2,
'#description' => t("This field accepts domain names in the following format: http://something.com OR http://something.com:8080"),
'#required' => TRUE,
);
$form['uc_ssl_status'] = array(
'#type' => 'radios',
'#title' => t('Ubercart SSL Status'),
'#default_value' => variable_get('uc_ssl_status', ''),
'#options' => array( TRUE => t('Enabled'), FALSE => t('Disabled')),
'#required' => TRUE,
);
$form['uc_ssl_switch_to_non_ssl'] = array(
'#type' => 'radios',
'#title' => t('Ubercart Non-SSL Switch'),
'#default_value' => variable_get('uc_ssl_switch_to_non_ssl', ''),
'#options' => array( TRUE => t('Enabled'), FALSE => t('Disabled')),
'#required' => TRUE,
'#description' => t("Since this module switches to SSL mode during checkout, all links will link back to a secured version of the website. If you want to switch back to non-secured mode after a transaction is complete or they click on a keep shopping link, etc. This isnt really necessary but some people may want to save on the small resources savings you can get from doing this. So if you want to keep http for any non-cart related things, you should check this as enabled. If you dont care, then set it to disabled.<br><br>DRUPAL 7 NOTICE: Enabling this will DISABLE the Overlay module because it cannot work securely in a mixed SSL environment!! Setting this back to Disabled WILL NOT re-enable the Overlay module. You must re-enable the Overlay module yourself."),
);
$form['uc_ssl_case_insensitive'] = array(
'#type' => 'radios',
'#title' => t('Ubercart SSL Case Sensitivity'),
'#default_value' => variable_get('uc_ssl_case_insensitive', TRUE),
'#options' => array( TRUE => t('Enabled'), FALSE => t('Disabled')),
'#required' => TRUE,
'#description' => t('By default drupal allows paths to be accessed in a case insensitive manor. This is fine but not very secure. SSL Standards dictate that paths should always be case sensitive. So in order to be compatible with SSL standards, we allow the administrator to change SSL path recognition to case sensitive by simply setting this setting to disabled. In order to stick close to drupal standards, this module defaults this value to Enabled which means /Cart AND /cart would match and be secured where as in case sensitive mode /Cart would NOT be secured, alerting the user to an insecure page but /cart would be secured.'),
);
return system_settings_form($form);
}
function uc_ssl_admin_validate($form, &$form_state)
{
$ssl_domain = $form_state['values']['uc_ssl_ssl_domain'];
$non_ssl_domain = $form_state['values']['uc_ssl_non_ssl_domain'];
if (!valid_url($ssl_domain) || !stristr($ssl_domain, 'https://'))
{
form_set_error('uc_ssl_ssl_domain', t('You must enter a valid SSL Domain name.'));
}
if (!valid_url($non_ssl_domain) || !stristr($non_ssl_domain, 'http://'))
{
form_set_error('uc_ssl_non_ssl_domain', t('You must enter a valid Non-SSL Domain name.'));
}
if (!uc_ssl_check($ssl_domain.base_path()))
{
form_set_error('uc_ssl_ssl_domain', t('CRITICAL ERROR! The domain you entered for the secured domain came back as non-secure or it does not point back to this installation of drupal. Your secure domain name MUST be setup and MUST be pointing at this install of drupal.'));
variable_set('uc_ssl_status', '0');
$form_state['values']['uc_ssl_status'] = '0';
$form['values']['uc_ssl_status'] = '0';
}
}
@@ -0,0 +1,14 @@
name = Ubercart SSL
description = (D7) Secure Ubercart /cart links with an SSL Cert. Do NOT enable this unless your site works with SSL as it WILL break your cart. If it does, just disable this and get your site working with SSL first and then re-enable this module. This module does not require Ubercart so it can be used for anything else as well, its just that its most common use is to be used with Ubercart so that is why I named it so. Feel free to use it with anything you like.
package = Ubercart - extra
core = 7.x
project = "uc_ssl"
configure = admin/config/system/uc_ssl
; Information added by drupal.org packaging script on 2012-12-03
version = "7.x-1.11"
core = "7.x"
project = "uc_ssl"
datestamp = "1354572030"
@@ -0,0 +1,12 @@
<?php
/**
* Implementation of hook_uninstall().
*/
function uc_ssl_uninstall()
{
variable_del('uc_ssl_ssl_domain');
variable_del('uc_ssl_non_ssl_domain');
variable_del('uc_ssl_status');
variable_del('uc_ssl_switch_to_non_ssl');
}
@@ -0,0 +1,416 @@
<?php
//
//Note about the include/exclude hooks. As an example, this module includes /cart* which essentially secures ALL cart pages with SSL. But it also implements an exclude which is called /cart Which you would think would negate the include but it DOES NOT. Even though Excludes take presedence over includes, it still allows /cart/1 for example, to be secured. Why? Because the exclude is not a wildcard match! Thus the exclude would NOT match /cart/1 because it's defined as /cart. Thus while /cart IS excluded (not secured with SSL),/cart/1 is STILL secured.
//Implementation of hook_include_ssl_paths()
//NOTE: Exclude takes presedence over Includes!!! So if the same path is in both hooks, then the path will be EXCLUDED from secured pages.
//Since base_url has a design flaw in D4/D5/D6/D7 that seemingly will never be fixed, we fix it here.
function uc_ssl_boot()
{
global $conf;
//the $conf['https'] option MUST be enabled if mixed ssl mode is enabled. We make sure its enabled here. If it's not enabled then user
//sessions will not persist across the secure and non-secure domain names resulting in the user being prompted to log in whenever they are
//sent to a secure page.
if (uc_ssl_switch_to_non_ssl() && !isset($conf['https']) || (isset($conf['https']) && !$conf['https']))
{
$conf['https'] = TRUE;
}
if ($GLOBALS['base_url'])
{
//If base_url exists and is set to a non-secure protocol while the page being called is secure, allow base_url to be changed to
//the secure version of the website. Drupal for some reason doesnt have this logic anywhere for $base_url.
if (uc_ssl_page_is_in_https_mode() && stristr($GLOBALS['base_url'], 'http://'))
{
$GLOBALS['base_url'] = str_ireplace('http://', 'https://', $GLOBALS['base_url']);
}
}
}
function uc_ssl_permission()
{
$perms = array(
'administer uc_ssl' => array(
'title' => t('Administer Ubercart SSL settings'),
),
);
return $perms;
}
function uc_ssl_page_is_in_https_mode()
{
if (isset($_SERVER['HTTPS']) && !empty($_SERVER['HTTPS']))
{
return TRUE;
}
return FALSE;
}
function uc_ssl_disable_overlay_module()
{
$modules = module_list();
$base_path = base_path();
if (variable_get('uc_ssl_switch_to_non_ssl', '') && in_array('overlay', $modules))
{
//Overlay is on, lets disable it.
module_disable(array(0 => 'overlay'));
}
}
function uc_ssl_init()
{
//Drupals Overlay panel is not secure! Fix that here by disable it all together. We cannot allow this. Drupal 7 is insecure by default, such a sad thing :/
uc_ssl_disable_overlay_module();
//menu_rebuild();
//First and foremost, make sure Clean URLs are on.
if (uc_ssl_clean_urls_enabled() && !drupal_is_cli())
{
//Setup a hook for ssl checking
uc_ssl_check();
//Run ssl check on every page load.
uc_ssl_run();
}
}
function uc_ssl_include_ssl_paths()
{
global $user;
$paths = array(
'All Ubercart Cart pages' => '/cart*', //Secure ALL cart pages.
//NOTE: If you remove the /user definition here, you CAN NOT use Mixed SSL Mode with D7. It HAS to exist because D7 http sessions are missing
//data that https sessions require. But http sessions work with the https session variables. Thus, you MUST login with https or your sessions
//will not carry between the http and https versions of your wesite. This is a D7 bug only, I believe its more of a feature than a bug.
'All User pages' => '/user*', //Secure ALL user pages.
//Added support for Drupal commerce which is based upon Ubercart.
'All Drupal Commerce Checkout pages' => '/checkout*', //Secure ALL cart pages. NOTE: SEE EXCLUDES to see why the cart listing itself isnt secured!
//This supports both UC and Commerce admins
'All Ubercart Cart Admin pages' => '/admin/store*', //Secure ALL UC admin pages.
#'example 1' => '/cart', //Only secure the /cart and /cart/ page.
#'example 2' => 'cart', //Only secure the /cart and /cart/ page.
#'example 3' => 'cart/', //Only secure the /cart and /cart/ page.
#'example 4' => '/cart*/something', //matches /cartThis/something and /cart/something but NOT thiscart/something etc.
#'example 5' => '/cart/*/something', //Matches /cart/123/something and cart/this/something, but NOT /cart/something
#'example 6' => '/cart/something', //Only secure the /cart/something and /cart/something/ page.
);
return $paths;
}
//Implementation of hook_exclude_ssl_paths()
function uc_ssl_exclude_ssl_paths()
{
$paths = array(
//These excludes no longer seem to be required for the D7 version and thus they have been removed until issues come up again.
//'Exclude the cart page itself' => '/cart', //We dont need to secure the cart contents so we exclude that here
//'Exclude Autocomplete' => '*autocomplete*',
//'Exclude Ajax' => '*ajax*',
#'example 1' => '/cart', //Only exclude the /cart and /cart/ page.
#'example 2' => 'cart', //Only exclude the /cart and /cart/ page.
#'example 3' => 'cart/', //Only secure the /cart and /cart/ page.
#'example 4' => '/cart*/something', //matches /cartThis/something and /cart/something but NOT thiscart/something etc.
#'example 5' => '/cart/*/something', //Matches /cart/123/something and cart/this/something, but NOT /cart/something
#'example 6' => '/cart/something', //Only exclude the /cart/something and /cart/something/ page.
);
return $paths;
}
//Implementation of hook_exclude_ssl_paths()
//This function is so you can completely ignore ANY switching at all no matter what.
function uc_ssl_exclude_ssl_switch_paths()
{
$paths = array(
#'example 1' => '/*ajax*', //Dont touch ajax paths whatsoever, this is just an example.
);
return $paths;
}
function uc_ssl_menu()
{
$items = array();
$items['admin/config/system/uc_ssl'] = array(
'title' => 'Ubercart SSL',
// 'title' => 'Ubercart SSL module settings',
'description' => 'Ubercart SSL module settings',
'page callback' => 'drupal_get_form',
'page arguments' => array('uc_ssl_admin'),
'access arguments' => array('administer uc_ssl'),
'type' => MENU_NORMAL_ITEM,
'file' => 'uc_ssl.admin.inc',
);
return $items;
}
function uc_ssl_enabled()
{
return variable_get('uc_ssl_status', '');
}
function uc_ssl_is_configured()
{
$query = "SELECT count(name) AS count from {variable} WHERE name = 'uc_ssl_status'";
$results = db_query($query);
$rows = $results->fetchAll();
$rows = $rows[0];
//If uc_ssl is not configured, warn the user and show a link to config it.
if (!$rows->count)
{
$admin_link = 'http://'.$_SERVER['HTTP_HOST'].url().'admin/config/system/uc_ssl';
drupal_set_message("Please configure Ubercart SSL for SSL enabled cart checkouts to work: <a href = '$admin_link'>$admin_link</a>", 'warning');
}
return $rows->count;
}
function uc_ssl_switch_to_non_ssl()
{
return variable_get('uc_ssl_switch_to_non_ssl', '');
}
function uc_ssl_path_match($path, $pages)
{
$result = FALSE; //This is here just to get rid of retarded php 'notice' messages. I hate php notices.
if (!empty($pages))
{
//Trim off trailing / and then re-add it to avoid double /'s. This allows cart and cart/ to match as the same thing or cart/1 and cart/1/ to be the same.
$path = trim($path, '/').'/';
$regexp = '/^(';
foreach ($pages AS $key => $regexp_path)
{
if ($regexp_path)
{
$regexp_path = trim($regexp_path, '/').'/';
$regexp_path1 = str_replace('/', '\/', $regexp_path); //replace / with a \/
$regexp_path2 = str_replace('*', '.*', $regexp_path1);//replace * with a .*
$regexp .= $regexp_path2.'|'; //Append a | to signify end of this expression
}
}
$regexp = trim($regexp, '|');
$regexp .= ')$/';
if (variable_get('uc_ssl_case_insensitive', 1))
{
$regexp .= 'i';
}
$result = preg_match($regexp, $path);
}
return $result;
}
function is_clean_url($path)
{
//Skip ANY ?q= urls since these do NOT come from a web browser when clean URLs is on. Doing this allows us to skip
//ajax and autocomplete type URLs that come from within drupal itself. This is a major problem with Secure_Pages module
//and we dont want the same problems so we restrict people to using ONLY clean URLs.
if (!stristr($path, '?q='))
{
return TRUE;
}
return FALSE;
}
function uc_ssl_http_request($url)
{
//Handle JS type urls in the background as they are likely to be ajax type posts.
if (arg(0) == 'js')
{
$js_response = drupal_http_request($url);
return;
}
//Handle post type URLs. This should handle ajax POST urls such as the tax calculation/progress bars.
if ($_SERVER['REQUEST_METHOD'] == 'POST' && empty($_POST['uc_ssl_post']))
{
$_POST['uc_ssl_post'] = 1;
$options['headers'] = array('Content-Type' => 'application/x-www-form-urlencoded');
$options['method'] = 'POST';
$options['data'] = http_build_query($_POST, '', '&');
//Drupal 7 decided to completely recode drupal_http_request so I dont know if this will cause issues or not. Report any bugs please.
//$response = drupal_http_request($url, $headers, 'POST', http_build_query($_POST, '', '&'));
$response = drupal_http_request($url, $options);
return;
}
//All other types of urls are handled normally.
// Reset destination variables or we'll get sent there instead of to $url
unset($_REQUEST['destination'], $_GET['destination']);
drupal_goto($url);
return;
}
function uc_ssl_run()
{
//Set the current path without url() since it will mess up regex checks. Using url() also supports languages, base_path does not.
//$current_path = trim($_SERVER['REQUEST_URI'], url());
$root = url();
$current_path = substr(trim($_SERVER['REQUEST_URI']),strlen($root));
$ssl_domain = variable_get('uc_ssl_ssl_domain', '');
$nonssl_domain = variable_get('uc_ssl_non_ssl_domain', '');
//If uc_ssl is configured, run it.
if (uc_ssl_is_configured() && is_clean_url($current_path))
{
if (uc_ssl_enabled())
{
//Get a list of all the URL links that should be included and excluded from SSL pages from other modules.
$items = array();
$exclude_secured_urls = array();
$exclude_secured_urls = module_invoke('uc_ssl', 'exclude_ssl_paths', $exclude_secured_urls);
foreach (module_implements('exclude_ssl_paths') as $module)
{
$new = module_invoke($module, 'exclude_ssl_paths', $exclude_secured_urls);
if ($module != 'uc_ssl') //Skip uc_ssl since we already processed it above. Allow external modules to change uc_ssl defaults.
{
if (is_array($new))
{
$exclude_secured_urls = array_merge($exclude_secured_urls, $new);
}
}
}
$include_secured_urls = array();
$include_secured_urls = module_invoke('uc_ssl', 'include_ssl_paths', $include_secured_urls);
foreach (module_implements('include_ssl_paths') as $module)
{
$new = module_invoke($module, 'include_ssl_paths', $include_secured_urls);
if ($module != 'uc_ssl') //Skip uc_ssl since we already processed it above. Allow external modules to change uc_ssl defaults.
{
if (is_array($new))
{
$include_secured_urls = array_merge($include_secured_urls, $new);
}
}
}
// Get a list of all the URL links that should not switch between HTTP/HTTPS at all.
$exclude_switch_urls = array();
$exclude_switch_urls = module_invoke('uc_ssl', 'exclude_ssl_switch_paths', $exclude_switch_urls);
foreach (module_implements('exclude_ssl_switch_paths') as $module)
{
$new = module_invoke($module, 'exclude_ssl_switch_paths', $exclude_switch_urls);
if ($module != 'uc_ssl') //Skip uc_ssl since we already processed it above. Allow external modules to change uc_ssl defaults.
{
if (is_array($new))
{
$exclude_switch_urls = array_merge($exclude_switch_urls, $new);
}
}
}
// make sure we return an array
if (! is_array($include_secured_urls)) { $include_secured_urls = array(); }
if (! is_array($exclude_secured_urls)) { $exclude_secured_urls = array(); }
if (! is_array($exclude_switch_urls)) { $exclude_switch_urls = array(); }
//If the path is in the exclude switch hook, dont do ANYTHING at all.
if (uc_ssl_path_match($current_path, $exclude_switch_urls))
{
return;
}
//Switch to SSL no matter what the link is if Switch isnt on.
if (!uc_ssl_switch_to_non_ssl())
{
if (!uc_ssl_page_is_in_https_mode() && !uc_ssl_path_match($current_path, $exclude_secured_urls))
{
uc_ssl_http_request($ssl_domain . $_SERVER['REQUEST_URI']);
}
}
else
{
//If uc_ssl_switch_to_non_ssl() is enabled then we switch back to non-ssl link for non-cart links.
if (uc_ssl_page_is_in_https_mode())
{
//The only exceptions are the links listed in the exclude list by other modules.
if (uc_ssl_path_match($current_path, $exclude_secured_urls))
{
uc_ssl_http_request($nonssl_domain . $_SERVER['REQUEST_URI']);
}
if (!uc_ssl_path_match($current_path, $include_secured_urls) && uc_ssl_switch_to_non_ssl())
{
uc_ssl_http_request($nonssl_domain . $_SERVER['REQUEST_URI']);
}
}
}
//If on a cart url, use HTTPS and re-post/re-direct to the SSL version of the url.
if (!uc_ssl_page_is_in_https_mode() && uc_ssl_path_match($current_path, $include_secured_urls) && !uc_ssl_path_match($current_path, $exclude_secured_urls))
{
uc_ssl_http_request($ssl_domain . $_SERVER['REQUEST_URI']);
}
}
}
}
function uc_ssl_clean_urls_enabled()
{
$check = variable_get('clean_url', 0);
if ($check == 1)
{
return TRUE;
}
drupal_set_message('Ubercart SSL (uc_ssl): The uc_ssl module REQUIRES that Clean URLs be enabled in order to work properly. This allows the module to distinguish between an Ajax call and a Web browser call as the two are handled differently so we need a way to distinguish them and this is the quickest easiest way to do so. This will also make your website URLs search engine friendly as well which is a good thing. Please enable clean urls here: <a href="?q=admin/settings/clean-urls">Clean URLs</a>', 'warn');
return FALSE;
}
//This is a quickie hook to see if SSL works or not. It's not fool proof but it works for me.
function uc_ssl_check($site = '')
{
error_reporting(E_ALL);
if (!$site)
{
if (isset($_GET['uc_ssl_check']) && $_GET['uc_ssl_check'])
{
if (uc_ssl_page_is_in_https_mode())
{
echo TRUE;
}
else
{
echo FALSE;
}
exit;
}
}
else
{
$check = drupal_http_request($site."?uc_ssl_check=1");
if (isset($check->data) && $check->data == '1')
{
return TRUE;
}
//IF we are here, then the request failed for some reason, so we try to use a different method.
if (ini_get('allow_url_fopen'))
{
if (file_get_contents($site."?uc_ssl_check=1") == '1')
{
return TRUE;
}
}
//If we are here, something went wrong. Give the user some ideas on what they need to fix.
drupal_set_message("Ubercart SSL (uc_ssl): The uc_ssl_check() function is returning FALSE because it was unable to contact the SSL (https) version of your website that you defined in the settings. This can be caused by 3 things. 1. Your website is not setup properly for SSL, 2. The OpenSSL extension is not enabled on in your PHP installation, 3. allow_url_fopen is not enabled in php.ini. If #2 fails, uc_ssl will try to use file_get_contents() which requires allow_url_fopen to be set to TRUE in php.ini. Hopefully these hints will help you fix this issue so that you can use uc_ssl. You can try to debug this by going to $site?uc_ssl_check=1", "error");
}
return FALSE;
}
if (!function_exists('printr'))
{
function printr($arr)
{
echo "<pre>";
print_r($arr);
echo "</pre>";
}
}
@@ -1,4 +1,3 @@
This is a highly flexible and easy extendable filter module to embed any type
of video in your site using a simple tag. Other modules can add video
sites/formats (called codecs) using an easy plug-in architecture.
@@ -22,6 +21,9 @@ processed after that filter.
To enable WYSIWYG support, go to the WYSIWYG settings for each input format and
enable the Video Filter button.
To enable CKEditor (standalone) support, please see plugin instructions in:
editors/ckeditor/README.txt
========= Usage =========
Single video: [video:url]
@@ -1,35 +1,15 @@
##############################################
## ONLY if you use ckeditor WITHOUT wysiwyg ##
##############################################
############################################################
## ONLY if with the CKEditor module, *NOT* WYSIWYG module ##
############################################################
Installation:
Do the following steps to add video_filter button to the CKEditor toolbar:
1. Open ckeditor.config.js (in the ckeditor module root)
1. Go to Configuration -> CKEditor (admin/config/content/ckeditor)
Click "Edit" on the profile you what to use with the video filter.
2. Scroll down to the end of the file, right before "};" insert:
2. Expand "Editor appearance" and go to "Toolbar". Drag the new video_filter
button from the "All Buttons" toolbar to the "Used Buttons" toolbar.
// Video_filter plugin.
config.extraPlugins += (config.extraPlugins ? ',video_filter' : 'video_filter' );
CKEDITOR.plugins.addExternal('video_filter', Drupal.settings.basePath + Drupal.settings.video_filter.modulepath + '/editors/ckeditor/');
3. Add button to the toolbar.
3.1 Go to Configuration -> CKEditor (admin/config/content/ckeditor)
Click "Edit" on the profile you what to use with Linkit.
3.2 Expand "Editor appearance" and go to "Toolbar".
The button name is: video_filter
For example if you have a toolbar with an array of buttons defined as
follows:
['Bold','Italic']
simply add the button somewhere in the array:
['Bold','Italic','video_filter']
(remember the single quotes).
3. Go to "Plugins" on the same page. Enable the video filter plugin.
@@ -7,25 +7,126 @@
requires : [],
init: function(editor) {
// Add Button
editor.ui.addButton('video_filter', {
label: 'Video filter',
command: 'video_filter',
icon: this.path + 'video_filter.png'
});
// Add Command
editor.addCommand('video_filter', {
exec : function () {
var path = (Drupal.settings.video_filter.url.wysiwyg_ckeditor) ? Drupal.settings.video_filter.url.wysiwyg_ckeditor : Drupal.settings.video_filter.url.ckeditor
var media = window.showModalDialog(path, { 'opener' : window, 'editorname' : editor.name }, "dialogWidth:580px; dialogHeight:480px; center:yes; resizable:yes; help:no;");
}
});
// Register an extra fucntion, this will be used in the popup.
editor._.video_filterFnNum = CKEDITOR.tools.addFunction(insert, editor);
if(typeof window.showModalDialog !== 'undefined') {
editor.addCommand('video_filter', {
exec : function () {
var path = (Drupal.settings.video_filter.url.wysiwyg_ckeditor) ? Drupal.settings.video_filter.url.wysiwyg_ckeditor : Drupal.settings.video_filter.url.ckeditor
var media = window.showModalDialog(path, { 'opener' : window, 'editorname' : editor.name }, "dialogWidth:580px; dialogHeight:480px; center:yes; resizable:yes; help:no;");
}
});
// Register an extra function, this will be used in the popup.
editor._.video_filterFnNum = CKEDITOR.tools.addFunction(insert, editor);
}
else {
editor.addCommand('video_filter', new CKEDITOR.dialogCommand('video_filterDialog'));
}
}
});
CKEDITOR.dialog.add('video_filterDialog', function( editor ) {
var instructions_path = Drupal.settings.video_filter.instructions_url;
return {
title : 'Add Video',
minWidth : 600,
minHeight : 180,
contents : [{
id : 'general',
label : 'Settings',
elements : [
{
type : 'text',
id : 'file_url',
label : 'URL',
validate : CKEDITOR.dialog.validate.notEmpty( 'The link must have a URL.' ),
required : true,
commit : function( data )
{
data.file_url = this.getValue();
}
},
{
type : 'text',
id : 'width',
label : 'Width',
commit : function( data )
{
data.width = this.getValue();
}
},
{
type : 'text',
id : 'height',
label : 'Height',
commit : function( data )
{
data.height = this.getValue();
}
},
{
type : 'select',
id : 'align',
label : 'Align',
'default': 'none',
items: [
['None', ''],
['Left', 'left'],
['Right', 'right'],
['Center', 'center']
],
commit : function( data )
{
data.align = this.getValue();
}
},
{
type : 'checkbox',
id : 'autoplay',
label : 'Autoplay',
'default': '',
commit : function( data )
{
data.autoplay = this.getValue() ? 1 : 0;
}
},
{
type: 'html',
html: '<iframe src="' + instructions_path + '" style="width:100%; height: 200px;"></iframe>',
},
]
}],
onOk : function()
{
var dialog = this,
data = {},
link = editor.document.createElement( 'p' );
this.commitContent( data );
var str = '[video:' + data.file_url;
if (data.width) {
str += ' width:' + data.width;
}
if (data.height) {
str += ' height:' + data.height;
}
if (data.align) {
str += ' align:' + data.align;
}
if (data.autoplay) {
str += ' autoplay:' + data.autoplay;
}
str += ']';
link.setHtml( str );
editor.insertElement( link );
}
};
});
function insert(params, editor) {
@@ -49,6 +150,9 @@
if (params.autoplay) {
str += ' autoplay:' + params.autoplay;
}
else {
str += ' autoplay:' + '0';
}
str += ']';
for (var i = 0, len = ranges.length; i < len; i++) {
@@ -43,6 +43,9 @@ function Ok() {
if ($('#edit-autoplay').is(':checked')) {
str += ' autoplay:' + $('#edit-autoplay').val();
}
else {
str += ' autoplay:' + '0';
}
str += ']';
oEditor.FCKUndo.SaveUndoStep();
@@ -34,6 +34,9 @@ video_filter_dialog = {
if ($('#edit-autoplay').is(':checked')) {
str += ' autoplay:' + $('#edit-autoplay').val();
}
else {
str += ' autoplay:' + '0';
}
str += ']';
ed.execCommand('mceInsertContent', false, str);
@@ -0,0 +1,70 @@
<?php
/**
* @file
* Hooks provided by the Video Filter module.
*/
/**
* Defines video codecs and callbacks.
*
* @return array
* A video codec described as an associative array that may contain the
* following key-value pairs:
* - name: Required. A name for the codec.
* - sample_url: Required. An example of a URL the codec can handle.
* - callback: The function to call to generate embed code for the codec.
* Either this or html5_callback must be specified.
* - html5_callback: The function to call to generate device agnostic, HTML5
* embed code for the codec. Either this or callback must be specified.
* - instructions: Instructions for using the codec, to be displayed on the
* "Compse tips" page at filter/tips.
* - regexp: Required. A regular expression describing URLs the codec can
* handle. Multiple regular expressions may be supplied as an array.
* $video['codec']['delta'] will be set to the key of the match.
* - ratio: Required. A ratio for resizing the video within the dimensions
* optionally supplied in the token, expressed as height / width.
* - control_bar_height: The pixel height of the video player control bar, if
* applicable.
*/
function hook_codec_info() {
$codecs = array();
$codecs['minimal_example'] = array(
'name' => t('Minimal Example'),
'sample_url' => 'http://minimal.example.com/uN1qUeId',
'callback' => 'MODULE_minimal_example',
'regexp' => '/minimal\.example\.com\/([a-z0-9\-_]+)/i',
'ratio' => 4 / 3,
);
$codecs['complete_example'] = array(
'name' => t('Complete Example'),
'sample_url' => 'http://complete.example.com/username/uN1qUeId',
'callback' => 'MODULE_complete_example',
'html5_callback' => 'MODULE_complete_example_html5',
'instructions' => t('Your Complete Example username can be the first URL argument or a sub-subdomain.'),
'regexp' => array(
'/complete\.example\.com\/([a-z0-9\-_]+)\/([a-z0-9\-_]+)/i',
'/([a-z0-9\-_]+)\.complete\.example\.com\/([a-z0-9\-_]+)/i',
),
'ratio' => 4 / 3,
'control_bar_height' => 25,
);
return $codecs;
}
/**
* Alters the codecs available to Video Filter.
*
* @param array $codecs
*/
function hook_video_filter_codec_info_alter(&$codecs) {}
/**
* Alters a video's attributes previous to rendering.
*
* @param array $video
*/
function hook_video_filter_video_alter(&$video) {}
@@ -34,6 +34,15 @@ function video_filter_codec_info() {
'control_bar_height' => 30,
);
$codecs['candidcareer'] = array(
'name' => t('Candid Career'),
'sample_url' => 'https://www.candidcareer.com/embed.php?vkey=ed5fdd900a274930252f&shared=CandidCareer&uid=30',
'callback' => 'video_filter_candidcareer',
'regexp' => '/candidcareer\.com\/embed\.php\?vkey=([a-zA-Z0-9\-_&;]+)shared=([a-zA-Z0-9\-_&;]+)uid=([a-zA-Z0-9\-_&;]+)/',
'ratio' => 16 / 9,
'control_bar_height' => 20,
);
$codecs['capped'] = array(
'name' => t('Capped'),
'sample_url' => 'http://capped.tv/playeralt.php?vid=some-title',
@@ -196,7 +205,7 @@ function video_filter_codec_info() {
'sample_url' => 'http://wistia.com/medias/9pj9n6ftlk',
'callback' => 'video_filter_wistia_html5',
'html5_callback' => 'video_filter_wistia_html5',
'regexp' => '@https?://(.+\.)?(wistia\.com|wi\.st)/((m|medias|projects)|embed/(iframe|playlists))/([a-zA-Z0-9]+)@',
'regexp' => '@https?://(.+\.)?(wistia\.(com|net)|wi\.st)/((m|medias|projects)|embed/(iframe|playlists))/([a-zA-Z0-9]+)@',
);
$codecs['youtube'] = array(
@@ -205,9 +214,10 @@ function video_filter_codec_info() {
'callback' => 'video_filter_youtube',
'html5_callback' => 'video_filter_youtube_html5',
'regexp' => array(
'/youtube\.com\/watch\?v=([a-z0-9\-_]+)/i',
'/youtube\.com\/watch\?.*?v=([a-z0-9\-_]+)/i',
'/youtu.be\/([a-z0-9\-_]+)/i',
'/youtube\.com\/v\/([a-z0-9\-_]+)/i',
'/youtube\.com\/embed\/([a-z0-9\-_]+)/i',
),
'ratio' => 16 / 9,
'control_bar_height' => 25,
@@ -233,7 +243,7 @@ function video_filter_codec_info() {
* @see video_filter_codec_info()
*/
function video_filter_archive($video) {
$video['source'] = 'http://www.archive.org/embed/' . $video['codec']['matches'][1];
$video['source'] = '//www.archive.org/embed/' . $video['codec']['matches'][1];
return video_filter_iframe($video);
}
@@ -290,7 +300,7 @@ function video_filter_bliptv($video) {
}
}
$video['source'] = 'http://blip.tv/play/' . $id;
$video['source'] = '//blip.tv/play/' . $id;
$params = array(
'allowscriptaccess' => 'always',
);
@@ -298,13 +308,27 @@ function video_filter_bliptv($video) {
return video_filter_flash($video, $params);
}
/**
* Callback for Candid Career codec.
*
* @see video_filter_codec_info()
*/
function video_filter_candidcareer($video) {
// Their urls contain & symbols which Drupal is encoding, so decode those.
$decoded = decode_entities($video['codec']['matches'][0]);
$video['source'] = '//' . $decoded;
$video['attributes']['marginwidth'] = 0;
$video['attributes']['marginheight'] = 0;
return video_filter_iframe($video);
}
/**
* Callback for Capped codec.
*
* @see video_filter_codec_info()
*/
function video_filter_capped($video) {
$video['source'] = 'http://capped.micksam7.com/playeralt.swf?vid=' . $video['codec']['matches'][1];
$video['source'] = '//capped.micksam7.com/playeralt.swf?vid=' . $video['codec']['matches'][1];
return video_filter_flash($video);
}
@@ -315,7 +339,7 @@ function video_filter_capped($video) {
* @see video_filter_codec_info()
*/
function video_filter_collegehumor($video) {
$video['source'] = 'http://www.collegehumor.com/moogaloop/moogaloop.swf?clip_id=' . $video['codec']['matches'][1] . '&amp;fullscreen=1';
$video['source'] = '//www.collegehumor.com/moogaloop/moogaloop.swf?clip_id=' . $video['codec']['matches'][1] . '&amp;fullscreen=1';
return video_filter_flash($video);
}
@@ -326,7 +350,7 @@ function video_filter_collegehumor($video) {
* @see video_filter_codec_info()
*/
function video_filter_dailymotion($video) {
$video['source'] = 'http://www.dailymotion.com/swf/' . $video['codec']['matches'][1];
$video['source'] = '//www.dailymotion.com/swf/' . $video['codec']['matches'][1];
return video_filter_flash($video);
}
@@ -354,7 +378,7 @@ function video_filter_flickr_slideshows($video) {
* @see video_filter_codec_info()
*/
function video_filter_flickr_video($video) {
$video['source'] = 'http://www.flickr.com/apps/video/stewart.swf?v=1.161';
$video['source'] = '//www.flickr.com/apps/video/stewart.swf?v=1.161';
$params['flashvars'] = '&amp;photo_id=' . $video['codec']['matches'][2] . '&amp;flickr_show_info_box=true';
@@ -373,7 +397,7 @@ function video_filter_gametrailers($video) {
elseif (is_numeric($video['codec']['matches'][2])) {
$match = $video['codec']['matches'][2];
}
$video['source'] = 'http://media.mtvnservices.com/embed/mgid:moses:video:gametrailers.com:' . $match;
$video['source'] = '//media.mtvnservices.com/embed/mgid:moses:video:gametrailers.com:' . $match;
return video_filter_iframe($video);
}
@@ -384,7 +408,7 @@ function video_filter_gametrailers($video) {
* @see video_filter_codec_info()
*/
function video_filter_gamevideos($video) {
$video['source'] = 'http://gamevideos.1up.com/swf/gamevideos12.swf?embedded=1&amp;fullscreen=1&amp;autoplay=0&amp;src=http://gamevideos.1up.com/do/videoListXML%3Fid%3D' . $video['codec']['matches'][1];
$video['source'] = '//gamevideos.1up.com/swf/gamevideos12.swf?embedded=1&amp;fullscreen=1&amp;autoplay=0&amp;src=http://gamevideos.1up.com/do/videoListXML%3Fid%3D' . $video['codec']['matches'][1];
return video_filter_flash($video);
}
@@ -395,7 +419,7 @@ function video_filter_gamevideos($video) {
* @see video_filter_codec_info()
*/
function video_filter_godtube($video) {
$video['source'] = 'http://www.godtube.com/embed/watch/' . $video['codec']['matches'][1];
$video['source'] = '//www.godtube.com/embed/watch/' . $video['codec']['matches'][1];
return video_filter_iframe($video);
}
@@ -406,7 +430,7 @@ function video_filter_godtube($video) {
* @see video_filter_codec_info()
*/
function video_filter_google($video) {
$video['source'] = 'http://video.google.com/googleplayer.swf?docId=' . $video['codec']['matches'][1];
$video['source'] = '//video.google.com/googleplayer.swf?docId=' . $video['codec']['matches'][1];
return video_filter_flash($video);
}
@@ -417,7 +441,7 @@ function video_filter_google($video) {
* @see video_filter_codec_info()
*/
function video_filter_metacafe($video) {
$video['source'] = 'http://metacafe.com/fplayer/' . $video['codec']['matches'][1] . '/' . $video['codec']['matches'][2] . '.swf';
$video['source'] = '//metacafe.com/fplayer/' . $video['codec']['matches'][1] . '/' . $video['codec']['matches'][2] . '.swf';
return video_filter_flash($video);
}
@@ -430,7 +454,7 @@ function video_filter_metacafe($video) {
function video_filter_myspace($video) {
// The last match is the ID we need.
$last = count($video['codec']['matches']);
$video['source'] = 'http://mediaservices.myspace.com/services/media/embed.aspx/m=' . $video['codec']['matches'][$last - 1];
$video['source'] = '//mediaservices.myspace.com/services/media/embed.aspx/m=' . $video['codec']['matches'][$last - 1];
return video_filter_flash($video, $params);
}
@@ -441,7 +465,7 @@ function video_filter_myspace($video) {
* @see video_filter_codec_info()
*/
function video_filter_picasa_slideshows($video) {
$video['source'] = 'http://picasaweb.google.com/s/c/bin/slideshow.swf';
$video['source'] = '//picasaweb.google.com/s/c/bin/slideshow.swf';
$user_name = $video['codec']['matches'][1];
$set_id = $video['codec']['matches'][2];
@@ -457,7 +481,7 @@ function video_filter_picasa_slideshows($video) {
* @see video_filter_codec_info()
*/
function video_filter_slideshare($video) {
$video['source'] = 'http://www.slideshare.net/slideshow/embed_code/' . $video['codec']['matches'][1];
$video['source'] = '//www.slideshare.net/slideshow/embed_code/' . $video['codec']['matches'][1];
return video_filter_iframe($video);
}
@@ -468,7 +492,7 @@ function video_filter_slideshare($video) {
* @see video_filter_codec_info()
*/
function video_filter_streamhoster($video) {
$video['source'] = 'http://public.streamhoster.com/Resources/Flash/JWFLVMediaPlayer/mediaplayer.swf';
$video['source'] = '//public.streamhoster.com/Resources/Flash/JWFLVMediaPlayer/mediaplayer.swf';
$params = array('allowscriptaccess' => 'always');
$protocol = $video['codec']['matches'][1];
@@ -496,7 +520,7 @@ function video_filter_streamhoster($video) {
* @see video_filter_codec_info()
*/
function video_filter_teachertube($video) {
$video['source'] = 'http://www.teachertube.com/embed/player.swf';
$video['source'] = '//www.teachertube.com/embed/player.swf';
$params['flashvars'] = 'file=http://www.teachertube.com/embedFLV.php?pg=video_' . $video['codec']['matches'][1] . '&amp;menu=false&amp;frontcolor=ffffff&amp;lightcolor=FF0000&amp;logo=http://www.teachertube.com/www3/images/greylogo.swf&amp;skin=http://www.teachertube.com/embed/overlay.swf&amp;volume=80&amp;controlbar=over&amp;displayclick=link&amp;viral.link=http://www.teachertube.com/viewVideo.php?video_id=' . $video['codec']['matches'][1] . '&amp;stretching=exactfit&amp;plugins=viral-2&amp;viral.callout=none&amp;viral.onpause=false';
@@ -509,7 +533,7 @@ function video_filter_teachertube($video) {
* @see video_filter_codec_info()
*/
function video_filter_vimeo($video) {
$video['source'] = 'http://www.vimeo.com/moogaloop.swf?clip_id=' . $video['codec']['matches'][1] . '&amp;server=www.vimeo.com&amp;fullscreen=1&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;autoplay=' . $video['autoplay'];
$video['source'] = '//www.vimeo.com/moogaloop.swf?clip_id=' . $video['codec']['matches'][1] . '&amp;server=www.vimeo.com&amp;fullscreen=1&amp;show_title=1&amp;show_byline=1&amp;show_portrait=0&amp;color=&amp;autoplay=' . $video['autoplay'];
return video_filter_flash($video);
}
@@ -520,7 +544,7 @@ function video_filter_vimeo($video) {
* @see video_filter_codec_info()
*/
function video_filter_vimeo_html5($video) {
$video['source'] = 'http://player.vimeo.com/video/' . $video['codec']['matches'][1] . ($video['autoplay'] ? '?autoplay=1' : '');
$video['source'] = '//player.vimeo.com/video/' . $video['codec']['matches'][1] . ($video['autoplay'] ? '?autoplay=1' : '');
return video_filter_iframe($video);
}
@@ -537,7 +561,7 @@ function video_filter_youtube($video) {
'fs' => 'fs=1',
);
$video['source'] = 'http://www.youtube.com/v/' . $video['codec']['matches'][1] . '?' . implode('&amp;', $attributes);
$video['source'] = '//www.youtube.com/v/' . $video['codec']['matches'][1] . '?' . implode('&amp;', $attributes);
$params['wmode'] = 'opaque';
@@ -551,11 +575,12 @@ function video_filter_youtube($video) {
*/
function video_filter_youtube_html5($video) {
$attributes = array(
'html5' => 'html5=1',
'rel' => $video['related'] ? 'rel=1' : 'rel=0',
'autoplay' => $video['autoplay'] ? 'autoplay=1' : 'autoplay=0',
'wmode' => 'wmode=opaque',
);
$video['source'] = 'http://www.youtube.com/embed/' . $video['codec']['matches'][1] . '?' . implode('&amp;', $attributes);
$video['source'] = '//www.youtube.com/embed/' . $video['codec']['matches'][1] . '?' . implode('&amp;', $attributes);
return video_filter_iframe($video);
}
@@ -571,7 +596,7 @@ function video_filter_youtube_playlist_html5($video) {
'autoplay' => $video['autoplay'] ? 'autoplay=1' : 'autoplay=0',
'wmode' => 'wmode=opaque',
);
$video['source'] = 'http://www.youtube.com/embed/videoseries?list=' . $video['codec']['matches'][1] . '&amp;' . implode('&amp;', $attributes);
$video['source'] = '//www.youtube.com/embed/videoseries?list=' . $video['codec']['matches'][1] . '&amp;' . implode('&amp;', $attributes);
return video_filter_iframe($video);
}
@@ -584,9 +609,9 @@ function video_filter_youtube_playlist_html5($video) {
* @see video_filter_codec_info()
*/
function video_filter_wistia_html5($video) {
$video_code = $video['codec']['matches'][6];
$video_code = $video['codec']['matches'][7];
$matches = $video['codec']['matches'];
$embed_type = ($matches[3] == 'projects' || $matches[5] == 'playlists') ? 'playlists' : 'iframe';
$embed_type = ($matches[4] == 'projects' || $matches[6] == 'playlists') ? 'playlists' : 'iframe';
// Get embed code via oEmbed.
$endpoint = 'http://fast.wistia.com/oembed';
@@ -599,12 +624,12 @@ function video_filter_wistia_html5($video) {
$html = $data['html'];
// See if the video source is already an iframe src.
$pattern = '@https?://fast.wistia.com/embed/(iframe|playlists)/[a-zA-Z0-9]+\?+.+@';
$pattern = '@https?://fast.wistia.(com|net)/embed/(iframe|playlists)/[a-zA-Z0-9]+\?+.+@';
$matches = array();
if (preg_match($pattern, $video['source'], $matches)) {
// Replace the oEmbed iframe src with that provided in the token, in order
// to support embed builder URLs.
$pattern = '@https?://fast.wistia.com/embed/(iframe|playlists)/[a-zA-Z0-9]+\?[^"]+@';
$pattern = '@https?://fast.wistia.(com|net)/embed/(iframe|playlists)/[a-zA-Z0-9]+\?[^"]+@';
$replacement = $matches[0];
$html = preg_replace($pattern, $replacement, $html);
}
@@ -5,9 +5,9 @@ package = Input filters
stylesheets[all][] = video_filter.css
; Information added by drupal.org packaging script on 2012-11-14
version = "7.x-3.1"
; Information added by Drupal.org packaging script on 2015-09-24
version = "7.x-3.1+17-dev"
core = "7.x"
project = "video_filter"
datestamp = "1352915891"
datestamp = "1443119944"
@@ -284,9 +284,14 @@ function theme_video_filter_flash($variables) {
$classes = video_filter_get_classes($video);
$output .= '<object class="' . implode(' ', $classes) . '" type="application/x-shockwave-flash" ';
$attributes = '';
if (!empty($video['attributes'])) {
$attributes = drupal_attributes($video['attributes']);
}
$output .= 'width="' . $video['width'] . '" height="' . $video['height'] . '" data="' . $video['source'] . '">' . "\n";
$output .= '<div class="video-filter"><object class="' . implode(' ', $classes) . '" type="application/x-shockwave-flash" ';
$output .= 'width="' . $video['width'] . '" height="' . $video['height'] . '" data="' . $video['source'] . '" ' . $attributes . '>' . "\n";
$defaults = array(
'movie' => $video['source'],
@@ -300,7 +305,7 @@ function theme_video_filter_flash($variables) {
$output .= ' <param name="' . $name . '" value="' . $value . '" />' . "\n";
}
$output .= '</object>' . "\n";
$output .= '</object></div>' . "\n";
return $output;
}
@@ -312,10 +317,13 @@ function theme_video_filter_flash($variables) {
*/
function theme_video_filter_iframe($variables) {
$video = $variables['video'];
$classes = video_filter_get_classes($video);
$attributes = '';
if (!empty($video['attributes'])) {
$attributes = drupal_attributes($video['attributes']);
}
$output = '<iframe src="' . $video['source'] . '" width="' . $video['width'] . '" height="' . $video['height'] . '" class="' . implode(' ', $classes) . '" frameborder="0"></iframe>';
$output = '<div class="video-filter"><iframe src="' . $video['source'] . '" width="' . $video['width'] . '" height="' . $video['height'] . '" class="' . implode(' ', $classes) . '" frameborder="0"' . $attributes . '></iframe></div>';
return $output;
}
@@ -378,6 +386,15 @@ function video_filter_menu() {
'theme callback' => '_video_filter_dashboard_theme',
);
$items['video_filter/instructions'] = array(
'title' => 'Videofilter instructions',
'description' => 'instructions',
'page callback' => 'video_filter_instructions_page',
'access arguments' => array('access content'),
'type' => MENU_CALLBACK,
'theme callback' => '_video_filter_dashboard_theme',
);
return $items;
}
@@ -432,7 +449,19 @@ function video_filter_dashboard_page($editor) {
break;
}
print theme('video_filter_dashboard', array('form' => render(drupal_get_form('_video_filter_form'))));
$form = drupal_get_form('_video_filter_form');
print theme('video_filter_dashboard', array('form' => render($form)));
exit();
}
/**
* Creates the instructions page.
*/
function video_filter_instructions_page() {
module_invoke('admin_menu', 'suppress');
$form = drupal_get_form('_video_filter_instructions_form');
print theme('video_filter_dashboard', array('form' => render($form)));
exit();
}
@@ -492,6 +521,25 @@ function _video_filter_form() {
'#weight' => 5,
);
$form += _video_filter_instructions_form();
$form['cancel'] = array(
'#type' => 'button',
'#value' => t('Cancel'),
'#weight' => 98,
);
$form['insert'] = array(
'#type' => 'button',
'#value' => t('Insert'),
'#weight' => 99,
);
return $form;
}
function _video_filter_instructions_form() {
$form['instructions'] = array(
'#type' => 'fieldset',
'#title' => t('Instructions'),
@@ -509,21 +557,28 @@ function _video_filter_form() {
'#markup' => $text,
);
$form['cancel'] = array(
'#type' => 'button',
'#value' => t('Cancel'),
'#weight' => 98,
);
$form['insert'] = array(
'#type' => 'button',
'#value' => t('Insert'),
'#weight' => 99,
);
return $form;
}
/**
* Implements hook_ckeditor_plugin().
*/
function video_filter_ckeditor_plugin() {
$plugins = array();
$plugins['video_filter'] = array(
'name' => 'video_filter',
'desc' => t('Plugin to directly embed videos with the video filter module.'),
'path' => drupal_get_path('module', 'video_filter') . '/editors/ckeditor/',
'buttons' => array(
'video_filter' => array(
'label' => t('Video filter'),
'icon' => 'video_filter.png',
),
),
);
return $plugins;
}
function video_filter_wysiwyg_plugin($editor, $version) {
_video_filter_add_settings('wysiwyg_' . $editor);
@@ -582,7 +637,10 @@ function _video_filter_add_settings($editor) {
// Add popup url.
$settings = array(
'video_filter' => array('url' => array($editor => url('video_filter/dashboard/' . $editor))),
'video_filter' => array(
'url' => array($editor => url('video_filter/dashboard/' . $editor)),
'instructions_url' => url('video_filter/instructions'),
),
);
drupal_add_js($settings, 'setting');
}
@@ -1,3 +1,42 @@
Search API 1.x, dev (xxxx-xx-xx):
---------------------------------
- #2667872 by Les Lim: Added "0" to field boost options.
- #2654328 by drunken monkey, donquixote: Fixed use of "<" and ">" for open
facet ranges.
- #2639200 by joachim: Added sorting to "related fields" select box.
- #2638740 by joachim, drunken monkey: Added a link to the index to the
"re-indexing necessary" message.
- #2629136 by drunken monkey, deranga: Fixed wrong facet counts in edge cases
for active OR facets.
- #2569461 by kraynuk.m, drunken monkey: Fixed existing table in update #7118.
- #2631276 by tauno: Fixed the MLT handler for multi-entity indexes.
- #2576265 by drunken monkey: Fixed view trying to search on non-fulltext field.
- #2572487 by drunken monkey: Removed operator setting for date facets.
- #2611714 by rakesh.gectcr, drunken monkey: Improved compliance with
documentation standards.
- #2613054 by temkin: Fixed the "search-api-index" Drush command to allow
setting further options when indexing on all indexes.
- #2611726 by Hubbs, rakesh.gectcr: Fixed several typos.
- #2603500 by drunken monkey, krishna savithraj: Fixed Views fulltext searches
for keyword "All".
- #2529262 by kingmackenzie, stefan.r: Added an option to Views date filters to
choose the format used by date popup.
- #2583263 by drunken monkey: Fixed Views integration in combination with
Search API ET and similar modules.
- #2592231 by drunken monkey, balintcsaba: Fixed ignored item language when
viewing translated items.
- #2570879 by thePanz, drunken monkey: Added sorting of remembered search IDs.
- #2565743 by drunken monkey: Fixed creation of comment indexes with specific
bundles.
- #2563793 by drunken monkey, smitty, ReBa: Fixed Views base table definition
for "Multiple types" indexes.
- #2567775 by joseph.olstad, drunken monkey: Fixed handling of broken HTML in
the "HTML filter" processor.
- #2565005 by drunken monkey: Properly escape labels of "checkboxes"/"radios"
options
- #2524314 by drunken monkey: Fixed bundle-setting for taxonomy term indexes.
- #2550599 by ACF, drunken monkey: Fixed error on entity rebuilds.
Search API 1.16 (2015-08-30):
-----------------------------
- #2502819: Fixed example code for hook_search_api_query_alter().
@@ -78,13 +78,10 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
// displayed.
$facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();
// Remember this search ID, if necessary.
$this->rememberSearchId($index_id, $search_id);
if (array_search($search_id, $facet_search_ids) === FALSE) {
$search_ids = variable_get('search_api_facets_search_ids', array());
if (empty($search_ids[$index_id][$search_id])) {
// Remember this search ID.
$search_ids[$index_id][$search_id] = $search_id;
variable_set('search_api_facets_search_ids', $search_ids);
}
if (!$default_true) {
continue; // We are only to show facets for explicitly named search ids.
}
@@ -103,6 +100,23 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
}
}
/**
* Adds a search ID to the list of known searches for an index.
*
* @param string $index_id
* The machine name of the search index.
* @param string $search_id
* The identifier of the executed search.
*/
protected function rememberSearchId($index_id, $search_id) {
$search_ids = variable_get('search_api_facets_search_ids', array());
if (empty($search_ids[$index_id][$search_id])) {
$search_ids[$index_id][$search_id] = $search_id;
asort($search_ids[$index_id]);
variable_set('search_api_facets_search_ids', $search_ids);
}
}
/**
* Add the given facet to the query.
*/
@@ -257,6 +271,9 @@ class SearchApiFacetapiAdapter extends FacetapiAdapter {
'#options' => $granularity_options,
'#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE,
);
// Date facets don't support the "OR" operator (for now).
$form['global']['operator']['#access'] = FALSE;
}
// Add an "Exclude" option for terms.
@@ -174,7 +174,7 @@ class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTy
// Always include the active facet items.
foreach ($this->adapter->getActiveItems($this->facet) as $filter) {
$build[$filter['value']]['#count'] = $results['result count'];
$build[$filter['value']]['#count'] = 0;
}
// Then, add the facets returned by the server.
@@ -9,9 +9,9 @@ files[] = plugins/facetapi/adapter.inc
files[] = plugins/facetapi/query_type_term.inc
files[] = plugins/facetapi/query_type_date.inc
; Information added by Drupal.org packaging script on 2015-08-30
version = "7.x-1.16"
; Information added by Drupal.org packaging script on 2016-02-26
version = "7.x-1.16+29-dev"
core = "7.x"
project = "search_api"
datestamp = "1440962813"
datestamp = "1456500713"
@@ -340,13 +340,13 @@ function search_api_facetapi_facet_map_callback(array $values, array $options =
$lower = isset($map[$range['lower']]) ? $map[$range['lower']] : $range['lower'];
$upper = isset($map[$range['upper']]) ? $map[$range['upper']] : $range['upper'];
if ($lower == '*' && $upper == '*') {
$map[$value] = t('any');
$map[$value] = t('any');
}
elseif ($lower == '*') {
$map[$value] = "< $upper";
$map[$value] = " $upper";
}
elseif ($upper == '*') {
$map[$value] = "> $lower";
$map[$value] = " $lower";
}
else {
$map[$value] = "$lower $upper";
@@ -18,6 +18,7 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
$options = parent::option_definition();
unset($options['break_phrase']);
unset($options['not']);
$options['entity_type'] = array('default' => FALSE);
$options['fields'] = array('default' => array());
return $options;
}
@@ -31,6 +32,20 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
unset($form['not']);
$index = search_api_index_load(substr($this->table, 17));
if ($index->datasource() instanceof SearchApiCombinedEntityDataSourceController) {
$types = array_intersect_key(search_api_entity_type_options_list(), array_flip($index->options['datasource']['types']));
$form['entity_type'] = array(
'#type' => 'select',
'#title' => t('Entity type'),
'#description' => t('Select the entity type of the argument.'),
'#options' => $types,
'#default_value' => $this->options['entity_type'],
'#required' => TRUE,
);
}
if (!empty($index->options['fields'])) {
$fields = array();
foreach ($index->getFields() as $key => $field) {
@@ -78,8 +93,15 @@ class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArg
else {
$fields = array_intersect($this->options['fields'], $index_fields);
}
if ($this->query->getIndex()->datasource() instanceof SearchApiCombinedEntityDataSourceController) {
$id = $this->options['entity_type'] . '/' . $this->argument;
}
else {
$id = $this->argument;
}
$mlt = array(
'id' => $this->argument,
'id' => $id,
'fields' => $fields,
);
$this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
@@ -16,6 +16,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
public function option_definition() {
return parent::option_definition() + array(
'widget_type' => array('default' => 'default'),
'date_popup_format' => array('default' => 'm/d/Y'),
'year_range' => array('default' => '-3:+3'),
);
}
@@ -35,14 +36,31 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
*/
public function extra_options_form(&$form, &$form_state) {
parent::extra_options_form($form, $form_state);
if (module_exists('date_popup')) {
$widget_options = array('default' => 'Default', 'date_popup' => 'Date popup');
$widget_options = array(
'default' => 'Default',
'date_popup' => 'Date popup',
);
$form['widget_type'] = array(
'#type' => 'radios',
'#title' => t('Date selection form element'),
'#default_value' => $this->options['widget_type'],
'#options' => $widget_options,
);
$form['date_popup_format'] = array(
'#type' => 'textfield',
'#title' => t('Date format'),
'#default_value' => $this->options['date_popup_format'],
'#description' => t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "Y-m-d" or "m/d/Y".', array(
'@doc-link' => 'http://php.net/manual/en/function.date.php'
)),
'#states' => array(
'visible' => array(
':input[name="options[widget_type]"]' => array('value' => 'date_popup'),
),
),
);
}
if (module_exists('date_api')) {
@@ -74,7 +92,7 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
// according to what date_popup expects.
if ($this->options['widget_type'] == 'date_popup' && module_exists('date_popup')) {
$form['value']['#type'] = 'date_popup';
$form['value']['#date_format'] = 'm/d/Y';
$form['value']['#date_format'] = $this->options['date_popup_format'];
$form['value']['#date_year_range'] = $this->options['year_range'];
unset($form['value']['#description']);
}
@@ -144,7 +144,8 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
return;
}
$fields = $this->options['fields'];
$fields = $fields ? $fields : array_keys($this->getFulltextFields());
$available_fields = array_keys($this->getFulltextFields());
$fields = $fields ? array_intersect($fields, $available_fields) : $available_fields;
// If something already specifically set different fields, we silently fall
// back to mere filtering.
@@ -17,4 +17,52 @@ class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
return array('=' => t('contains'), '<>' => t("doesn't contain"));
}
/**
* Determines whether input from the exposed filters affects this filter.
*
* Overridden to not treat "All" differently.
*
* @param array $input
* The user input from the exposed filters.
*
* @return bool
* TRUE if the input should change the behavior of this filter.
*/
public function accept_exposed_input($input) {
if (empty($this->options['exposed'])) {
return TRUE;
}
if (!empty($this->options['expose']['use_operator']) && !empty($this->options['expose']['operator_id']) && isset($input[$this->options['expose']['operator_id']])) {
$this->operator = $input[$this->options['expose']['operator_id']];
}
if (!empty($this->options['expose']['identifier'])) {
$value = $input[$this->options['expose']['identifier']];
// Various ways to check for the absence of non-required input.
if (empty($this->options['expose']['required'])) {
if (($this->operator == 'empty' || $this->operator == 'not empty') && $value === '') {
$value = ' ';
}
if (!empty($this->always_multiple) && $value === '') {
return FALSE;
}
}
if (isset($value)) {
$this->value = $value;
if (empty($this->always_multiple) && empty($this->options['expose']['multiple'])) {
$this->value = array($value);
}
}
else {
return FALSE;
}
}
return TRUE;
}
}
@@ -538,7 +538,7 @@ class SearchApiViewsQuery extends views_plugin_query {
* query backend.
*/
public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
$type = $this->index->item_type;
$type = $this->index->getEntityType() ? $this->index->getEntityType() : $this->index->item_type;
$wrappers = array();
$load_items = array();
foreach ($results as $row_index => $row) {
@@ -27,9 +27,9 @@ files[] = includes/handler_sort.inc
files[] = includes/plugin_cache.inc
files[] = includes/query.inc
; Information added by Drupal.org packaging script on 2015-08-30
version = "7.x-1.16"
; Information added by Drupal.org packaging script on 2016-02-26
version = "7.x-1.16+29-dev"
core = "7.x"
project = "search_api"
datestamp = "1440962813"
datestamp = "1456500713"
@@ -11,7 +11,6 @@
function search_api_views_views_data() {
try {
$data = array();
$entity_types = entity_get_info();
foreach (search_api_index_load_multiple(FALSE) as $index) {
// Fill in base data.
$key = 'search_api_index_' . $index->machine_name;
@@ -25,12 +24,8 @@ function search_api_views_views_data() {
'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
'query class' => 'search_api_views_query',
);
if (isset($entity_types[$index->getEntityType()])) {
$table['table'] += array(
'entity type' => $index->getEntityType(),
'skip entity load' => TRUE,
);
}
$table['table']['entity type'] = $index->getEntityType();
$table['table']['skip entity load'] = TRUE;
try {
$wrapper = $index->entityWrapper(NULL, FALSE);
@@ -269,10 +269,13 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
/**
* Helper method for getting all available aggregation types.
*
* @param $info (optional)
* One of "name", "type" or "description", to indicate what values should be
* returned for the types. Defaults to "name".
* @param string $info
* (optional) One of "name", "type" or "description", to indicate what
* information should be returned for the types.
*
* @return string[]
* An associative array of aggregation type identifiers mapped to their
* names, data types or descriptions, as requested.
*/
protected function getTypes($info = 'name') {
switch ($info) {
@@ -310,6 +313,7 @@ class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
'list' => t('The List aggregation collects all field values into a multi-valued field containing all values.'),
);
}
return array();
}
/**
@@ -74,7 +74,7 @@ class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {
// we use try/catch. This will at least prevent some errors, even though
// it's no protection against fatal errors and the like.
try {
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode);
$render = entity_view($type, array(entity_id($type, $item) => $item), $mode, $item->search_api_language);
$text = render($render);
if (!$text) {
$item->search_api_viewed = NULL;
@@ -70,7 +70,7 @@ class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
foreach ($list as $lang) {
$name = t($lang->name);
$native = $lang->native;
$languages[$lang->language] = ($name == $native) ? $name : "$name ($native)";
$languages[$lang->language] = check_plain(($name == $native) ? $name : "$name ($native)");
if (!$lang->enabled) {
$languages[$lang->language] .= ' [' . t('disabled') . ']';
}
@@ -22,7 +22,7 @@
interface SearchApiDataSourceControllerInterface {
/**
* Constructs a new data source controller.
* Constructs an SearchApiDataSourceControllerInterface object.
*
* @param string $type
* The item type for which this controller is created.
@@ -47,7 +47,7 @@ interface SearchApiDataSourceControllerInterface {
* Loads items of the type of this data source controller.
*
* @param array $ids
* The IDs of the items to laod.
* The IDs of the items to load.
*
* @return array
* The loaded items, keyed by ID.
@@ -592,7 +592,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
* {@inheritdoc}
*/
public function trackItemInsert(array $item_ids, array $indexes) {
if (!$this->table) {
if (!$this->table || $item_ids === array()) {
return;
}
@@ -622,7 +622,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
* {@inheritdoc}
*/
public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
if (!$this->table) {
if (!$this->table || $item_ids === array()) {
return NULL;
}
@@ -652,7 +652,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
*/
public function trackItemQueued($item_ids, SearchApiIndex $index) {
$this->checkIndex($index);
if (!$this->table) {
if (!$this->table || $item_ids === array()) {
return;
}
$update = db_update($this->table)
@@ -670,7 +670,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
* {@inheritdoc}
*/
public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
if (!$this->table) {
if (!$this->table || $item_ids === array()) {
return;
}
$this->checkIndex($index);
@@ -687,7 +687,7 @@ abstract class SearchApiAbstractDataSourceController implements SearchApiDataSou
* {@inheritdoc}
*/
public function trackItemDelete(array $item_ids, array $indexes) {
if (!$this->table) {
if (!$this->table || $item_ids === array()) {
return NULL;
}
@@ -160,7 +160,36 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
$query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
$query->addExpression('1', 'changed');
if ($bundles = $this->getIndexBundles($index)) {
$query->condition($this->bundleKey, $bundles);
$bundle_column = $this->bundleKey;
if (!db_field_exists($table, $bundle_column)) {
if ($this->entityType == 'taxonomy_term') {
$bundle_column = 'vid';
$bundles = db_query('SELECT vid FROM {taxonomy_vocabulary} WHERE machine_name IN (:bundles)', array(':bundles' => $bundles))->fetchCol();
}
elseif ($this->entityType == 'comment') {
// Comments are significantly more complicated, since they don't
// store their bundle explicitly in their database table. Instead,
// we need to get all the nodes from the enabled types and filter
// by those.
$bundle_column = 'nid';
$node_types = array();
foreach ($bundles as $bundle) {
if (substr($bundle, 0, 13) === 'comment_node_') {
$node_types[] = substr($bundle, 13);
}
}
if ($node_types) {
$bundles = db_query('SELECT nid FROM {node} WHERE type IN (:bundles)', array(':bundles' => $node_types))->fetchCol();
}
else {
return;
}
}
else {
$this->startTrackingFallback(array($index->machine_name => $index));
}
}
$query->condition($bundle_column, $bundles);
}
// INSERT ... SELECT ...
@@ -170,20 +199,38 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
}
}
else {
// In the absence of a 'base table', use the slower way of retrieving the
// items and inserting them "manually". For each index we get the item IDs
// (since selected bundles might differ) and insert all of them as new.
foreach ($indexes as $index) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', $this->entityType);
if ($bundles = $this->getIndexBundles($index)) {
$query->entityCondition('bundle', $bundles);
}
$result = $query->execute();
$ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array();
if ($ids) {
$this->trackItemInsert($ids, array($index));
}
$this->startTrackingFallback($indexes);
}
}
/**
* Initializes tracking of the index status of items for the given indexes.
*
* Fallback for when the items cannot directly be loaded into
* {search_api_item} via "INSERT INTO … SELECT …".
*
* @param SearchApiIndex[] $indexes
* The indexes for which item tracking should be initialized.
*
* @throws SearchApiDataSourceException
* Thrown if any error state was encountered.
*
* @see SearchApiEntityDataSourceController::startTracking()
*/
protected function startTrackingFallback(array $indexes) {
// In the absence of a 'base table', use the slower way of retrieving the
// items and inserting them "manually". For each index we get the item IDs
// (since selected bundles might differ) and insert all of them as new.
foreach ($indexes as $index) {
$query = new EntityFieldQuery();
$query->entityCondition('entity_type', $this->entityType);
if ($bundles = $this->getIndexBundles($index)) {
$query->entityCondition('bundle', $bundles);
}
$result = $query->execute();
$ids = !empty($result[$this->entityType]) ? array_keys($result[$this->entityType]) : array();
if ($ids) {
$this->trackItemInsert($ids, array($index));
}
}
}
@@ -225,7 +272,7 @@ class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceCon
'#type' => 'checkboxes',
'#title' => t('Bundles'),
'#description' => t('Restrict the entity bundles that will be included in this index. Leave blank to include all bundles. This setting cannot be changed for existing indexes.'),
'#options' => $options,
'#options' => array_map('check_plain', $options),
'#attributes' => array('class' => array('search-api-checkboxes-list')),
'#disabled' => !empty($form_state['index']),
);
@@ -252,7 +252,7 @@ class SearchApiCombinedEntityDataSourceController extends SearchApiAbstractDataS
'#type' => 'checkboxes',
'#title' => t('Entity types'),
'#description' => t('Select the entity types which should be included in this index.'),
'#options' => search_api_entity_type_options_list(),
'#options' => array_map('check_plain', search_api_entity_type_options_list()),
'#attributes' => array('class' => array('search-api-checkboxes-list')),
'#disabled' => !empty($form_state['index']),
'#required' => TRUE,
@@ -19,7 +19,7 @@ class SearchApiException extends Exception {
*/
public function __construct($message = NULL) {
if (!$message) {
$message = t('An error occcurred in the Search API framework.');
$message = t('An error occurred in the Search API framework.');
}
parent::__construct($message);
}
@@ -934,7 +934,12 @@ class SearchApiIndex extends Entity {
$i = $only_indexed ? 1 : 0;
if (!isset($this->fulltext_fields[$i])) {
$this->fulltext_fields[$i] = array();
$fields = $only_indexed ? $this->options['fields'] : $this->getFields(FALSE);
if ($only_indexed) {
$fields = isset($this->options['fields']) ? $this->options['fields'] : array();
}
else {
$fields = $this->getFields(FALSE);
}
foreach ($fields as $key => $field) {
if (search_api_is_text_type($field['type'])) {
$this->fulltext_fields[$i][] = $key;
@@ -172,7 +172,7 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface
$default_fields = drupal_map_assoc(array_keys($this->options['fields']));
}
foreach ($fields as $name => $field) {
$field_options[$name] = $field['name'];
$field_options[$name] = check_plain($field['name']);
if (!empty($default_fields[$name]) || (!isset($this->options['fields']) && $this->testField($name, $field))) {
$default_fields[$name] = $name;
}
@@ -390,13 +390,15 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface
}
/**
* Determines whether to process data from the given field.
*
* @param $name
* The field's machine name.
* @param array $field
* The field's information.
*
* @return
* TRUE, iff the field should be processed.
* @return bool
* TRUE, if the field should be processed, FALSE otherwise.
*/
protected function testField($name, array $field) {
if (empty($this->options['fields'])) {
@@ -406,8 +408,13 @@ abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface
}
/**
* @return
* TRUE, iff the type should be processed.
* Determines whether fields of the given type should normally be processed.
*
* Defaults to processing text types, but can easily be overridden by
* subclasses.
*
* @return bool
* TRUE, if the type should be processed, FALSE otherwise.
*/
protected function testType($type) {
return search_api_is_text_type($type, array('text', 'tokens'));
@@ -86,12 +86,12 @@ class SearchApiHighlight extends SearchApiAbstractProcessor {
),
),
);
// Exclude certain fulltextfields
// Exclude certain fulltext fields.
$fields = $this->index->getFields();
$fulltext_fields = array();
foreach ($this->index->getFulltextFields() as $field) {
if (isset($fields[$field])) {
$fulltext_fields[$field] = $fields[$field]['name'] . ' (' . $field . ')';
$fulltext_fields[$field] = check_plain($fields[$field]['name'] . ' (' . $field . ')');
}
}
$form['exclude_fields'] = array(
@@ -120,7 +120,9 @@ class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
);
}
$text = substr($text, $pos + 1);
preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m);
if (!preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m)) {
continue;
}
$text = substr($text, strpos($text, '>') + 1);
if ($m[1]) {
// Closing tag.
@@ -86,7 +86,9 @@ class SearchApiStopWords extends SearchApiAbstractProcessor {
}
/**
* @return
* Retrieves the processor's configured stopwords.
*
* @return array
* An array whose keys are the stopwords set in either the file or the text
* field.
*/
@@ -155,7 +155,7 @@ interface SearchApiQueryInterface {
* The field to sort by. The special fields 'search_api_relevance' (sort by
* relevance) and 'search_api_id' (sort by item id) may be used. Also, if
* the search server supports the "search_api_random_sort" feature, the
* "search_api_random_sort" special field can be used to sort randomly.
* "search_api_random" special field can be used to sort randomly.
* @param string $order
* The order to sort items in - either 'ASC' or 'DESC'.
*
@@ -219,7 +219,10 @@ interface SearchApiQueryInterface {
* - execution: The actual query to the search server, in whatever form.
* - postprocessing: Preparing the results for returning.
* Additional metadata may be returned in other keys. Only 'result count'
* and 'result' always have to be set, all other entries are optional.
* and 'results' always have to be set, all other entries are optional.
*
* @throws SearchApiException
* If an error prevented the search from completing.
*/
public function execute();
@@ -468,8 +471,8 @@ class SearchApiQuery implements SearchApiQueryInterface {
);
$modes['terms'] = array(
'name' => t('Multiple terms'),
'description' => t('The query is interpreted as multiple keywords seperated by spaces. ' .
'Keywords containing spaces may be "quoted". Quoted keywords must still be seperated by spaces.'),
'description' => t('The query is interpreted as multiple keywords separated by spaces. ' .
'Keywords containing spaces may be "quoted". Quoted keywords must still be separated by spaces.'),
);
// @todo Add fourth mode for complicated expressions, e.g.: »"vanilla ice" OR (love NOT hate)«
return $modes;
@@ -241,7 +241,7 @@ class SearchApiServer extends Entity {
/**
* Adds a new index to this server.
*
* If an exception in the service class implementation of this method occcurs,
* If an exception in the service class implementation of this method occurs,
* it will be caught and the operation saved as an pending server task.
*
* @see SearchApiServiceInterface::addIndex()
@@ -268,7 +268,7 @@ class SearchApiServer extends Entity {
* If the service class implementation of the method returns TRUE, this will
* automatically take care of marking the items on the index for re-indexing.
*
* If an exception in the service class implementation of this method occcurs,
* If an exception in the service class implementation of this method occurs,
* it will be caught and the operation saved as an pending server task.
*
* @see SearchApiServiceInterface::fieldsUpdated()
@@ -296,7 +296,7 @@ class SearchApiServer extends Entity {
/**
* Removes an index from this server.
*
* If an exception in the service class implementation of this method occcurs,
* If an exception in the service class implementation of this method occurs,
* it will be caught and the operation saved as an pending server task.
*
* @see SearchApiServiceInterface::removeIndex()
@@ -334,7 +334,7 @@ class SearchApiServer extends Entity {
/**
* Deletes indexed items from this server.
*
* If an exception in the service class implementation of this method occcurs,
* If an exception in the service class implementation of this method occurs,
* it will be caught and the operation saved as an pending server task.
*
* @see SearchApiServiceInterface::deleteItems()
@@ -1806,7 +1806,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
// An array of option arrays for types, keyed by nesting level.
$types = array(0 => search_api_field_types());
$entity_types = entity_get_info();
$boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
$boosts = drupal_map_assoc(array('0.0', '0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
$fulltext_types = array(0 => array('text'));
// Add all custom data types with fallback "text" to fulltext types as well.
@@ -1949,6 +1949,7 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
);
if ($additional) {
asort($additional);
reset($additional);
$form['additional'] = array(
'#type' => 'fieldset',
@@ -2102,6 +2103,9 @@ function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapp
* @param array $variables
* An associative array containing:
* - element: A render element representing the form.
*
* @return string
* The HTML for a field list form.
*/
function theme_search_api_admin_fields_table($variables) {
$form = $variables['element'];
@@ -2147,6 +2151,7 @@ function theme_search_api_admin_fields_table($variables) {
function search_api_admin_index_fields_submit(array $form, array &$form_state) {
$index = $form_state['index'];
$options = isset($index->options) ? $index->options : array();
$index_path = 'admin/config/search/search_api/index/' . $index->machine_name;
if ($form_state['values']['op'] == t('Save changes')) {
$fields = $form_state['values']['fields'];
$default_types = search_api_default_field_types();
@@ -2176,18 +2181,18 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) {
$ret = $index->update(array('options' => $options));
if ($ret) {
drupal_set_message(t('The indexed fields were successfully changed. ' .
'The index was cleared and will have to be re-indexed with the new settings.'));
$vars = array('@url' => $index_path);
drupal_set_message(t('The indexed fields were successfully changed. The index was cleared and will have to be <a href="@url">re-indexed</a> with the new settings.', $vars));
}
else {
drupal_set_message(t('No values were changed.'));
}
if (isset($index->options['data_alter_callbacks']) || isset($index->options['processors'])) {
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
$form_state['redirect'] = $index_path . '/fields';
}
else {
drupal_set_message(t('Please set up the indexing workflow.'));
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
$form_state['redirect'] = $index_path . '/workflow';
}
return;
}
@@ -2202,7 +2207,7 @@ function search_api_admin_index_fields_submit(array $form, array &$form_state) {
else {
drupal_set_message(t('No values were changed.'));
}
$form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
$form_state['redirect'] = $index_path . '/fields';
}
/**
@@ -584,15 +584,15 @@ function hook_default_search_api_index_alter(array &$defaults) {
* This function will be called for fields of the specific data type to convert
* all individual values of the field to the correct format.
*
* @param $value
* @param mixed $value
* The raw, single value, as extracted from an entity wrapper.
* @param $original_type
* @param string $original_type
* The original Entity API type of the value.
* @param $type
* @param string $type
* The custom data type to which the value should be converted. Can be ignored
* if the callback is only used for a single data type.
*
* @return
* @return mixed|null
* The converted value, if a conversion could be executed. NULL otherwise.
*
* @see hook_search_api_data_type_info()
@@ -71,9 +71,10 @@ function search_api_drush_command() {
'drush sapi-i default_node_index' => dt('Index items for the index with the machine name !name.', array('!name' => 'default_node_index')),
'drush sapi-i 1 100' => dt("Index a maximum number of !limit items (index's cron batch size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!id' => 1)),
'drush sapi-i 1 100 10' => dt("Index a maximum number of !limit items (!batch_size items per batch run) for the index with the ID !id.", array('!limit' => 100, '!batch_size' => 10, '!id' => 1)),
'drush sapi-i 0 0 100' => dt("Index all items of all indexes with !batch_size items per batch run.", array('!batch_size' => 100)),
),
'arguments' => array(
'index_id' => dt('The numeric ID or machine name of an index.'),
'index_id' => dt('The numeric ID or machine name of an index. Set to 0 to index all indexes. Defaults to 0 (index all).'),
'limit' => dt("The number of items to index (index's cron batch size items per run). Set to 0 to index all items. Defaults to 0 (index all)."),
'batch_size' => dt("The number of items to index per batch run. Set to 0 to index all items at once. Defaults to the index's cron batch size."),
),
@@ -326,6 +327,7 @@ function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = N
if (search_api_drush_static(__FUNCTION__)) {
return;
}
$index_id = !empty($index_id) ? $index_id : NULL;
$indexes = search_api_drush_get_index($index_id);
if (empty($indexes)) {
return;
@@ -36,9 +36,9 @@ files[] = includes/service.inc
configure = admin/config/search/search_api
; Information added by Drupal.org packaging script on 2015-08-30
version = "7.x-1.16"
; Information added by Drupal.org packaging script on 2016-02-26
version = "7.x-1.16+29-dev"
core = "7.x"
project = "search_api"
datestamp = "1440962813"
datestamp = "1456500713"
@@ -1035,6 +1035,13 @@ function search_api_update_7117() {
* Adds the {search_api_item_string_id} table for items with string IDs.
*/
function search_api_update_7118() {
// Some users have reported that the table already existed for them, for
// whatever reason. Therefore, just bail if the table already exists, assuming
// it already looks as expected.
if (db_table_exists('search_api_item_string_id')) {
return;
}
$table = array(
'description' => 'Stores the items which should be indexed for each index, and their status. Used only for items with string IDs.',
'fields' => array(
@@ -2,7 +2,7 @@
/**
* @file
* Provides a flexible framework for implementing search servives.
* Provides a flexible framework for implementing search services.
*/
/**
@@ -883,7 +883,7 @@ function search_api_entity_update($entity, $type) {
list($id, , $new_bundle) = entity_extract_ids($type, $entity);
// Check if the entity's bundle changed.
if (isset($entity->original)) {
if (!empty($entity->original)) {
list(, , $old_bundle) = entity_extract_ids($type, $entity->original);
if ($new_bundle != $old_bundle) {
_search_api_entity_datasource_bundle_change($type, $id, $old_bundle, $new_bundle);
@@ -2100,8 +2100,8 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
* @param array $allowed
* Optionally, an array of allowed types.
*
* @return
* TRUE if $type is either one of the specified types, or a list of such
* @return bool
* TRUE if $type is either one of the specified types or a list of such
* values. FALSE otherwise.
*
* @see search_api_extract_inner_type()
@@ -2117,7 +2117,7 @@ function search_api_is_text_type($type, array $allowed = array('text')) {
* @param $type
* A string containing the type to check.
*
* @return
* @return bool
* TRUE iff $type is a list type ("list<*>").
*/
function search_api_is_list_type($type) {
@@ -2130,7 +2130,7 @@ function search_api_is_list_type($type) {
* @param $type
* A string containing the type to check.
*
* @return
* @return int
* The nesting level of the type. 0 for singular types, 1 for lists of
* singular types, etc.
*/
@@ -2157,7 +2157,7 @@ function search_api_list_nesting_level($type) {
* @param $nested_type
* Another type, determining the nesting level.
*
* @return
* @return string
* A list version of $type, as specified above.
*/
function search_api_nest_type($type, $nested_type) {
@@ -2174,7 +2174,7 @@ function search_api_nest_type($type, $nested_type) {
* @param $type
* A string containing the list type to process.
*
* @return
* @return string
* A string containing the primitive type contained within the list, e.g.
* "text" for "list<text>" (or for "list<list<text>>"). If $type is no list
* type, it is returned unchanged.
@@ -2196,7 +2196,7 @@ function search_api_extract_inner_type($type) {
*
* Modules implementing other datasource controllers, that use a table other
* than {search_api_item}, can use this function, too. It should be called
* uncoditionally in a hook_search_api_index_update() implementation. If this
* unconditionally in a hook_search_api_index_update() implementation. If this
* function isn't used, similar code should be added there.
*
* However, note that this is only necessary (and this function should only be
@@ -2453,7 +2453,7 @@ function search_api_access_disable_page(Entity $entity) {
* @param Entity $entity
* The server or index for which the access to the delete page is checked.
*
* @return
* @return bool
* TRUE if the delete page can be accessed by the user, FALSE otherwise.
*/
function search_api_access_delete_page(Entity $entity) {
@@ -2568,7 +2568,7 @@ function search_api_server_clear($id) {
* @param $id
* The ID or machine name of the server to delete.
*
* @return
* @return int|false
* 1 on success, 0 or FALSE on failure.
*/
function search_api_server_delete($id) {
@@ -2779,7 +2779,7 @@ function search_api_index_disable($id) {
* @param $id
* The ID or machine name of the index to re-index.
*
* @return
* @return bool
* TRUE on success, FALSE on failure.
*/
function search_api_index_reindex($id) {
@@ -2803,7 +2803,7 @@ function _search_api_index_reindex(SearchApiIndex $index) {
* @param $id
* The ID or machine name of the index to clear.
*
* @return
* @return bool
* TRUE on success, FALSE on failure.
*/
function search_api_index_clear($id) {
@@ -2817,7 +2817,7 @@ function search_api_index_clear($id) {
* @param $id
* The ID or machine name of the index to delete.
*
* @return
* @return bool
* TRUE on success, FALSE on failure.
*/
function search_api_index_delete($id) {
@@ -3174,7 +3174,7 @@ function _search_api_batch_indexing_callback(SearchApiIndex $index, $batch_size,
* @param boolean $success
* Whether the batch finished successfully.
* @param array $results
* Detailed informations about the result.
* Detailed information about the result.
*/
function _search_api_batch_indexing_finished($success, $results) {
// Check if called from drush.
@@ -10,9 +10,9 @@ files[] = search_api_test.module
hidden = TRUE
; Information added by Drupal.org packaging script on 2015-08-30
version = "7.x-1.16"
; Information added by Drupal.org packaging script on 2016-02-26
version = "7.x-1.16+29-dev"
core = "7.x"
project = "search_api"
datestamp = "1440962813"
datestamp = "1456500713"
@@ -1,29 +1,11 @@
Search API Saved Searches 1.5 (2015-06-08):
-------------------------------------------
- #2346679 by drunken monkey, dcmul: Added display of saved search
enabled/disabled status to UI.
- #2346677 by drunken monkey, dcmul: Fixed firing of saved searches for
disabled users.
- #2387155 by Dimiona, drunken monkey: Fixed default value for custom
notification interval.
- #2359003 by drunken monkey: Fixed removal of cron queue items of deleted
searches.
- #2347243 by queenvictoria, drunken monkey: Added tokens from the first search
to the mail texts.
- #1829678 by aschmoe: Fixed render order of "Save search" block in panels.
- #2354863 by drunken monkey: Fixed alert intervals not being respected.
- #2300175 by drunken monkey: Added database indexes to improve performance.
- #2221683 by drunken monkey | rjacobs: Fixed duplicate mails due to overlaps
conditions in cron queue.
Search API Saved Searches 1.3 (2014-05-24):
Search API Saved Searches 1.3 (05/24/2014):
-------------------------------------------
- #2082325 by balajidharma, drunken monkey: Added classes to edit and delete
links.
- #2088045 by leeomara, drunken monkey: Added hook to override generated names.
- #2042299 by drunken monkey: Added access callbacks for both entity types.
Search API Saved Searches 1.2 (2013-07-21):
Search API Saved Searches 1.2 (07/21/2013):
-------------------------------------------
- #2040469 by drunken monkey: Fixed Views display of saved search results.
- #2027441 by drunken monkey: Added option to filter by date instead of IDs.
@@ -32,7 +14,7 @@ Search API Saved Searches 1.2 (2013-07-21):
- #2018983 by drunken monkey: Fixed error after deleting last saved search.
- #2012714 by drunken monkey: Fixed the $reset parameter for load functions.
Search API Saved Searches 1.1 (2013-03-27):
Search API Saved Searches 1.1 (03/27/2013):
-------------------------------------------
- #1371344 by leeomara, drunken monkey: Added a README.txt file.
- #1888140 by drunken monkey: Added extended Views integration.
@@ -54,6 +36,6 @@ Search API Saved Searches 1.1 (2013-03-27):
- #1398310 by drunken monkey: Fixed visibility of "custom time" field.
- #1857822 by sepgil: Added quantity tokens.
Search API Saved Searches 1.0 (2012-10-04):
Search API Saved Searches 1.0 (10/04/2012):
-------------------------------------------
First stable release of the Search API Saved Searches project.
@@ -280,7 +280,7 @@ You can configure your saved searches at the following address:
$form['options']['interval']['set_interval_custom'] = array(
'#type' => 'textfield',
'#description' => t('Enter the custom notification interval, in seconds. Use a negative value to disable notifications.'),
'#default_value' => $options['set_interval'],
'#default_value' => isset($interval_options[$options['set_interval']]) ? $options['set_interval'] : 0,
'#parents' => array('options', 'set_interval_custom'),
'#states' => array(
'visible' => array(
@@ -343,8 +343,7 @@ You can configure your saved searches at the following address:
'#type' => 'textfield',
'#title' => t('Subject'),
'#description' => t("Enter the mail's subject.") . ' ' . t('Available variables are: @vars.',
array('@vars' => '[site:name], [user:name], [user:mail], [search-api-saved-search:name], [search-api-saved-search:created]')) . '<br />' .
t('The search-specific variables are taken from the first saved search of the mail. (Alerts from multiple searches are sent at the same time.)'),
array('@vars' => '[site:name], [user:name], [user:mail]')),
'#default_value' => $options['mail']['notify']['title'],
'#required' => TRUE,
);
@@ -353,12 +352,8 @@ You can configure your saved searches at the following address:
'#title' => t('Body'),
'#description' => t("Enter the mail's body.") . ' ' . t('Available variables are: @vars.',
array('@vars' => '[site:name], [site:url], [user:name], [user:mail], [site:url-brief], ' .
'[search-api-saved-searches:results], [user:search-api-saved-searches-url], ' .
'[search-api-saved-search:name], [search-api-saved-search:created], ' .
'[search-api-saved-search:results-capped], [search-api-saved-search:view-url], ' .
'[search-api-saved-search:edit-url], [search-api-saved-search:delete-url]')) . ' ' .
'[search-api-saved-searches:results], [user:search-api-saved-searches-url]')) . ' ' .
t('The replacement for @var can be configured below.', array('@var' => '[search-api-saved-searches:results]')) . '<br />' .
t('The search-specific variables are taken from the first saved search of the mail. (Alerts from multiple searches are sent at the same time.)') . '<br />' .
t('Please note: For anonymous users, the [user:*] variables will be empty.'),
'#default_value' => $options['mail']['notify']['body'],
'#rows' => 12,
@@ -10,9 +10,9 @@ files[] = views/handler_field_saved_search_interval.inc
files[] = views/handler_field_saved_search_link.inc
files[] = views/handler_field_saved_search_name.inc
; Information added by Drupal.org packaging script on 2015-06-08
version = "7.x-1.5"
; Information added by Drupal.org packaging script on 2014-05-24
version = "7.x-1.3"
core = "7.x"
project = "search_api_saved_searches"
datestamp = "1433745784"
datestamp = "1400922530"
@@ -112,11 +112,6 @@ function search_api_saved_searches_schema() {
'type' => 'int',
'not null' => TRUE,
),
'last_queued' => array(
'description' => 'The Unix timestamp when the saved search was last queued.',
'type' => 'int',
'not null' => TRUE,
),
'last_execute' => array(
'description' => 'The Unix timestamp when the saved search was last executed.',
'type' => 'int',
@@ -146,8 +141,8 @@ function search_api_saved_searches_schema() {
),
),
'indexes' => array(
'user' => array('mail', 'uid'),
'notify' => array('enabled', 'notify_interval', 'last_queued', 'last_execute'),
'uid' => array('uid'),
'mail' => array('mail'),
),
'primary key' => array('id'),
);
@@ -177,68 +172,3 @@ function search_api_saved_searches_update_7101() {
->execute();
}
}
/**
* Add {search_api_saved_search}.last_queued for more accurate queue tracking.
*/
function search_api_saved_searches_update_7102() {
$spec = array(
'description' => 'The Unix timestamp when the saved search was last queued.',
'type' => 'int',
'not null' => TRUE,
);
db_change_field('search_api_saved_search', 'last_execute', 'last_queued', $spec);
$spec['description'] = 'The Unix timestamp when the saved search was last executed.';
$spec['not null'] = FALSE;
db_add_field('search_api_saved_search', 'last_execute', $spec);
db_update('search_api_saved_search')
->expression('last_execute', 'last_queued')
->execute();
$spec['not null'] = TRUE;
db_change_field('search_api_saved_search', 'last_execute', 'last_execute', $spec);
}
/**
* Optimize indexes on {search_api_saved_search} to improve performance.
*/
function search_api_saved_searches_update_7103() {
db_drop_index('search_api_saved_search', 'uid');
db_drop_index('search_api_saved_search', 'mail');
db_add_index('search_api_saved_search', 'user', array('mail', 'uid'));
db_add_index('search_api_saved_search', 'notify', array('enabled', 'notify_interval', 'last_queued', 'last_execute'));
}
/**
* Fix incorrect {search_api_saved_search}.last_queued values.
*
* This might lead to duplicate mails for saved searches in some rare cases, but
* is necessary to prevent saved searches from never being executed at all
* anymore in others.
*
* @see https://www.drupal.org/node/2354863
*/
function search_api_saved_searches_update_7104() {
db_update('search_api_saved_search')
->expression('last_queued', 'last_execute')
->where('last_queued > last_execute')
->execute();
}
/**
* Disable saved searches whose users were disabled.
*/
function search_api_saved_searches_update_7105() {
// Get the UIDs of all inactive users.
$query = db_select('users', 'u');
$query->fields('u', array('uid'))
->condition('u.status', 0)
->condition('u.uid', 0, '<>');
// Then, disable all their searches.
db_update('search_api_saved_search')
->condition('uid', $query, 'IN')
->fields(array(
'enabled' => 0,
))
->execute();
}
@@ -66,24 +66,6 @@ function search_api_saved_searches_menu() {
'access arguments' => array(NULL, 2, 4),
'file' => 'search_api_saved_searches.pages.inc',
);
$items['search-api/saved-search/%search_api_saved_search/enable'] = array(
'title' => 'Enable/Disable saved search',
'description' => 'Enable or disable a saved search.',
'page callback' => 'search_api_saved_searches_search_enable',
'page arguments' => array(2),
'access callback' => 'search_api_saved_search_edit_access',
'access arguments' => array(NULL, 2, 4),
'file' => 'search_api_saved_searches.pages.inc',
);
$items['search-api/saved-search/%search_api_saved_search/disable'] = array(
'title' => 'Enable/Disable saved search',
'description' => 'Enable or disable a saved search.',
'page callback' => 'drupal_get_form',
'page arguments' => array('search_api_saved_searches_search_disable_form', 2),
'access callback' => 'search_api_saved_search_edit_access',
'access arguments' => array(NULL, 2, 4),
'file' => 'search_api_saved_searches.pages.inc',
);
$items['search-api/saved-search/%search_api_saved_search/edit'] = array(
'title' => 'Edit saved search',
'description' => 'Edit a saved search.',
@@ -178,7 +160,6 @@ function search_api_saved_searches_entity_property_info_alter(array &$info) {
$searches['settings_id']['type'] = 'token';
$searches['enabled']['type'] = 'boolean';
$searches['created']['type'] = 'date';
$searches['last_queued']['type'] = 'date';
$searches['last_execute']['type'] = 'date';
// We can't assign "duration" until Entity API Views integration supports
// this.
@@ -309,14 +290,11 @@ function search_api_saved_searches_user_insert(&$edit, $account, $category) {
* If a user gets activated, associate saved searches with the same mail address
* with them.
*
* If a user gets deactivated, disable all related saved searches.
*
* Also, change mail address of saved searches when the user mail address
* changes.
*/
function search_api_saved_searches_user_update(&$edit, $account, $category) {
// For newly activated users, transfer all saved searches with their mail
// address to them.
// Changes of mail and status.
if (!empty($account->status) && empty($account->original->status)) {
foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => 0)) as $search) {
$search->uid = $account->uid;
@@ -326,15 +304,6 @@ function search_api_saved_searches_user_update(&$edit, $account, $category) {
$search->save();
}
}
// If an account gets deactivated/banned, disable all associated searches.
if (empty($account->status) && !empty($account->original->status)) {
foreach (search_api_saved_search_load_multiple(FALSE, array('uid' => $account->uid)) as $search) {
$search->enabled = FALSE;
$search->save();
}
}
// If the user's mail address changed, also change the mail address of the
// user's saved searches.
if ($account->mail != $account->original->mail) {
foreach (search_api_saved_search_load_multiple(FALSE, array('mail' => $account->mail, 'uid' => $account->uid)) as $search) {
$search->mail = $account->mail;
@@ -481,8 +450,9 @@ function search_api_saved_searches_settings_load($id, $reset = FALSE) {
* @param bool $reset
* If TRUE, will reset the internal entity cache.
*
* @return SearchApiSavedSearchesSettings[]
* All saved search settings matching the conditions, keyed by delta.
* @return array
* An array of SearchApiSavedSearchesSettings objects matching the conditions,
* keyed by delta.
*/
function search_api_saved_searches_settings_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
$settings = entity_load('search_api_saved_searches_settings', $ids, $conditions, $reset);
@@ -508,16 +478,16 @@ function search_api_saved_search_load($id, $reset = FALSE) {
/**
* Loads multiple saved search objects.
*
* @param int[]|false $ids
* @param $ids
* The saved search's IDs; or FALSE to load all saved searches.
* @param array $conditions
* Associative array of field => value conditions that returned objects must
* satisfy.
* @param bool $reset
* @param $reset
* If TRUE, will reset the internal entity cache.
*
* @return SearchApiSavedSearch[]
* All saved searches matching the conditions, keyed by their IDs.
* @return array
* An array of SearchApiSavedSearch objects matching the conditions.
*/
function search_api_saved_search_load_multiple($ids = FALSE, array $conditions = array(), $reset = FALSE) {
return entity_load('search_api_saved_search', $ids, $conditions, $reset);
@@ -654,15 +624,6 @@ function search_api_saved_searches_block_info() {
return $blocks;
}
/**
* Implements hook_ctools_block_info().
*/
function search_api_saved_searches_ctools_block_info($module, $delta, &$info) {
$info['category'] = t('Search API Saved Searches');
// Allow blocks to be used before the search results in Panels.
$info['render last'] = TRUE;
}
/**
* Implements hook_block_configure().
*/
@@ -1009,7 +970,6 @@ function search_api_saved_searches_save_form_submit(array $form, array &$form_st
'name' => $values['name'],
'mail' => $values['mail'],
'created' => REQUEST_TIME,
'last_queued' => REQUEST_TIME,
'last_execute' => REQUEST_TIME,
'notify_interval' => $values['notify_interval'],
'query' => $query,
@@ -1097,14 +1057,9 @@ function search_api_saved_searches_mail($key, array &$message, array $params) {
case 'notify':
$settings = $params['settings'];
$search = $params['searches'][0]['search'];
$data = array(
'user' => $params['user'],
'search_api_saved_searches' => $params['searches'],
'search_api_saved_search_info' => array(
'search' => $search,
'results' => array(),
),
);
$title = $settings->getTranslatedOption('mail.notify.title', $language->language);
$message['subject'] .= token_replace($title, $data, array('language' => $language, 'sanitize' => FALSE));
@@ -1126,9 +1081,7 @@ function search_api_saved_searches_cron() {
$ids = db_select('search_api_saved_search', 's')
->fields('s', array('id'))
->condition('enabled', 1)
->condition('notify_interval', 0, '>=')
->where('last_execute >= last_queued')
->where('last_queued + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
->where('notify_interval >= 0 AND last_execute + notify_interval < :time', array(':time' => REQUEST_TIME + 15))
->execute()
->fetchCol();
if (!$ids) {
@@ -1140,18 +1093,21 @@ function search_api_saved_searches_cron() {
// Group the search according to mail and settings. Grouping by mail prevents
// a user from getting several mails at once, for different searches. Grouping
// by settings is necessary since the mails can differ between settings.
$user_searches = array();
foreach ($searches as $search) {
$user_searches[$search->mail . ' ' . $search->settings_id][] = $search->id;
// Set the last execution timestamp now, so the interval doesn't move and we
// don't get problems if the next cron run occurs before the queue is
// completely executed.
$search->last_queued = REQUEST_TIME;
$search->save();
$user_searches[$search->mail . ' ' . $search->settings_id][] = $search;
}
foreach ($user_searches as $searches) {
$queue->createItem($searches);
}
// Set the last execution timestamp now, so the interval doesn't move and we
// don't get problems if the next cron run occurrs before the queue is
// completely executed.
db_update('search_api_saved_search')
->fields(array(
'last_execute' => REQUEST_TIME,
))
->condition('id', $ids, 'IN')
->execute();
}
/**
@@ -1171,44 +1127,17 @@ function search_api_saved_searches_cron_queue_info() {
/**
* Checks for new results for saved searches, and sends a mail if necessary.
*
* Used as a worker callback for the homonymous cron queue.
*
* @param int[] $search_ids
* The IDs of the saved searches to check for new results. All of these should
* have the same mail address and base settings.
*
* @throws SearchApiException
* If an error occurred in one of the searches.
*
* @see search_api_saved_searches_cron_queue_info()
* @param array $searches
* An array of SearchApiSavedSearch objects to check for new results. All of
* these should have the same mail address and base settings.
*/
function search_api_saved_searches_check_updates(array $search_ids) {
if (!$search_ids) {
return;
}
// Since in earlier versions this function got the loaded searches passed
// directly instead of just IDs, and there might still be some such items in
// the queue when updating to the new style, we have to stay
// backwards-compatible here. So, when an array of loaded searches is passed,
// we first replace them with their IDs and only then load them again.
if (!is_scalar(reset($search_ids))) {
/** @var SearchApiSavedSearch[] $searches */
$searches = $search_ids;
$search_ids = array();
function search_api_saved_searches_check_updates(array $searches) {
try {
$search = reset($searches);
$settings = $search->settings();
$index = $settings->index();
$mail_params = array();
foreach ($searches as $search) {
$search_ids[] = $search->id;
}
}
$searches = search_api_saved_search_load_multiple($search_ids, array('enabled' => 1));
if (!$searches) {
return;
}
$search = $searches[key($searches)];
$settings = $search->settings();
$index = $settings->index();
$mail_params = array();
foreach ($searches as $search) {
try {
// Make sure we run the query as the user who owns the saved search.
// Otherwise node access will not work properly.
$search->query['options']['search_api_access_account'] = $search->uid;
@@ -1221,7 +1150,6 @@ function search_api_saved_searches_check_updates(array $search_ids) {
}
$response = $query->execute();
if (!empty($response['results'])) {
$old = array();
$new = $results = drupal_map_assoc(array_keys($response['results']));
if (empty($settings->options['date_field'])) {
// ID-based method: Compare these results to the old ones.
@@ -1261,28 +1189,23 @@ function search_api_saved_searches_check_updates(array $search_ids) {
if (empty($settings->options['date_field']) && ($new || array_diff($old, $results))) {
// The results changed in some way: store the latest version.
$search->results = implode(',', $results);
$search->save();
}
}
// Use time() instead of REQUEST_TIME to minimize the potential of sending
// duplicate results due to longer-running cron queue workers.
$search->last_execute = time();
$search->save();
}
catch (SearchApiException $e) {
$args = _drupal_decode_exception($e);
$args['@id'] = $search->id;
throw new SearchApiException(t('%type while trying to check for new results on saved search @id: !message in %function (line %line of %file).', $args));
// If we set any searches in the mail parameters, send the mail.
if ($mail_params) {
$mail_params['user'] = user_load($search->uid);
$mail_params['settings'] = $settings;
$message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
user_preferred_language($mail_params['user']), $mail_params);
if ($message['result']) {
watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
array('@mail' => $search->mail), WATCHDOG_INFO);
}
}
}
// If we set any searches in the mail parameters, send the mail.
if ($mail_params) {
$mail_params['user'] = user_load($search->uid);
$mail_params['settings'] = $settings;
$message = drupal_mail('search_api_saved_searches', 'notify', $search->mail,
user_preferred_language($mail_params['user']), $mail_params);
if ($message['result']) {
watchdog('search_api_saved_searches', 'A mail with new saved search results was sent to @mail.',
array('@mail' => $search->mail), WATCHDOG_INFO);
}
catch (SearchApiException $e) {
watchdog('search_api_saved_searches', $e->getMessage(), NULL, WATCHDOG_ERROR);
}
}
@@ -45,30 +45,16 @@ function search_api_saved_searches_user_listing($account) {
$interval = format_interval($search->notify_interval, 1);
}
$enable_options['attributes']['class'][] = 'saved-search-enable';
$edit_options['attributes']['class'][] = 'saved-search-edit';
$delete_options['attributes']['class'][] = 'saved-search-delete';
if ($search->enabled) {
$enable_action = '/disable';
$enable_label = t('disable');
}
else {
$enable_action = '/enable';
$enable_label = t('enable');
$enable_options['query']['token'] = drupal_get_token($search->id);
}
$path = $base_path . $search->id;
$enable_link = l($enable_label, $path . $enable_action, $enable_options);
$edit_link = l(t('edit'), $path . '/edit', $edit_options);
$delete_link = l(t('delete'), $path . '/delete', $delete_options);
$rows[] = array(
$name,
$created,
$last_execute,
$interval,
$enable_link . ' | ' . $edit_link . ' | ' . $delete_link,
l(t('edit'), $path . '/edit', $edit_options) . ' | ' . l(t('delete'), $path . '/delete', $delete_options),
);
}
@@ -184,12 +170,6 @@ function search_api_saved_searches_search_edit_form(array $form, array &$form_st
$form_state['destination'] = array($search->options['page']['path'], $search->options['page']);
}
$form['enabled'] = array(
'#type' => 'checkbox',
'#title' => t('Enabled'),
'#description' => t('Disable to stop receiving notifications from this saved search.'),
'#default_value' => $search->enabled,
);
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
@@ -233,7 +213,6 @@ function search_api_saved_searches_search_edit_form_submit(array $form, array &$
$values = $form_state['values'];
$search = $form_state['search'];
$search->name = $values['name'];
$search->enabled = $values['enabled'];
if (isset($values['notify_interval'])) {
$search->notify_interval = $values['notify_interval'];
}
@@ -248,56 +227,6 @@ function search_api_saved_searches_search_edit_form_submit(array $form, array &$
}
}
/**
* Page callback: Enables a saved search.
*
* @param SearchApiSavedSearch $search
* The search to enable.
*/
function search_api_saved_searches_search_enable(SearchApiSavedSearch $search = NULL) {
if (!isset($_GET['token']) || !drupal_valid_token($_GET['token'], $search->id)) {
return MENU_ACCESS_DENIED;
}
$search->enabled = TRUE;
if ($search->save()) {
drupal_set_message(t('The saved search was successfully enabled.'));
}
else {
drupal_set_message(t('The saved search could not be enabled.'), 'error');
}
drupal_goto(_search_api_saved_searches_admin_redirect_url($search));
return NULL;
}
/**
* Page callback: Constructs a form for disabling a saved search.
*
* @param SearchApiSavedSearch $search
* The search to disable.
*
* @see search_api_saved_searches_search_disable_form_submit()
*
* @ingroup forms
*/
function search_api_saved_searches_search_disable_form(array $form, array &$form_state, SearchApiSavedSearch $search) {
$form_state['search'] = $search;
$url = _search_api_saved_searches_admin_redirect_url($search);
return confirm_form($form, t('Do you really want to disable this saved search?'), $url, t('You will not receive any updates for this saved search until it is re-enabled.'));
}
/**
* Form submission handler for search_api_saved_searches_search_disable_form().
*/
function search_api_saved_searches_search_disable_form_submit(array $form, array &$form_state) {
$search = $form_state['search'];
$search->enabled = FALSE;
$search->save();
drupal_set_message(t('The saved search was successfully disabled.'));
$form_state['redirect'] = _search_api_saved_searches_admin_redirect_url($search);
}
/**
* Form builder for confirming the deletion of a saved search.
*
@@ -309,7 +238,15 @@ function search_api_saved_searches_search_disable_form_submit(array $form, array
*/
function search_api_saved_searches_search_delete_form(array $form, array &$form_state, SearchApiSavedSearch $search) {
$form_state['search'] = $search;
$url = _search_api_saved_searches_admin_redirect_url($search);
if ($search->uid && search_api_saved_search_edit_access(user_load($search->uid))) {
$url = 'user/' . $search->uid . '/saved-searches';
}
elseif (!empty($search->options['page'])) {
$url = $search->options['page'];
}
else {
$url = '<front>';
}
return confirm_form($form, t('Do you really want to delete this saved search?'), $url);
}
@@ -321,29 +258,15 @@ function search_api_saved_searches_search_delete_form(array $form, array &$form_
function search_api_saved_searches_search_delete_form_submit(array $form, array &$form_state) {
$search = $form_state['search'];
$search->delete();
drupal_set_message(t('The saved search was successfully deleted.'));
$form_state['redirect'] = _search_api_saved_searches_admin_redirect_url($search);
}
/**
* Returns the correct redirect URL after changing a saved search.
*
* This will be the user's "Saved searches" overview tab, if it is accessible;
* otherwise, if the search has a page associated with it, that page; and if
* none of the two are the case, the front page.
*
* @param SearchApiSavedSearch $search
* The saved search that was edited, deleted or otherwise changed.
*
* @return string
* The URL to redirect to.
*/
function _search_api_saved_searches_admin_redirect_url(SearchApiSavedSearch $search) {
if ($search->uid && search_api_saved_search_edit_access(user_load($search->uid))) {
return 'user/' . $search->uid . '/saved-searches';
$url = 'user/' . $search->uid . '/saved-searches';
}
elseif (!empty($search->options['page'])) {
return $search->options['page'];
$url = $search->options['page'];
}
return '<front>';
else {
$url = '<front>';
}
drupal_set_message(t('The saved search was successfully deleted.'));
$form_state['redirect'] = $url;
}
@@ -68,11 +68,6 @@ class SearchApiSavedSearch extends Entity {
*/
public $created;
/**
* @var integer
*/
public $last_queued;
/**
* @var integer
*/
@@ -150,7 +145,7 @@ class SearchApiSavedSearch extends Entity {
}
/**
* @return SearchApiSavedSearchesSettings
* @return SearchApiIndex
* The settings this saved search uses.
*
* @throws SearchApiException
@@ -8,9 +8,9 @@ package = Multilingual - Internationalization
files[] = search_api_saved_searches_i18n.controller.inc
files[] = search_api_saved_searches_i18n.string_wrapper.inc
; Information added by Drupal.org packaging script on 2015-06-08
version = "7.x-1.5"
; Information added by Drupal.org packaging script on 2014-05-24
version = "7.x-1.3"
core = "7.x"
project = "search_api_saved_searches"
datestamp = "1433745784"
datestamp = "1400922530"
@@ -1,3 +1,19 @@
Search API Solr Search 1.x, dev (xxxx-xx-xx):
---------------------------------------------
- #2645366 by tedfordgif, drunken monkey: Fixed browser incorrectly filling the
HTTP Auth form fields.
- #2611716 by aditya_anurag, drunken monkey: Improved the method documentation
comments.
- #2599658 by Loparev, drunken monkey: Added the possibility to highlight
non-fulltext fields.
- #2598288 by ethan.han777, JeroenT, drunken monkey: Fixed potential notices in
the spellchecker code.
- #2564927 by thePanz: Added support for random sorting in grouped searches.
- #2527528 by maximpodorov, drunken monkey: Fixed searching of string fields
with leading/trailing spaces.
- #2513314 by fortis, drunken monkey: Fixed error for empty filters.
- #2551763 by drunken monkey: Fixed mention of INSTALL.txt in an error message.
Search API Solr Search 1.9 (2015-08-30):
----------------------------------------
- #2503829 by das-peter: Added support for grouping on "magic" fields.
@@ -137,6 +137,11 @@ Hidden variables
what you are doing. Old indexed items will be lost when the hash is changed
and all items will have to be reindexed. Can only contain alphanumeric
characters.
- search_api_solr_highlight_prefix (default: "tm_")
The prefix of Solr fields for which field-level highlighting will be enabled.
Since the prefix of fields is used to determine the field type (by default),
this lets you enable highlighting for other field types. By default,
highlighting will be possible for all fulltext fields.
[8] http://wiki.apache.org/solr/UpdateXmlMessages#A.22commit.22_and_.22optimize.22
@@ -57,7 +57,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
protected $request_handler = NULL;
/**
* Overrides SearchApiAbstractService::configurationForm().
* {@inheritdoc}
*/
public function configurationForm(array $form, array &$form_state) {
if ($this->options) {
@@ -176,6 +176,10 @@ class SearchApiSolrService extends SearchApiAbstractService {
'#type' => 'textfield',
'#title' => t('Username'),
'#default_value' => $options['http_user'],
// This prefix with no-op text and password field will keep most browsers
// from autocompleting these fields, which is hardly ever what the user
// wants.
'#prefix' => '<input type="text" style="display:none" /><input type="password" style="display:none" />',
);
$form['http']['http_pass'] = array(
'#type' => 'password',
@@ -271,7 +275,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Overrides SearchApiAbstractService::configurationFormValidate().
* {@inheritdoc}
*/
public function configurationFormValidate(array $form, array &$values, array &$form_state) {
if (isset($values['port']) && (!is_numeric($values['port']) || $values['port'] < 0 || $values['port'] > 65535)) {
@@ -280,7 +284,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Overrides SearchApiAbstractService::configurationFormSubmit().
* {@inheritdoc}
*/
public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
// Since the form is nested into another, we can't simply use #parents for
@@ -429,8 +433,8 @@ class SearchApiSolrService extends SearchApiAbstractService {
$status = 'error';
}
elseif (substr($stats_summary['@schema_version'], 0, 9) != 'drupal-4.') {
$variables['@url'] = url(drupal_get_path('module', 'search_api_solr') . '/INSTALL.txt');
$message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in the <a href="@url">INSTALL.txt</a> file for setting up Solr.', $variables);
$variables['@url'] = url('https://www.drupal.org/node/1999310');
$message = t('You are using an incompatible schema.xml configuration file. Please follow the instructions in <a href="@url">the handbook</a> for setting up Solr.', $variables);
drupal_set_message($message, 'error');
$status = 'error';
}
@@ -495,7 +499,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Overrides SearchApiAbstractService::addIndex().
* {@inheritdoc}
*/
public function addIndex(SearchApiIndex $index) {
if (module_exists('search_api_multi') && module_exists('search_api_views')) {
@@ -504,7 +508,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Overrides SearchApiAbstractService::fieldsUpdated().
* {@inheritdoc}
*/
public function fieldsUpdated(SearchApiIndex $index) {
if (module_exists('search_api_multi') && module_exists('search_api_views')) {
@@ -525,7 +529,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Overrides SearchApiAbstractService::removeIndex().
* {@inheritdoc}
*/
public function removeIndex($index) {
if (module_exists('search_api_multi') && module_exists('search_api_views')) {
@@ -536,7 +540,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Implements SearchApiServiceInterface::indexItems().
* {@inheritdoc}
*/
public function indexItems(SearchApiIndex $index, array $items) {
$documents = array();
@@ -772,6 +776,17 @@ class SearchApiSolrService extends SearchApiAbstractService {
*
* It is up to the caller to ensure $ids is a valid query when the method is
* called in this fashion.
*
* @param array|string $ids
* Either an array containing the ids of the items that should be deleted,
* or 'all' if all items should be deleted. Other formats might be
* recognized by implementing classes, but these are not standardized.
* @param SearchApiIndex $index
* The index from which items should be deleted, or NULL if all indexes on
* this server should be cleared (then, $ids has to be 'all').
*
* @throws SearchApiException
* If an error occurred while trying to delete the items.
*/
public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
$this->connect();
@@ -804,7 +819,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
/**
* Implements SearchApiServiceInterface::search().
* {@inheritdoc}
*/
public function search(SearchApiQueryInterface $query) {
$time_method_called = microtime(TRUE);
@@ -1080,6 +1095,18 @@ class SearchApiSolrService extends SearchApiAbstractService {
if (substr($f, 0, 3) == 'ss_') {
$f = 'sort_' . substr($f, 3);
}
// The default Solr schema provides a virtual field named
// "random_SEED" that can be used to randomly sort the results;
// the field is available only at query-time.
if ($group_sort_field == 'search_api_random') {
$params = $query->getOption('search_api_random_sort', array());
// Random seed: getting the value from parameters or computing a
// new one.
$seed = !empty($params['seed']) ? $params['seed'] : mt_rand();
$f = 'random_' . $seed;
}
$order = strtolower($order);
$group_params['group.sort'][] = $f . ' ' . $order;
}
@@ -1336,8 +1363,10 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
}
if (!empty($this->options['highlight_data'])) {
$prefix = variable_get('search_api_solr_highlight_prefix', 'tm_');
$prefix_length = strlen($prefix);
foreach ($field_mapping as $search_api_property => $solr_property) {
if (substr($solr_property, 0, 3) == 'tm_' && !empty($response->highlighting->$id->$solr_property)) {
if (substr($solr_property, 0, $prefix_length) == $prefix && !empty($response->highlighting->$id->$solr_property)) {
// Contrary to above, we here want to preserve HTML, so we just
// replace the [HIGHLIGHT] tags with the appropriate format.
$fields[$search_api_property] = $this->formatHighlighting($response->highlighting->$id->$solr_property);
@@ -1557,10 +1586,10 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
elseif ($f instanceof SearchApiQueryFilterInterface) {
$q = $this->createFilterQueries($f, $solr_fields, $fields);
if ($filter->getConjunction() != $f->getConjunction()) {
if ($filter->getConjunction() != $f->getConjunction() && count($q) > 1) {
// $or == TRUE means the nested filter has conjunction AND, and vice versa
$sep = $or ? ' ' : ' OR ';
$fq[] = count($q) == 1 ? reset($q) : '((' . implode(')' . $sep . '(', $q) . '))';
$fq[] = '((' . implode(')' . $sep . '(', $q) . '))';
}
else {
$fq = array_merge($fq, $q);
@@ -1604,7 +1633,6 @@ class SearchApiSolrService extends SearchApiAbstractService {
if ($value === NULL) {
return ($operator == '=' ? '*:* AND -' : '') . "$field:[* TO *]";
}
$value = trim($value);
$value = $this->formatFilterValue($value, search_api_extract_inner_type($field_info['type']));
switch ($operator) {
case '<>':
@@ -1717,7 +1745,7 @@ class SearchApiSolrService extends SearchApiAbstractService {
}
if (!empty($this->options['highlight_data'])) {
$highlight_params['hl.fl'] = 'tm_*';
$highlight_params['hl.fl'] = variable_get('search_api_solr_highlight_prefix', 'tm_') . '*';
$highlight_params['hl.snippets'] = 1;
$highlight_params['hl.fragsize'] = 0;
if (!empty($this->options['excerpt'])) {
@@ -224,7 +224,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::ping().
* {@inheritdoc}
*/
public function ping($timeout = 2) {
$start = microtime(TRUE);
@@ -251,28 +251,28 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::setSoftCommit().
* {@inheritdoc}
*/
public function setSoftCommit($soft_commit) {
$this->soft_commit = (bool) $soft_commit;
}
/**
* Implements SearchApiSolrConnectionInterface::getSoftCommit().
* {@inheritdoc}
*/
public function getSoftCommit() {
return $this->soft_commit;
}
/**
* Implements SearchApiSolrConnectionInterface::setStreamContext().
* {@inheritdoc}
*/
public function setStreamContext($stream_context) {
$this->stream_context = $stream_context;
}
/**
* Implements SearchApiSolrConnectionInterface::getStreamContext().
* {@inheritdoc}
*/
public function getStreamContext() {
return $this->stream_context;
@@ -333,6 +333,9 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
/**
* Sets $this->luke with the metadata about the index from admin/luke.
*
* @param int $num_terms
* (optional) The number of "top terms" to return.
*/
protected function setLuke($num_terms = 0) {
if (empty($this->luke[$num_terms])) {
@@ -360,7 +363,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getFields().
* {@inheritdoc}
*/
public function getFields($num_terms = 0) {
$fields = array();
@@ -371,7 +374,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getLuke().
* {@inheritdoc}
*/
public function getLuke($num_terms = 0) {
if (!isset($this->luke[$num_terms])) {
@@ -381,7 +384,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getSolrVersion().
* {@inheritdoc}
*/
public function getSolrVersion() {
// Allow for overrides by the user.
@@ -430,7 +433,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getStats().
* {@inheritdoc}
*/
public function getStats() {
if (!isset($this->stats)) {
@@ -440,7 +443,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getStatsSummary().
* {@inheritdoc}
*/
public function getStatsSummary() {
$stats = $this->getStats();
@@ -506,7 +509,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::clearCache().
* {@inheritdoc}
*/
public function clearCache() {
if ($cid = $this->getCacheId()) {
@@ -545,7 +548,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::makeServletRequest().
* {@inheritdoc}
*/
public function makeServletRequest($servlet, array $params = array(), array $options = array()) {
// Add default params.
@@ -561,7 +564,19 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Central method for making a GET operation against this Solr Server
* Sends a GET request to the Solr server.
*
* @param string $url
* The URL to which the request should be sent.
* @param array $options
* Additional options for the request, as recognized by
* drupal_http_request().
*
* @return object
* The HTTP response, as returned by drupal_http_request().
*
* @throws SearchApiException
* If an error occurs, either during sending or on the server side.
*/
protected function sendRawGet($url, array $options = array()) {
$options['method'] = 'GET';
@@ -571,7 +586,19 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Central method for making a POST operation against this Solr Server
* Sends a PUT request to the Solr server.
*
* @param string $url
* The URL to which the request should be sent.
* @param array $options
* Additional options for the request, as recognized by
* drupal_http_request().
*
* @return object
* The HTTP response, as returned by drupal_http_request().
*
* @throws SearchApiException
* If an error occurs, either during sending or on the server side.
*/
protected function sendRawPost($url, array $options = array()) {
$options['method'] = 'POST';
@@ -588,6 +615,15 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
* Sends an HTTP request to Solr.
*
* This is just a wrapper around drupal_http_request().
*
* @param string $url
* The URL to which the request should be sent.
* @param array $options
* Additional options for the request, as recognized by
* drupal_http_request().
*
* @return object
* The HTTP response, as returned by drupal_http_request().
*/
protected function makeHttpRequest($url, array $options = array()) {
if (empty($options['method']) || $options['method'] == 'GET' || $options['method'] == 'HEAD') {
@@ -631,7 +667,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::escape().
* {@inheritdoc}
*/
public static function escape($value, $version = 0) {
$replacements = array();
@@ -651,7 +687,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::escapePhrase().
* {@inheritdoc}
*/
public static function escapePhrase($value) {
$replacements['"'] = '\"';
@@ -660,14 +696,14 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::phrase().
* {@inheritdoc}
*/
public static function phrase($value) {
return '"' . self::escapePhrase($value) . '"';
}
/**
* Implements SearchApiSolrConnectionInterface::escapeFieldName().
* {@inheritdoc}
*/
public static function escapeFieldName($value) {
$value = str_replace(':', '\:', $value);
@@ -685,6 +721,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
* Additional query string to append to the URL.
*
* @return string
* The complete URL.
*/
protected function constructUrl($servlet, array $params = array(), $added_query_string = NULL) {
// PHP's built in http_build_query() doesn't give us the format Solr wants.
@@ -704,14 +741,14 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::getBaseUrl().
* {@inheritdoc}
*/
public function getBaseUrl() {
return $this->base_url;
}
/**
* Implements SearchApiSolrConnectionInterface::setBaseUrl().
* {@inheritdoc}
*/
public function setBaseUrl($url) {
$this->base_url = $url;
@@ -719,7 +756,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::update().
* {@inheritdoc}
*/
public function update($rawPost, $timeout = 3600) {
if (empty($this->update_url)) {
@@ -735,7 +772,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::addDocuments().
* {@inheritdoc}
*/
public function addDocuments(array $documents, $overwrite = NULL, $commitWithin = NULL) {
$attr = '';
@@ -759,21 +796,21 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::commit().
* {@inheritdoc}
*/
public function commit($waitSearcher = TRUE, $timeout = 3600) {
return $this->optimizeOrCommit('commit', $waitSearcher, $timeout);
}
/**
* Implements SearchApiSolrConnectionInterface::deleteById().
* {@inheritdoc}
*/
public function deleteById($id, $timeout = 3600) {
return $this->deleteByMultipleIds(array($id), $timeout);
}
/**
* Implements SearchApiSolrConnectionInterface::deleteByMultipleIds().
* {@inheritdoc}
*/
public function deleteByMultipleIds(array $ids, $timeout = 3600) {
$rawPost = '<delete>';
@@ -787,7 +824,7 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::deleteByQuery().
* {@inheritdoc}
*/
public function deleteByQuery($rawQuery, $timeout = 3600) {
$rawPost = '<delete><query>' . htmlspecialchars($rawQuery, ENT_NOQUOTES, 'UTF-8') . '</query></delete>';
@@ -796,14 +833,14 @@ class SearchApiSolrConnection implements SearchApiSolrConnectionInterface {
}
/**
* Implements SearchApiSolrConnectionInterface::optimize().
* {@inheritdoc}
*/
public function optimize($waitSearcher = TRUE, $timeout = 3600) {
return $this->optimizeOrCommit('optimize', $waitSearcher, $timeout);
}
/**
* Sends an commit or optimize command to the Solr server.
* Sends a commit or optimize command to the Solr server.
*
* Will be synchronous unless $waitSearcher is set to FALSE.
*
@@ -228,7 +228,7 @@ interface SearchApiSolrConnectionInterface {
/**
* Sends a delete request for several documents, based on the document IDs.
*
* @param array $id
* @param array $ids
* The IDs of the documents which should be deleted. Expected to be UTF-8
* encoded.
* @param int|false $timeout
@@ -306,8 +306,8 @@ interface SearchApiSolrConnectionInterface {
*
* @param string $value
* The string to escape.
* @param string $version
* An integer representing major solr version release.
* @param int $version
* An integer representing the major solr version.
*
* @return string
* An escaped string suitable for passing to Solr.
@@ -23,8 +23,10 @@ class SearchApiSpellcheckSolr extends SearchApiSpellcheck {
if (isset($response->spellcheck->suggestions)) {
$suggestions = $response->spellcheck->suggestions;
foreach ($suggestions as $word => $data) {
foreach ($data->suggestion as $suggestion) {
$this->addSuggestion(new SearchApiSpellcheckSuggestion($word, $suggestion));
if (isset($data->suggestion)) {
foreach ($data->suggestion as $suggestion) {
$this->addSuggestion(new SearchApiSpellcheckSuggestion($word, $suggestion));
}
}
}
}
@@ -11,9 +11,9 @@ files[] = includes/solr_connection.interface.inc
files[] = includes/solr_field.inc
files[] = includes/spellcheck.inc
; Information added by Drupal.org packaging script on 2015-08-30
version = "7.x-1.9"
; Information added by Drupal.org packaging script on 2016-02-23
version = "7.x-1.9+10-dev"
core = "7.x"
project = "search_api_solr"
datestamp = "1440962648"
datestamp = "1456233244"
@@ -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.
@@ -0,0 +1,55 @@
Term Merge
------------------------
by:
* Max Nylin <max@articstudios.se>
* Oleksandr Trotsenko
Description
-----------
When using taxonomy for free tagging purposes, it's easy to end up with
several terms having the same meaning. This may be due to spelling errors,
or different users simply making up synonymous terms as they go.
You, as an administrator, may then want to correct such errors or unify
synonymous terms, thereby pruning the taxonomy to a more manageable set.
This module allows you to merge multiple terms into one, while updating
all fields referring to those terms to refer to the replacement term instead.
Currently, the module only acts on:
* fields of 'taxonomy term reference' type
* Views Taxonomy Term filter handlers
* Redirects
It would be desirable to update other possible
places where deleted terms are used.
Integration
-------------
Currently module integrates with the following core and contributed modules:
* Redirect module (http://drupal.org/project/redirect). During term merging
you may set up SEO friendly redirects from the branch terms to point to the
trunk term
* Synonyms module (http://drupal.org/project/synonyms). During term merging
you will be able to choose a trunk term's field into which all the branch terms
will be added as synonyms (until cardinality limit for that field is reached).
* Hierarchical Select (http://drupal.org/project/hierarchical_select). If
Hierarchical Select module is configured to be used for working with Taxonomy,
its widget will be shown on the form, where you choose what terms to merge into
what term.
* Views (http://drupal.org/project/views). If the branch terms are to be
deleted after the merging process, you could end up having some Views filters
to filter on no longer existing terms. Term Merge module, while merging terms,
will update those filters to filter not on the branch term, but on the trunk
term. This way you will not have senseless filters and will not have to update
them manually.
Requirements
-------------
The modules requires enabled the following modules:
* Taxonomy module (ships with Drupal core)
* Entity API (http://drupal.org/project/entity)
Installation
------------
* Copy the module's directory to your modules directory and activate the
module.
@@ -0,0 +1,66 @@
/**
* @file
* JavaScript enhancements for "Merge Duplicate Terms" form.
*/
(function ($) {
/**
* Behavior that elaborates Tableselect of duplicate terms.
*/
Drupal.behaviors.termMergeDuplicateTableselect = {
attach: function(context) {
// We want to disable checkbox of a duplicate term if it is currently
// selected as a trunk term in that group of duplicate terms, i.e. you
// cannot (and should not) merge a term into itself.
$('.term-merge-duplicate-trunk', context).closest(':has(.form-checkbox)').once('term-merge-duplicate-trunk', function() {
var container = $(this);
container.find('.term-merge-duplicate-trunk').change(function() {
// Removing disabled from all disabled checkboxes.
container.parents('table').find('.form-checkbox:disabled').removeAttr('disabled');
if ($(this).is(':checked')) {
container.find('.form-checkbox').attr('disabled', true);
}
});
});
}
};
/**
* Behavior that kicks off general switch button on Duplicate terms form.
*/
Drupal.behaviors.termMergeDuplicateGeneralSwitch = {
attach: function (context) {
$('.term-merge-duplicate-general-switch', context).once('term-merge-duplicate-general-switch', function() {
var container = $(this).parents('form');
$(this).change(function() {
var term_branches = container.find('table:not(.sticky-header) .select-all .form-checkbox');
if ($(this).is(':checked')) {
term_branches.attr('checked', true).trigger({
type: 'click',
target: this
});
// For some reason the checkboxes get unchecked, we check them back.
term_branches.attr('checked', true);
// We also want to trigger "change" on those radio buttons, see
// Drupal.behaviors.termMergeDuplicateTableselect for more info.
container.find('table').find('.term-merge-duplicate-trunk:first').attr('checked', true).trigger('change');
}
else {
term_branches.removeAttr('checked').trigger({
type: 'click',
target: this
});
// For some reason the checkboxes get checked, we uncheck them back.
term_branches.removeAttr('checked');
// We also want to trigger "change" on those radio buttons, see
// Drupal.behaviors.termMergeDuplicateTableselect for more info.
container.find('table').find('.term-merge-duplicate-trunk').removeAttr('checked').trigger('change');
}
});
});
}
};
})(jQuery);
@@ -0,0 +1,28 @@
<?php
/**
* @file
* Documentation for Term Merge module.
*/
/**
* Notify other modules when merging of 2 taxonomy terms occurs.
*
* @param object $term_trunk
* Fully loaded taxonomy term object of the term trunk, term into which
* merging occurs, aka 'destination'
* @param object $term_branch
* Fully loaded taxonomy term object of the term branch, term that is being
* merged, aka 'source'
* @param array $context
* Array $context as it is passed to term_merge_action() - you can get a
* little more info about context about merging from this array
*/
function hook_term_merge($term_trunk, $term_branch, $context) {
// Here we might want to run a query like:
// UPDATE {my_table}
// SET term_tid = $term_trunk->tid
// WHERE term_tid = $term_branch->tid
// Or do anything else that you own logic requires when merging of 2 terms
// happens.
}
@@ -0,0 +1,102 @@
<?php
/**
* @file
* Batch process callbacks for Term Merge module.
*/
/**
* Process batch function.
*
* Trigger action 'term_merge_action' on each pair of term branch:term trunk.
*
* @param array $term_branch
* An array of term tids to be merged, aka term branches
* @param int $term_trunk
* The tid of the term to merge term branches into, aka term trunk
* @param array $merge_settings
* Array of settings that control how merging should happen. Currently
* supported settings are:
* - term_branch_keep: (bool) Whether the term branches should not be
* deleted, also known as "merge only occurrences" option
* - merge_fields: (array) Array of field names whose values should be
* merged into the values of corresponding fields of term trunk (until
* each field's cardinality limit is reached)
* - keep_only_unique: (bool) Whether after merging within one field only
* unique taxonomy term references should be kept in other entities. If
* before merging your entity had 2 values in its taxonomy term reference
* field and one was pointing to term branch while another was pointing to
* term trunk, after merging you will end up having your entity
* referencing to the same term trunk twice. If you pass TRUE in this
* parameter, only a single reference will be stored in your entity after
* merging
* - redirect: (int) HTTP code for redirect from $term_branch to
* $term_trunk, 0 stands for the default redirect defined in Redirect
* module. Use constant TERM_MERGE_NO_REDIRECT to denote not creating any
* HTTP redirect. Note: this parameter requires Redirect module enabled,
* otherwise it will be disregarded
* - synonyms: (array) Array of field names of trunk term into which branch
* terms should be added as synonyms (until each field's cardinality limit
* is reached). Note: this parameter requires Synonyms module enabled,
* otherwise it will be disregarded
* - step: (int) How many term branches to merge per script run in batch. If
* you are hitting time or memory limits, decrease this parameter
* @param array $context
* Drupal Batch API context array
*/
function _term_merge_batch_process($term_branch, $term_trunk, $merge_settings, &$context) {
// Initializing sandbox.
if (!isset($context['sandbox']['current'])) {
$context['sandbox']['current'] = 0;
}
// Populating $merge_settings with defaults.
$merge_settings += array(
'term_branch_keep' => FALSE,
'merge_fields' => array(),
'keep_only_unique' => TRUE,
'redirect' => TERM_MERGE_NO_REDIRECT,
'synonyms' => array(),
'step' => 40,
);
$total = count($term_branch);
// To speed up the process we take advantage of taxonomy_term_load_multiple()
// instead of just repeating calls to taxonomy_term_load().
$till = min($total, $context['sandbox']['current'] + $merge_settings['step']);
$length = $till - $context['sandbox']['current'];
$term_branch = array_slice($term_branch, $context['sandbox']['current'], $length);
$term_branch = array_values(taxonomy_term_load_multiple($term_branch));
for ($i = 0; $i < $merge_settings['step'] && $context['sandbox']['current'] < $total; $i++) {
actions_do('term_merge_action', $term_branch[$i], array(
'term_trunk' => $term_trunk,
'term_branch_keep' => $merge_settings['term_branch_keep'],
'merge_fields' => $merge_settings['merge_fields'],
'keep_only_unique' => $merge_settings['keep_only_unique'],
'redirect' => $merge_settings['redirect'],
'synonyms' => $merge_settings['synonyms'],
));
$context['sandbox']['current']++;
}
if ($context['sandbox']['current'] != $total) {
$context['finished'] = $context['sandbox']['current'] / $total;
$term = $term_branch[$i - 1];
$context['message'] = t('Merged up to @term', array('@term' => $term->name));
}
}
/**
* Batch 'finished' callback.
*
* Process results of Term Merge batch.
*/
function term_merge_batch_finished($success, $results, $operations) {
if ($success) {
drupal_set_message(t('The terms have been successfully merged.'));
}
else {
// An error happened. We have to notify the user.
drupal_set_message(t('An error occurred. We are sorry, please, report this error to the maintainers of Term Merge module.'), 'error');
}
}
@@ -0,0 +1,16 @@
name = Term Merge
description = This module allows you to merge multiple terms into one, while updating all fields referring to those terms to refer to the replacement term instead.
package = Taxonomy
core = 7.x
files[] = term_merge.test
dependencies[] = taxonomy
dependencies[] = entity
; Information added by Drupal.org packaging script on 2015-01-14
version = "7.x-1.2"
core = "7.x"
project = "term_merge"
datestamp = "1421194982"
@@ -0,0 +1,711 @@
<?php
/**
* @file
* Provide functionality for merging taxonomy terms one into another.
*/
/**
* Constant to use in term merge action.
*
* Constant denotes "do not create HTTP redirect" logic for term merge action.
*
* @var int
*/
define('TERM_MERGE_NO_REDIRECT', -1);
/**
* Constant to use in term merge action.
*
* Constant denotes "Create a new term and use it as the term trunk" logic
* for term merge action.
*
* @var int
*/
define('TERM_MERGE_NEW_TERM_TRUNK', -1);
/**
* Implements hook_menu().
*/
function term_merge_menu() {
$items = array();
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/merge'] = array(
'title' => 'Merge terms',
'page callback' => 'drupal_get_form',
'page arguments' => array('term_merge_form', 3),
'access callback' => 'term_merge_access',
'access arguments' => array(3),
'file' => 'term_merge.pages.inc',
'type' => MENU_LOCAL_TASK,
);
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/merge/default'] = array(
'title' => 'Default',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/merge/duplicates'] = array(
'title' => 'Merge Duplicate Terms',
'page callback' => 'drupal_get_form',
'page arguments' => array('term_merge_duplicates_form', 3),
'access callback' => 'term_merge_access',
'access arguments' => array(3),
'file' => 'term_merge.pages.inc',
'type' => MENU_LOCAL_TASK,
);
$items['taxonomy/term/%taxonomy_term/merge'] = array(
'title' => 'Merge Terms',
'page callback' => 'drupal_get_form',
'page arguments' => array('term_merge_form', NULL, 2),
'access callback' => 'term_merge_access',
'access arguments' => array(NULL, 2),
'file' => 'term_merge.pages.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 10,
);
$items['taxonomy/term/%taxonomy_term/merge/default'] = array(
'title' => 'Default',
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['taxonomy/term/%taxonomy_term/merge/duplicates'] = array(
'title' => 'Merge Duplicate Terms',
'page callback' => 'drupal_get_form',
'page arguments' => array('term_merge_duplicates_form', NULL, 2),
'access callback' => 'term_merge_access',
'access arguments' => array(NULL, 2),
'file' => 'term_merge.pages.inc',
'type' => MENU_LOCAL_TASK,
);
$items['term-merge/autocomplete/term-trunk/%taxonomy_vocabulary_machine_name'] = array(
'title' => 'Autocomplete Term Merge form term trunk',
'page callback' => 'term_merge_form_term_trunk_widget_autocomplete_autocomplete',
'page arguments' => array(3),
'access callback' => 'term_merge_access',
'access arguments' => array(3),
'file' => 'term_merge.pages.inc',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_admin_paths().
*/
function term_merge_admin_paths() {
return array(
'taxonomy/term/*/merge' => TRUE,
'taxonomy/term/*/merge/*' => TRUE,
);
}
/**
* Implements hook_permission().
*/
function term_merge_permission() {
$permissions = array();
$permissions['merge terms'] = array(
'title' => t('Merge any terms'),
'description' => t('Gives the ability to merge any taxonomy terms.'),
);
$vocabularies = taxonomy_get_vocabularies();
foreach ($vocabularies as $vocabulary) {
$permissions['merge ' . $vocabulary->machine_name . ' terms'] = array(
'title' => t('Merge %name vocabulary terms', array('%name' => $vocabulary->name)),
'description' => t('Gives the ability to merge taxonomy terms that belong to vocabulary %name.', array('%name' => $vocabulary->name)),
);
}
return $permissions;
}
/**
* Implements hook_action_info().
*/
function term_merge_action_info() {
return array(
'term_merge_action' => array(
'type' => 'taxonomy',
'label' => t('Merge term'),
'configurable' => TRUE,
'behavior' => array('changes_property'),
),
);
}
/**
* Implements hook_help().
*/
function term_merge_help($path, $arg) {
switch ($path) {
// Main module help for the Term Merge module.
case 'admin/help#term_merge':
return '<p>' . t('Allows you to merge multiple terms into one and and at the same time update all fields referencing to the old ones.') . '</p>';
break;
}
}
/**
* Access callback for term merge action.
*
* Decide whether to grant access to an account for an operation of merging
* terms in a vocabulary.
*
* @param object $vocabulary
* Fully loaded vocabulary object inside of which term merge operation is
* requested for access granting
* @param object $term
* Fully loaded term object which belongs to the vocabulary inside of which
* term merge operation is requested for access granting. You are supposed
* only to provide either $vocabulary or $term. Depending on your context it
* might be more convenient for you to provide $term, and on other occasions
* it might be $vocabulary of more convenience
* @param object $account
* Fully loaded user object who is requesting access granting for the
* operation of term merging. You may provide nothing here, and the currently
* logged in user will be considered
*
* @return bool
* Whether the access for term merging operation has been granted
*/
function term_merge_access($vocabulary = NULL, $term = NULL, $account = NULL) {
if (is_null($vocabulary) && is_null($term)) {
// This is no go, at least one of these 2 has to be provided.
return FALSE;
}
if (is_null($account)) {
// Falling back on currently logged in user.
$account = $GLOBALS['user'];
}
if (is_null($vocabulary)) {
$vocabulary = taxonomy_vocabulary_load($term->vid);
}
return user_access('merge terms', $account) || user_access('merge ' . $vocabulary->machine_name . ' terms', $account);
}
/**
* Generate the configuration form for action "Term merge".
*/
function term_merge_action_form($context) {
$form = array();
$form['displaimer'] = array(
'#markup' => '<b>' . t('Sorry, currently Term Merge action is not supported via user interface. Please, contact the maintainers at the official website if you need it enabled via user interface.') . '</b>',
);
return $form;
}
/**
* Form submission function.
*
* Store information about configurable action.
*/
function term_merge_action_submit($form, &$form_state) {
// We don't have enabled UI for this action. It's just a dummy function.
return array();
}
/**
* Action function. Perform action "Term Merge".
*/
function term_merge_action($object, $context) {
$term_branch = $object;
$term_trunk = taxonomy_term_load($context['term_trunk']);
$vocabulary = taxonomy_vocabulary_load($term_branch->vid);
$term_branch_children = array();
foreach (taxonomy_get_tree($term_branch->vid, $term_branch->tid) as $term) {
$term_branch_children[] = $term->tid;
}
if ($term_branch->vid != $term_trunk->vid) {
watchdog('term_merge', 'Trying to merge 2 terms (%term_branch, %term_trunk) from different vocabularies', array(
'%term_branch' => $term_branch->name,
'%term_trunk' => $term_trunk->name,
), WATCHDOG_WARNING);
return;
}
if ($term_branch->tid == $term_trunk->tid) {
watchdog('term_merge', 'Trying to merge a term %term into itself.', array('%term' => $term_branch->name), WATCHDOG_WARNING);
return;
}
if (in_array($term_trunk->tid, $term_branch_children)) {
watchdog('term_merge', 'Trying to merge a term %term_branch into its child %term_trunk.', array(
'%term_branch' => $term_branch->name,
'%term_trunk' => $term_trunk->name,
), WATCHDOG_WARNING);
return;
}
// Defining some default values.
if (!isset($context['term_branch_keep'])) {
// It's easier to manually delete the unwanted terms, rather than
// search for your DB back up. So by default we keep the term branch.
$context['term_branch_keep'] = TRUE;
}
if (!isset($context['merge_fields'])) {
// Initializing it with an empty array if client of this function forgot to
// provide info about what fields to merge.
$context['merge_fields'] = array();
}
if (!isset($context['keep_only_unique'])) {
// Seems logical that mostly people will prefer to keep only one value in
// term reference field per taxonomy term.
$context['keep_only_unique'] = TRUE;
}
if (!isset($context['redirect']) || !module_exists('redirect')) {
// This behavior requires Redirect module installed and enabled.
$context['redirect'] = TERM_MERGE_NO_REDIRECT;
}
if (!isset($context['synonyms']) || !module_exists('synonyms')) {
// This behavior requires Synonyms module installed and enabled.
$context['synonyms'] = array();
}
// Calling a hook, this way we let whoever else to react and do his own extra
// logic when merging of terms occurs. We prefer to call it before we handle
// our own logic, because our logic might delete $term_branch and maybe a
// module that implements this hook needs this term not deleted yet.
module_invoke_all('term_merge', $term_trunk, $term_branch, $context);
if (!empty($context['merge_fields'])) {
// "Merging" the fields from $term_branch into $term_trunk where it is
// possible.
foreach ($context['merge_fields'] as $field_name) {
// Getting the list of available languages for this field.
$languages = array();
if (isset($term_trunk->$field_name) && is_array($term_trunk->$field_name)) {
$languages = array_merge($languages, array_keys($term_trunk->$field_name));
}
if (isset($term_branch->$field_name) && is_array($term_branch->$field_name)) {
$languages = array_merge($languages, array_keys($term_branch->$field_name));
}
$languages = array_unique($languages);
// Merging the data of both terms into $term_trunk.
foreach ($languages as $language) {
if (!isset($term_trunk->{$field_name}[$language])) {
$term_trunk->{$field_name}[$language] = array();
}
if (!isset($term_branch->{$field_name}[$language])) {
$term_branch->{$field_name}[$language] = array();
}
$term_trunk->{$field_name}[$language] = array_merge($term_trunk->{$field_name}[$language], $term_branch->{$field_name}[$language]);
}
}
// And now we can save $term_trunk after shifting all the fields from
// $term_branch.
taxonomy_term_save($term_trunk);
}
// Updating all the links to $term_branch to point now to $term_trunk
// firstly we go through the list of all fields searching for
// taxonomy_term_reference field type because potentially some of these fields
// values will have to be updated after merging terms.
$fields = field_info_field_map();
$result = array();
foreach ($fields as $field_name => $v) {
// Additionally we group by field_name to know what field has to be updated
// in each found entity.
// @todo: Here would be nice to throw in a hook, allowing other modules to
// supply meta data about their field types if they also use taxonomy
// references, defining it in their own field types.
if ($v['type'] == 'taxonomy_term_reference') {
$result[$field_name] = array();
$query = new EntityFieldQuery();
// Making sure we search in the entire scope of entities.
$query->addMetaData('account', user_load(1));
$query->fieldCondition($field_name, 'tid', $term_branch->tid);
$_result = $query->execute();
$result[$field_name] = array_merge_recursive($result[$field_name], $_result);
}
}
// Now we load all entities that have taxonomy_term_reference pointing to
// $term_branch.
foreach ($result as $field_name => $entity_types) {
foreach ($entity_types as $entity_type => $v) {
$ids = array_keys($v);
$entities = entity_load($entity_type, $ids);
// After we have loaded it, we alter the taxonomy_term_reference
// to point to $term_trunk.
foreach ($entities as $entity) {
// What is more, we have to do it for every available language.
foreach ($entity->$field_name as $language => $items) {
// Keeping track of whether term trunk is already present in this
// field in this language. This is useful for the option
// 'keep_only_unique'.
$is_trunk_added = FALSE;
foreach ($entity->{$field_name}[$language] as $delta => $item) {
if ($context['keep_only_unique'] && $is_trunk_added && in_array($item['tid'], array($term_trunk->tid, $term_branch->tid))) {
// We are instructed to keep only unique references and we already
// have term trunk in this field, so we just unset value for this
// delta.
unset($entity->{$field_name}[$language][$delta]);
}
else {
// Merging term references if necessary, and keep an eye on
// whether we already have term trunk among this field values.
switch ($item['tid']) {
case $term_trunk->tid:
$is_trunk_added = TRUE;
break;
case $term_branch->tid:
$is_trunk_added = TRUE;
$entity->{$field_name}[$language][$delta]['tid'] = $term_trunk->tid;
break;
}
}
}
// Above in the code, while looping through all deltas of this field,
// we might have unset some of the deltas to keep term references
// unique. We should better keep deltas as a series of consecutive
// numbers, because it is what it is supposed to be.
$entity->{$field_name}[$language] = array_values($entity->{$field_name}[$language]);
}
// After updating all the references, save the entity.
entity_save($entity_type, $entity);
}
}
}
// Adding term branch as synonym (Synonyms module integration).
foreach ($context['synonyms'] as $synonym_field) {
synonyms_add_entity_as_synonym($term_trunk, 'taxonomy_term', $synonym_field, $term_branch, 'taxonomy_term');
}
// It turned out we gotta go tricky with the Redirect module. If we create
// redirection before deleting the branch term (if we are instructed to delete
// in this action) redirect module will do its "auto-clean up" in
// hook_entity_delete() and will delete our just created redirects. But at the
// same time we have to get the path alias of the $term_branch before it gets
// deleted. Otherwise the path alias will be deleted along with the term
// itself. Similarly would be lost all redirects pointing to branch term
// paths. We will redirect normal term path and its RSS feed.
$redirect_paths = array();
if ($context['redirect'] != TERM_MERGE_NO_REDIRECT) {
$redirect_paths['taxonomy/term/' . $term_trunk->tid] = array(
'taxonomy/term/' . $term_branch->tid,
);
$redirect_paths['taxonomy/term/' . $term_trunk->tid . '/feed'] = array(
'taxonomy/term/' . $term_branch->tid . '/feed',
);
foreach ($redirect_paths as $redirect_destination => $redirect_sources) {
// We create redirect from Drupal normal path, then we try to fetch its
// alias. Lastly we collect a set of redirects that point to either of the
// 2 former paths. Everything we were able to fetch will be redirecting to
// the trunk term.
$alias = drupal_get_path_alias($redirect_sources[0]);
if ($alias != $redirect_sources[0]) {
$redirect_sources[] = $alias;
}
$existing_redirects = array();
foreach ($redirect_sources as $redirect_source) {
foreach (redirect_load_multiple(array(), array('redirect' => $redirect_source)) as $v) {
$existing_redirects[] = $v->source;
}
}
$redirect_paths[$redirect_destination] = array_unique(array_merge($redirect_sources, $existing_redirects));
}
}
if (!$context['term_branch_keep']) {
// If we are going to delete branch term, we need firstly to make sure
// all its children now have the parent of term_trunk.
foreach (taxonomy_get_children($term_branch->tid, $vocabulary->vid) as $child) {
$parents = taxonomy_get_parents($child->tid);
// Deleting the parental link to the term that is being merged.
unset($parents[$term_branch->tid]);
// And putting the parental link to the term that we merge into.
$parents[$term_trunk->tid] = $term_trunk;
$parents = array_unique(array_keys($parents));
$child->parent = $parents;
taxonomy_term_save($child);
}
// Views module integration. We update all Views taxonomy filter handlers
// configured to filter on term branch to filter on term trunk now, since
// the former becomes the latter.
if (module_exists('views')) {
$views = views_get_all_views();
foreach ($views as $view) {
// For better efficiency, we keep track of whether we have updated
// anything in a view, and thus whether we need to save it.
$needs_saving = FALSE;
// Even worse, we have to go through each display of each view.
foreach ($view->display as $display_id => $display) {
$view->set_display($display_id);
$filters = $view->display_handler->get_handlers('filter');
foreach ($filters as $filter_id => $filter_handler) {
// Currently we know how to update filters only of this particular
// class.
if (get_class($filter_handler) == 'views_handler_filter_term_node_tid') {
$filter = $view->get_item($display_id, 'filter', $filter_id);
if (isset($filter['value'][$term_branch->tid])) {
// Substituting term branch with term trunk.
unset($filter['value'][$term_branch->tid]);
$filter['value'][$term_trunk->tid] = $term_trunk->tid;
$view->set_item($display_id, 'filter', $filter_id, $filter);
$needs_saving = TRUE;
}
}
}
}
if ($needs_saving) {
$view->save();
}
}
}
// We are instructed to delete the term branch after the merge,
// and so we do.
taxonomy_term_delete($term_branch->tid);
}
// Here we do the 2nd part of integration with the Redirect module. Once the
// branch term has been deleted (if deleted), we can add the redirects
// without being afraid that the redirect module will delete them in its
// hook_entity_delete().
foreach ($redirect_paths as $redirect_destination => $redirect_sources) {
foreach ($redirect_sources as $redirect_source) {
$redirect = redirect_load_by_source($redirect_source);
if (!$redirect) {
// Seems like redirect from such URI does not exist yet, we will create
// it.
$redirect = new stdClass();
redirect_object_prepare($redirect, array(
'source' => $redirect_source,
));
}
$redirect->redirect = $redirect_destination;
$redirect->status_code = $context['redirect'];
redirect_save($redirect);
}
}
watchdog('term_merge', 'Successfully merged term %term_branch into term %term_trunk in vocabulary %vocabulary. Context: @context', array(
'%term_branch' => $term_branch->name,
'%term_trunk' => $term_trunk->name,
'%vocabulary' => $vocabulary->name,
'@context' => var_export($context, 1),
));
}
/**
* Merge terms one into another using batch API.
*
* @param array $term_branch
* A single term tid or an array of term tids to be merged, aka term branches
* @param int $term_trunk
* The tid of the term to merge term branches into, aka term trunk
* @param array $merge_settings
* Array of settings that control how merging should happen. Currently
* supported settings are:
* - term_branch_keep: (bool) Whether the term branches should not be
* deleted, also known as "merge only occurrences" option
* - merge_fields: (array) Array of field names whose values should be
* merged into the values of corresponding fields of term trunk (until
* each field's cardinality limit is reached)
* - keep_only_unique: (bool) Whether after merging within one field only
* unique taxonomy term references should be kept in other entities. If
* before merging your entity had 2 values in its taxonomy term reference
* field and one was pointing to term branch while another was pointing to
* term trunk, after merging you will end up having your entity
* referencing to the same term trunk twice. If you pass TRUE in this
* parameter, only a single reference will be stored in your entity after
* merging
* - redirect: (int) HTTP code for redirect from $term_branch to
* $term_trunk, 0 stands for the default redirect defined in Redirect
* module. Use constant TERM_MERGE_NO_REDIRECT to denote not creating any
* HTTP redirect. Note: this parameter requires Redirect module enabled,
* otherwise it will be disregarded
* - synonyms: (array) Array of field names of trunk term into which branch
* terms should be added as synonyms (until each field's cardinality limit
* is reached). Note: this parameter requires Synonyms module enabled,
* otherwise it will be disregarded
* - step: (int) How many term branches to merge per script run in batch. If
* you are hitting time or memory limits, decrease this parameter
*/
function term_merge($term_branch, $term_trunk, $merge_settings = array()) {
// Older versions of this module had another interface of this function,
// as backward capability we still support the older interface, instead of
// supplying a $merge_settings array, it was supplying all the settings as
// additional function arguments.
// @todo: delete this backward capability at some point.
if (!is_array($merge_settings)) {
$merge_settings = array(
'term_branch_keep' => $merge_settings,
);
}
// Create an array of sources if it isn't yet.
if (!is_array($term_branch)) {
$term_branch = array($term_branch);
}
// Creating a skeleton for the merging batch.
$batch = array(
'title' => t('Merging terms'),
'operations' => array(
array('_term_merge_batch_process', array(
$term_branch,
$term_trunk,
$merge_settings,
)),
),
'finished' => 'term_merge_batch_finished',
'file' => drupal_get_path('module', 'term_merge') . '/term_merge.batch.inc',
);
// Initialize the batch process.
batch_set($batch);
}
/**
* Generate and return form elements that control behavior of merge action.
*
* Output of this function should be used in any form that merges terms,
* ensuring unified interface. It should be used in conjunction with
* term_merge_merge_options_submit(), which will process the submitted values
* for you and return an array of merge settings.
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object in which merging occurs
*
* @return array
* Array of form elements that allow controlling term merge action
*
* @see term_merge_merge_options_submit()
*/
function term_merge_merge_options_elements($vocabulary) {
// @todo: it would be nice to provide some ability to supply default values
// for each setting.
$form = array();
// Getting bundle name and a list of fields attached to this bundle for
// further use down below in the code while generating form elements.
$bundle = field_extract_bundle('taxonomy_term', $vocabulary);
$instances = field_info_instances('taxonomy_term', $bundle);
$form['term_branch_keep'] = array(
'#type' => 'checkbox',
'#title' => t('Only merge occurrences'),
'#description' => t('Check this if you want to only merge the occurrences of the specified terms, i.e. the terms will not be deleted from your vocabulary.'),
);
if (!empty($instances)) {
$options = array();
foreach ($instances as $instance) {
$options[$instance['field_name']] = $instance['label'];
}
$form['merge_fields'] = array(
'#type' => 'checkboxes',
'#title' => t('Merge Term Fields'),
'#description' => t('Check the fields whose values from branch terms you want to add to the values of corresponding fields of the trunk term. <b>Important note:</b> the values will be added until the cardinality limit for the selected fields is reached.'),
'#options' => $options,
);
}
$form['keep_only_unique'] = array(
'#type' => 'checkbox',
'#title' => t('Keep only unique terms after merging'),
'#description' => t('Sometimes after merging you may end up having a node (or any other entity) pointing twice to the same taxonomy term, tick this checkbox if want to keep only unique terms in other entities after merging.'),
);
if (module_exists('redirect')) {
$options = array(
TERM_MERGE_NO_REDIRECT => t('No redirect'),
0 => t('Default (@default)', array(
'@default' => variable_get('redirect_default_status_code', 301),
)),
) + redirect_status_code_options();
$form['redirect'] = array(
// We respect access rights defined in redirect.module here.
'#access' => user_access('administer redirects'),
'#type' => 'select',
'#title' => t('Create Redirect'),
'#description' => t('If you want to create an HTTP redirect from your branch terms to the trunk term, please, choose the HTTP redirect code here.'),
'#required' => TRUE,
'#options' => $options,
'#default_value' => TERM_MERGE_NO_REDIRECT,
);
}
else {
$form['redirect'] = array(
'#markup' => t('Enable the module ' . l('Redirect', 'http://drupal.org/project/redirect') . ' if you want to do an HTTP redirect from your term branch to the term trunk.'),
);
}
if (module_exists('synonyms')) {
$options = array();
foreach (synonyms_synonyms_fields($vocabulary) as $field_name) {
$options[$field_name] = $instances[$field_name]['label'];
}
$form['synonyms'] = array(
'#type' => 'checkboxes',
'#title' => t('Add as Synonyms'),
'#description' => t('Synonyms module allows you to add branch terms as synonyms into any of fields, enabled as sources of synonyms in vocabulary. Check the fields into which you would like to add branch terms as synonyms. <b>Important note:</b> the values will be added until the cardinality limit for the selected fields is reached.'),
'#options' => $options,
);
}
else {
$form['synonyms'] = array(
'#markup' => t('Enable the module ' . l('Synonyms', 'http://drupal.org/project/synonyms') . ' if you want to be able to add branch terms as synonyms into a field of your trunk term.'),
);
}
$form['step'] = array(
'#type' => 'textfield',
'#title' => t('Step'),
'#description' => t('Please, specify how many terms to process per script run in batch. If you are hitting time or memory limits in your PHP, decrease this number.'),
'#default_value' => 40,
'#required' => TRUE,
'#element_validate' => array('element_validate_integer_positive'),
);
return $form;
}
/**
* Return merge settings array.
*
* Output of this function should be used for supplying into term_merge()
* function or for triggering actions_do('term_merge_action', ...) action. This
* function should be invoked in a form submit handler for a form that used
* term_merge_merge_options_elements() for generating merge settings elements.
* It will process data and return an array of merge settings, according to the
* data user has submitted in your form.
*
* @param array $merge_settings_element
* That part of form that was generated by term_merge_merge_options_elements()
* @param array $form_state
* Form state array of the submitted form
* @param array $form
* Form array of the submitted form
*
* @return array
* Array of merge settings that can be used for calling term_merge() or
* invoking 'term_merge_action' action
*
* @see term_merge_merge_options_elements()
*/
function term_merge_merge_options_submit($merge_settings_element, &$form_state, $form) {
$merge_settings = array(
'term_branch_keep' => (bool) $merge_settings_element['term_branch_keep']['#value'],
'merge_fields' => isset($merge_settings_element['merge_fields']['#value']) ? array_values(array_filter($merge_settings_element['merge_fields']['#value'])) : array(),
'keep_only_unique' => (bool) $merge_settings_element['keep_only_unique']['#value'],
'redirect' => isset($merge_settings_element['redirect']['#value']) ? $merge_settings_element['redirect']['#value'] : TERM_MERGE_NO_REDIRECT,
'synonyms' => isset($merge_settings_element['synonyms']['#value']) ? array_values(array_filter($merge_settings_element['synonyms']['#value'])) : array(),
'step' => (int) $merge_settings_element['step']['#value'],
);
return $merge_settings;
}
@@ -0,0 +1,844 @@
<?php
/**
* @file
* Menu page callbacks for Term Merge module.
*/
/**
* Menu callback.
*
* Allow user to specify which terms to be merged into which term and any
* other settings needed for the term merge action.
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object
* @param object $term
* Fully loaded taxonomy term object that should be selected as the default
* merge term in the form. If the $vocabulary is omitted, the vocabulary of
* $term is considered
*
* @return array
* Array of the form in Form API format
*/
function term_merge_form($form, $form_state, $vocabulary = NULL, $term = NULL) {
if (is_null($vocabulary)) {
$vocabulary = taxonomy_vocabulary_load($term->vid);
}
// It's always handy to have the vocabulary by hand.
$form['#vocabulary'] = $vocabulary;
if (!isset($form_state['storage']['confirm'])) {
// We are at the set up step.
$tree = taxonomy_get_tree($vocabulary->vid);
$term_branch_value = is_null($term) ? NULL : array($term->tid);
if (variable_get('taxonomy_override_selector', FALSE) && module_exists('hs_taxonomy')) {
// We use Hierarchical Select module if it's available and configured to
// be used for taxonomy selects.
$form['term_branch'] = array(
'#type' => 'hierarchical_select',
// @todo: figure out why #required => TRUE doesn't work.
// As a matter of fact, this issue seems to cover our case.
// http://drupal.org/node/1275862.
//'#required' => TRUE,
'#config' => array(
'module' => 'hs_taxonomy',
'params' => array(
'vid' => $vocabulary->vid,
'exclude_tid' => NULL,
'root_term' => FALSE,
),
'enforce_deepest' => 0,
'entity_count' => 0,
'require_entity' => 0,
'save_lineage' => 0,
'level_labels' => array(
'status' => 0,
),
'dropbox' => array(
'status' => 1,
'limit' => 0,
),
'editability' => array(
'status' => 0,
),
'resizable' => TRUE,
'render_flat_select' => 0,
),
);
}
else {
// Falling back on a simple <select>.
$options = array();
foreach ($tree as $v) {
$options[$v->tid] = str_repeat('-', $v->depth) . $v->name . ' [tid: ' . $v->tid . ']';
}
$form['term_branch'] = array(
'#type' => 'select',
'#required' => TRUE,
'#multiple' => TRUE,
'#options' => $options,
'#size' => 8,
);
}
$form['term_branch'] = array(
'#title' => t('Terms to Merge'),
'#description' => t('Please, choose the terms you want to merge into another term.'),
'#ajax' => array(
'callback' => 'term_merge_form_term_trunk',
'wrapper' => 'term-merge-form-term-trunk',
'method' => 'replace',
'effect' => 'fade',
),
'#default_value' => $term_branch_value,
) + $form['term_branch'];
if (is_null($form['term_branch']['#default_value'])) {
unset($form['term_branch']['#default_value']);
}
$form['term_trunk'] = array(
'#type' => 'fieldset',
'#title' => t('Merge Into'),
'#prefix' => '<div id="term-merge-form-term-trunk">',
'#suffix' => '</div>',
'#tree' => TRUE,
);
// Array of currently available widgets for choosing term trunk.
$term_trunk_widget_options = array(
'autocomplete' => 'Autocomplete',
);
if (variable_get('taxonomy_override_selector', FALSE) && module_exists('hs_taxonomy')) {
$term_trunk_widget_options['hs_taxonomy'] = t('Hierarchical Select');
$term_trunk_widget = 'hs_taxonomy';
}
else {
$term_trunk_widget_options['select'] = t('Select');
$term_trunk_widget = 'select';
}
// If the vocabulary is too big, by default we want the trunk term widget to
// be autocomplete instead of select or hs_taxonomy.
if (count($tree) > 200) {
$term_trunk_widget = 'autocomplete';
}
// Override the term trunk widget if settings are found in $form_state.
if (isset($form_state['values']['term_trunk']['widget']) && in_array($form_state['values']['term_trunk']['widget'], array_keys($term_trunk_widget_options))) {
$term_trunk_widget = $form_state['values']['term_trunk']['widget'];
}
$form['term_trunk']['widget'] = array(
'#type' => 'radios',
'#title' => t('Widget'),
'#required' => TRUE,
'#options' => $term_trunk_widget_options,
'#default_value' => $term_trunk_widget,
'#description' => t('Choose what widget you prefer for entering the term trunk.'),
'#ajax' => array(
'callback' => 'term_merge_form_term_trunk',
'wrapper' => 'term-merge-form-term-trunk',
'method' => 'replace',
'effect' => 'fade',
),
);
// @todo:
// There is a known bug, if user has selected something in one widget, and
// then changes the widget, $form_states['values'] will hold the value for
// term trunk form element in the format that is used in one widget, while
// this value will be passed to another widget. This triggers different
// unpleasant effects like showing tid instead of term's name or vice-versa.
// I think we should just empty $form_state['values'] for the term trunk
// form element when widget changes. Better ideas are welcome!
$function = 'term_merge_form_term_trunk_widget_' . $term_trunk_widget;
$function($form, $form_state, $vocabulary);
// Ensuring the Merge Into form element has the same title no matter what
// widget has been used.
$form['term_trunk']['tid']['#title'] = t('Merge into');
// Adding necessary options of merging.
$form += term_merge_merge_options_elements($vocabulary);
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
}
else {
// We are at the confirmation step.
$count = count($form_state['values']['term_branch']);
$question = format_plural($count, 'Are you sure want to merge 1 term?', 'Are you sure want to merge @count terms?');
$form = confirm_form($form, $question, 'admin/structure/taxonomy/' . $vocabulary->machine_name);
}
return $form;
}
/**
* Supportive function.
*
* Validate the term_merge_form(). Make sure term trunk is not among the
* selected term branches or their children.
*/
function term_merge_form_validate($form, &$form_state) {
if (!isset($form_state['storage']['confirm'])) {
// We only validate the 1st step of the form.
$prohibited_trunks = array();
foreach ($form_state['values']['term_branch'] as $term_branch) {
$children = taxonomy_get_tree($form['#vocabulary']->vid, $term_branch);
$prohibited_trunks[] = $term_branch;
foreach ($children as $child) {
$prohibited_trunks[] = $child->tid;
}
}
if (in_array($form_state['values']['term_trunk']['tid'], $prohibited_trunks)) {
form_error($form['term_trunk']['tid'], t('Trunk term cannot be one of the selected branch terms or their children.'));
}
}
}
/**
* Submit handler for term_merge_form(). Merge terms one into another.
*/
function term_merge_form_submit($form, &$form_state) {
if (!isset($form_state['storage']['confirm'])) {
// Since merging terms is an important operation, we better confirm user
// really wants to do this.
$form_state['storage']['confirm'] = 0;
$form_state['rebuild'] = TRUE;
// Before storing the submitted values we slightly preprocess them to make
// sure they correspond to what is expected by submit handler of taxonomy
// creation form.
if (isset($form_state['values']['relations'])) {
$form_state['values'] += $form_state['values']['relations'];
}
$form_state['storage']['info'] = $form_state['values'];
$form_state['storage']['merge_settings'] = term_merge_merge_options_submit($form, $form_state, $form);
$form_state['storage']['old_form'] = $form;
}
else {
// The user has confirmed merging. We pull up the submitted values.
$form_state['values'] = $form_state['storage']['info'];
// If necessary, create the term trunk.
if ($form_state['values']['term_trunk']['tid'] == TERM_MERGE_NEW_TERM_TRUNK) {
// We try to mimic normal form submission for taxonomy module.
module_load_include('inc', 'taxonomy', 'taxonomy.admin');
taxonomy_form_term_submit($form_state['storage']['old_form']['term_trunk']['term_create'], $form_state);
$term_trunk = $form_state['term'];
}
else {
$term_trunk = taxonomy_term_load($form_state['values']['term_trunk']['tid']);
}
term_merge(array_values($form_state['values']['term_branch']), $term_trunk->tid, $form_state['storage']['merge_settings']);
$form_state['redirect'] = array('taxonomy/term/' . $term_trunk->tid);
}
}
/**
* Supportive function.
*
* Generate form elements for select widget for term trunk element of the
* term_merge_form().
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object
*/
function term_merge_form_term_trunk_widget_select(&$form, &$form_state, $vocabulary) {
$tree = taxonomy_get_tree($vocabulary->vid);
$options = array();
foreach ($tree as $v) {
$options[$v->tid] = str_repeat('-', $v->depth) . $v->name . ' [tid: ' . $v->tid . ']';
}
$term_branch_value = array();
// Firstly trying to look up selected term branches in the default value of
// term branch form element.
if (isset($form['term_branch']['#default_value']) && is_array($form['term_branch']['#default_value'])) {
$term_branch_value = $form['term_branch']['#default_value'];
}
if (isset($form_state['values']['term_branch']) && is_array($form_state['values']['term_branch'])) {
$term_branch_value = $form_state['values']['term_branch'];
}
if (!empty($term_branch_value)) {
// We have to make sure among term_trunk there is no term_branch or any of
// their children.
foreach ($term_branch_value as $v) {
unset($options[$v]);
foreach (taxonomy_get_tree($vocabulary->vid, $v) as $child) {
unset($options[$child->tid]);
}
}
$options = array(TERM_MERGE_NEW_TERM_TRUNK => 'New Term') + $options;
}
else {
// Term branch has not been selected yet.
$options = array();
}
$form['term_trunk']['tid'] = array(
'#type' => 'select',
'#required' => TRUE,
'#description' => t('Choose into what term you want to merge.'),
'#options' => $options,
'#ajax' => array(
'callback' => 'term_merge_form_term_trunk_term_create',
'wrapper' => 'term-merge-form-term-trunk-term-create',
'method' => 'replace',
'effect' => 'fade',
),
);
$form['term_trunk']['term_create'] = array(
'#prefix' => '<div id="term-merge-form-term-trunk-term-create">',
'#suffix' => '</div>',
);
// We throw in the Taxonomy native term create form only if the option for
// creation of a new term was selected by user.
if (isset($form_state['values']['term_trunk']['tid']) && $form_state['values']['term_trunk']['tid'] == TERM_MERGE_NEW_TERM_TRUNK) {
module_load_include('inc', 'taxonomy', 'taxonomy.admin');
$form['term_trunk']['term_create'] += array(
'#type' => 'fieldset',
'#title' => t('Create New Term'),
);
$form['term_trunk']['term_create'] += taxonomy_form_term($form['term_trunk']['term_create'], $form_state, array(), $vocabulary);
// We have our own submit button, so we unset the normal one from the term
// create form.
unset($form['term_trunk']['term_create']['actions']);
// Additionally we have to filter out from "Parent Terms" select the already
// selected branch terms and their children, because we can't merge into
// the term itself or its children.
// We do a trick here, since we know the 1st element is the <root> option
// and all others are normal taxonomy terms, we keep the 1st element as it
// is while all the other elements we substitute with our $options array
// which is basically identical but already has been filtered out unwanted
// terms. Plus we have to unset the 'New Term' option from $options.
unset($options[TERM_MERGE_NEW_TERM_TRUNK]);
if (is_array($form['term_trunk']['term_create']['relations']['parent']['#options'])) {
$form['term_trunk']['term_create']['relations']['parent']['#options'] = array_slice($form['term_trunk']['term_create']['relations']['parent']['#options'], 0, 1, TRUE) + $options;
}
// For each field attached to taxonomy term of this vocabulary that has
// unlimited cardinality we have to extra process the results, otherwise
// "Add another item" button doesn't work.
$instances = field_info_instances($form['term_trunk']['term_create']['#entity_type'], $form['term_trunk']['term_create']['#bundle']);
foreach ($instances as $instance) {
$field = field_info_field($instance['field_name']);
if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED) {
if (isset($form['term_trunk']['term_create'][$field['field_name']][LANGUAGE_NONE]['add_more']['#limit_validation_errors'])) {
$form['term_trunk']['term_create'][$field['field_name']][LANGUAGE_NONE]['add_more']['#limit_validation_errors'] = array(array('term_branch'), array('term_trunk'));
}
}
}
}
}
/**
* Supportive function.
*
* Generate form element for hierarchical select widget for term trunk element
* of the term_merge_form().
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object
*/
function term_merge_form_term_trunk_widget_hs_taxonomy(&$form, &$form_state, $vocabulary) {
$form['term_trunk']['tid'] = array(
'#type' => 'hierarchical_select',
'#description' => t('Please select a term to merge into.'),
'#required' => TRUE,
'#element_validate' => array('term_merge_form_trunk_term_widget_hs_taxonomy_validate'),
'#config' => array(
'module' => 'hs_taxonomy',
'params' => array(
'vid' => $vocabulary->vid,
'exclude_tid' => NULL,
'root_term' => FALSE,
),
'enforce_deepest' => 0,
'entity_count' => 0,
'require_entity' => 0,
'save_lineage' => 0,
'level_labels' => array(
'status' => 0,
),
'dropbox' => array(
'status' => 0,
),
'editability' => array(
'status' => 0,
),
'resizable' => TRUE,
'render_flat_select' => 0,
),
);
}
/**
* Supportive function.
*
* Generate form elements for autocomplete widget for term trunk element of the
* term_merge_form().
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object
*/
function term_merge_form_term_trunk_widget_autocomplete(&$form, &$form_state, $vocabulary) {
$form['term_trunk']['tid'] = array(
'#type' => 'textfield',
'#description' => t("Start typing in a term's name in order to get some suggestions."),
'#required' => TRUE,
'#autocomplete_path' => 'term-merge/autocomplete/term-trunk/' . $vocabulary->machine_name,
'#element_validate' => array('term_merge_form_trunk_term_widget_autocomplete_validate'),
);
}
/**
* Supportive function.
*
* Validate form element of the autocomplete widget of term trunk element of
* the form term_merge_form(). Make sure the entered string is a name of one of
* the existing terms in the vocabulary where the merge occurs. If term is found
* the function substitutes the name with its {taxonomy_term_data}.tid as it is
* what is expected from a term trunk widget to provide in its value.
*/
function term_merge_form_trunk_term_widget_autocomplete_validate($element, &$form_state, $form) {
$term = taxonomy_get_term_by_name($element['#value'], $form['#vocabulary']->machine_name);
if (!is_array($term) || empty($term)) {
// Seems like the user has entered a non existing name in the autocomplete
// textfield.
form_error($element, t('There are no terms with name %name in the %vocabulary vocabulary.', array(
'%name' => $element['#value'],
'%vocabulary' => $form['#vocabulary']->name,
)));
}
else {
// We have to substitute the term's name with its tid in order to make this
// widget consistent with the interface.
$term = array_pop($term);
form_set_value($element, $term->tid, $form_state);
}
}
/**
* Supportive function.
*
* Validate form element of the Hierarchical Select widget of term trunk element
* of the form term_merge_form(). Convert the value from array to a single tid
* integer value.
*/
function term_merge_form_trunk_term_widget_hs_taxonomy_validate($element, &$form_state, $form) {
$tid = 0;
if (is_array($element['#value']) && !empty($element['#value'])) {
$tid = (int) array_pop($element['#value']);
}
form_set_value($element, $tid, $form_state);
}
/**
* Menu page callback function.
*
* Autocomplete callback function for the trunk term form element in the widget
* of autocomplete. The code of this function was mainly copy-pasted from
* Taxonomy autocomplete widget menu callback function.
*
* @param object $vocabulary
* Fully loaded vocabulary object inside of which the terms are about to be
* merged
*/
function term_merge_form_term_trunk_widget_autocomplete_autocomplete($vocabulary) {
// If the request has a '/' in the search text, then the menu system will have
// split it into multiple arguments, recover the intended $tags_typed.
$args = func_get_args();
// Shift off the $vocabulary argument.
array_shift($args);
$tags_typed = implode('/', $args);
// Querying database for suggestions.
$query = db_select('taxonomy_term_data', 't');
$tags_return = $query->addTag('translatable')
->addTag('term_access')
->fields('t', array('tid', 'name'))
->condition('t.vid', $vocabulary->vid)
->condition('t.name', '%' . db_like($tags_typed) . '%', 'LIKE')
->range(0, 10)
->execute()
->fetchAllKeyed();
$term_matches = array();
foreach ($tags_return as $tid => $name) {
$n = $name;
// Term names containing commas or quotes must be wrapped in quotes.
if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
$n = '"' . str_replace('"', '""', $name) . '"';
}
$term_matches[$n] = check_plain($name . ' [tid: ' . $tid . ']');
}
drupal_json_output($term_matches);
}
/**
* Ajax callback function.
*
* Used in term_merge_term_merge_form() to replace the term_trunk element
* depending on already selected term_branch values.
*/
function term_merge_form_term_trunk($form, $form_state) {
return $form['term_trunk'];
}
/**
* Ajax callback function.
*
* Used in term_merge_term_merge_form() to replace the term create fieldset
* depending on already selected term_branch values and the term_trunk value.
*/
function term_merge_form_term_trunk_term_create($form, $form_state) {
return $form['term_trunk']['term_create'];
}
/**
* Generate 'term_merge_duplicates_form'.
*
* Allow merging terms with the same or similar names.
*
* @param object $vocabulary
* Fully loaded taxonomy vocabulary object inside of which term merging
* occurs, if this argument is omitted, then $term is required and will be
* used to obtain information about Taxonomy vocabulary
* @param object $parent_term
* Fully loaded taxonomy term object using which the function will pull up
* the vocabulary inside of which term merging occurs. Duplicate terms will be
* sought only among children of this term
*/
function term_merge_duplicates_form($form, &$form_state, $vocabulary = NULL, $parent_term = NULL) {
// TODO: make this JavaScript #attached.
drupal_add_js(drupal_get_path('module', 'term_merge') . '/js/duplicate.form.js');
// Checking if we were not given vocabulary object, we will use term object to
// obtain the former.
if (!is_null($parent_term) && is_null($vocabulary)) {
$vocabulary = taxonomy_vocabulary_load($parent_term->vid);
}
$tree = taxonomy_get_tree($vocabulary->vid, is_null($parent_term) ? 0 : $parent_term->tid);
// Helpful and self explaining text that should help people understand what's
// up.
$form['help'] = array(
'#markup' => '<p>' . t('Here you can merge terms with the same names. It is a useful tool against term-duplicates. If this tool is invoked on a term (not on the entire vocabulary), duplicate terms will be sought only among children of that term. The terms are grouped by names. Term into which the merging will occur is selected manually by user, however you must know that it is impossible to merge a parent term into any of its children.') . '</p>',
);
$form['scaling'] = array(
'#type' => 'fieldset',
'#title' => t('Scaling for large vocabularies'),
'#description' => t('Adjust these settings if your vocabulary is very large.'),
'#tree' => TRUE,
'#collapsible' => TRUE,
);
$form['scaling']['help'] = array(
'#markup' => '<p>' . format_plural(count($tree), 'Vocabulary %vocabulary has only 1 term. It is very unlikely you will merge anything here.', 'Vocabulary %vocabulary has @count terms. If this tool works slow, you may instruct the duplicate finder tool to terminate its work after it has found a specific number of possible duplicates.', array(
'%vocabulary' => $vocabulary->name,
)) . '</p>',
);
$form['scaling']['max_duplicates'] = array(
'#type' => 'textfield',
'#title' => t('Show N duplicates'),
'#description' => t('Input an integer here - this many duplicates will be show on the form. Once this amount of possible duplicates is found, the search process terminates.'),
'#required' => TRUE,
'#default_value' => isset($form_state['values']['scaling']['max_duplicates']) ? $form_state['values']['scaling']['max_duplicates'] : 300,
'#element_validate' => array('element_validate_integer_positive'),
);
$form['scaling']['update'] = array(
'#type' => 'button',
'#value' => t('Re-run duplicate search'),
'#ajax' => array(
'callback' => 'term_merge_duplicates_form_scaling',
'wrapper' => 'term-merge-duplicate-wrapper',
'method' => 'replace',
'effect' => 'fade',
),
);
// Amount of found duplicates.
$count = 0;
// Array of groups of terms with the same name. Each group is an array of
// duplicates. Trunk term of each group will be chosen by user.
$groups = array();
foreach ($tree as $term) {
if ($count >= $form['scaling']['max_duplicates']['#default_value']) {
// We have reached the limit of possible duplicates to be found.
break;
}
$name = term_merge_duplicates_process_name($term->name);
if (!isset($groups[$name])) {
$groups[$name] = array();
}
else {
// We increment count by one for the just encountered duplicate. Plus, if
// it is the second duplicate in this group, we also increment it by one
// for the 1st duplicate in the group.
$count++;
if (count($groups[$name]) == 1) {
$count++;
}
}
$groups[$name][$term->tid] = $term;
}
$form['wrapper'] = array(
'#prefix' => '<div id="term-merge-duplicate-wrapper">',
'#suffix' => '</div>',
);
if ($count > 0) {
$form['wrapper']['global_switch'] = array(
'#type' => 'checkbox',
'#title' => t('Select All Terms'),
'#description' => t('Checking here will select for merging all the encountered duplicate terms.'),
'#attributes' => array(
'class' => array('term-merge-duplicate-general-switch'),
),
);
}
$form['wrapper']['group'] = array(
'#tree' => TRUE,
);
foreach ($groups as $i => $group) {
if (count($group) > 1) {
// Sorting terms by tid for better usage experience.
ksort($group);
$first_term = reset($group);
$options = array();
foreach ($group as $term) {
$parents = array();
// Adding Root to the hierarchy.
$parents[] = t('Vocabulary Root');
foreach (taxonomy_get_parents_all($term->tid) as $parent) {
// We do not include the current term in the hierarchy.
if ($parent->tid != $term->tid) {
$parents[] = $parent->name;
}
}
$language = isset($term->language) ? $term->language : LANGUAGE_NONE;
if ($language == LANGUAGE_NONE) {
$language = t('Not Specified');
}
$options[$term->tid] = array(
'id' => $term->tid,
'title' => l($term->name, 'taxonomy/term/' . $term->tid),
'language' => $language,
'description' => check_markup($term->description, $term->format),
'parents' => implode(' &raquo; ', $parents),
);
}
$form['wrapper']['group'][$i] = array(
'#type' => 'fieldset',
'#title' => check_plain($first_term->name),
'#collapsible' => TRUE,
'#pre_render' => array('term_merge_duplicates_fieldset_preprocess'),
'#element_validate' => array('term_merge_duplicates_fieldset_validate'),
);
$form['wrapper']['group'][$i]['duplicates'] = array(
'#type' => 'tableselect',
'#title' => 'Duplicates',
'#header' => array(
'id' => t('ID'),
'title' => t('Title'),
'description' => t('Description'),
'language' => t('Language'),
'parents' => t('Parents'),
),
'#options' => $options,
);
$options = array();
foreach ($group as $term) {
$options[$term->tid] = $term->name;
}
$form['wrapper']['group'][$i]['trunk_tid'] = array(
'#type' => 'radios',
'#title' => t('Merge Into'),
'#options' => $options,
'#attributes' => array(
'class' => array('term-merge-duplicate-trunk'),
),
);
}
}
if ($count > 0) {
// Adding necessary options of merging.
$form += term_merge_merge_options_elements($vocabulary);
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['submit'] = array(
'#type' => 'submit',
'#value' => t('Submit'),
);
}
else {
if (is_null($parent_term)) {
$no_match_text = t('Sorry, seems like we were not able to find any possible duplicate terms in %vocabulary vocabulary.', array(
'%vocabulary' => $vocabulary->name,
));
}
else {
$no_match_text = t('Sorry, seems like we were not able to find any possible duplicate terms among children of %term term. You may want to search for duplicates through the entire <a href="!url">vocabulary</a>.', array(
'%term' => $parent_term->name,
'!url' => url('admin/structure/taxonomy/' . $vocabulary->machine_name . '/merge/duplicates'),
));
}
$form['nothing_found'] = array(
'#markup' => '<p><b>' . $no_match_text . '</b></p>',
);
}
return $form;
}
/**
* Submit handler for 'term_merge_duplicates_form'.
*
* Actually merge duplicate terms.
*/
function term_merge_duplicates_form_submit($form, &$form_state) {
$batch = array(
'title' => t('Merging terms'),
'operations' => array(),
'finished' => 'term_merge_batch_finished',
'file' => drupal_get_path('module', 'term_merge') . '/term_merge.batch.inc',
);
// Processing general options for merging.
$merge_settings = term_merge_merge_options_submit($form, $form_state, $form);
if (isset($form_state['values']['group'])) {
foreach ($form_state['values']['group'] as $values) {
// Filtering out only the selected duplicate terms.
$term_branches = array_filter($values['duplicates']);
// We also do not want to have trunk term to be among the branch terms.
unset($term_branches[$values['trunk_tid']]);
if (!empty($term_branches)) {
// If something has been selected in this group we schedule its merging.
$batch['operations'][] = array('_term_merge_batch_process', array(
$term_branches,
$values['trunk_tid'],
$merge_settings,
));
}
}
}
if (empty($batch['operations'])) {
drupal_set_message(t('No merging has been made, because you have not selected any duplicate term to merge.'));
}
else {
batch_set($batch);
}
}
/**
* String process function.
*
* Manipulate supplied var $name and by the output of this function terms in a
* vocabulary are grouped as duplicates.
*
* @param string $name
* String that needs to be manipulated
*
* @return string
* Processed string (normally it implies making it upper case, stripping down
* any special chars, etc.)
*/
function term_merge_duplicates_process_name($name) {
// Making upper case.
$name = drupal_strtoupper($name);
// Trying transliteration, if available.
if (module_exists('transliteration')) {
$name = transliteration_get($name);
// Keeping only ASCII chars.
$name = preg_replace('#\W#', '', $name);
}
return $name;
}
/**
* Form element preprocess function.
*
* Insert extra column for choosing term trunk into tableselect of terms to be
* merged.
*/
function term_merge_duplicates_fieldset_preprocess($element) {
$options = &$element['duplicates']['#options'];
foreach ($options as $tid => $row) {
$element['trunk_tid'][$tid]['#title_display'] = 'invisible';
$options[$tid] = array(
'trunk' => drupal_render($element['trunk_tid'][$tid]),
) + $options[$tid];
}
$element['trunk_tid']['#title_display'] = 'invisible';
$element['duplicates']['#header'] = array(
'trunk' => $element['trunk_tid']['#title'],
) + $element['duplicates']['#header'];
return $element;
}
/**
* FAPI element validation callback.
*
* Validate fieldset of a 'term_merge_duplicates_form' form, if any duplicate
* has been selected for merging, it makes sure the trunk term has been
* selected. We can't allow merging without knowing the explicit trunk term.
*/
function term_merge_duplicates_fieldset_validate($element, &$form_state, $form) {
if (!empty($element['duplicates']['#value']) && !is_numeric($element['trunk_tid']['#value'])) {
form_error($element, t('Please, choose %trunk_tid_label for the group %group_label', array(
'%trunk_tid_label' => $element['trunk_tid']['#title'],
'%group_label' => $element['#title'],
)));
}
}
/**
* Ajax callback function.
*
* Used in term_merge_duplicates_form() to replace the duplicates tables with
* new data per current scaling settings.
*/
function term_merge_duplicates_form_scaling($form, &$form_state) {
return $form['wrapper'];
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
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.
@@ -0,0 +1,67 @@
INTRODUCTION
------------
Module provides actions for management translations of entities using Views Bulk
Operations module.
* For a full description of the module, visit the project page:
http://drupal.org/project/entity_translation_actions
* To submit bug reports and feature suggestions, or to track changes:
http://drupal.org/project/issues/entity_translation_actions
REQUIREMENTS
------------
This module requires the following modules:
* Entity translations (https://drupal.org/project/entity_translation);
* Views Bulk Operations (https://drupal.org/project/views_bulk_operations).
INSTALLATION
------------
Install as usual, see
https://drupal.org/documentation/install/modules-themes/modules-7
for further information.
CONFIGURATION
-------------
Create new view for entities list in Administration » Structure » Views
» Add ( /admin/structure/views/add ).
- Select type of entity. Click "Continue & edit".
- For a new view add field "Bulk operations".
- In group "Selected bulk operations" enable option "Modify entity
translations".
- Add other fields and filters for view.
USAGE
-----
Got to page of created view. Select list of entities and apply operation
"Modify entity translations". On next page select operation from a list.
* Add translation. This operation will copy existed translation and create new
translation for selected language.
* Set translation. This operation will update entity language.
If translation for selected language doesn`t exist - it will be created based
on old entity language.
* Delete translation. This operation will delete translation for selected
language.
If selected language equal to entity language - entity language will be set
to "und".
CONTACT
-------
Current maintainers:
* Adyax (Open Source Experts) - https://drupal.org/marketplace/adyax
* Vitalii Tkachenko (vtkachenko) - https://drupal.org/user/2921049
@@ -0,0 +1,12 @@
name = Entity translation actions
description = Provide actions for entity translation.
core = 7.x
dependencies[] = entity_translation
dependencies[] = views_bulk_operations
; Information added by Drupal.org packaging script on 2014-08-06
version = "7.x-1.0"
core = "7.x"
project = "entity_translation_actions"
datestamp = "1407310128"
@@ -0,0 +1,248 @@
<?php
/**
* @file
* Entity translation actions module.
*/
// Skip any actions if translation for language exists.
define('ENTITY_TRANSLATION_ACTIONS_TRANSLATION_SKIP', 0);
// Clear values from language. Add all values from source language.
define('ENTITY_TRANSLATION_ACTIONS_TRANSLATION_REPLACE', 1);
// Keep existed values for language. Add new values from source language.
define('ENTITY_TRANSLATION_ACTIONS_TRANSLATION_MERGE', 2);
define('ENTITY_TRANSLATION_ACTIONS_RESULT_CREATED', 'SUCCESS CREATED');
define('ENTITY_TRANSLATION_ACTIONS_RESULT_REPLACED', 'SUCCESS REPLACED');
define('ENTITY_TRANSLATION_ACTIONS_RESULT_DELETED', 'SUCCESS DELETED');
define('ENTITY_TRANSLATION_ACTIONS_RESULT_EXISTS', 'WARNING EXISTS');
define('ENTITY_TRANSLATION_ACTIONS_RESULT_EQUAL', 'WARNING EQUAL');
define('ENTITY_TRANSLATION_ACTIONS_RESULT_NO_SOURCE', 'WARNING NO SOURCE');
/**
* Implements hook_views_api().
*/
function entity_translation_actions_views_api($module = NULL, $api = NULL) {
return array('api' => '3.0');
}
/**
* Implements hook_menu().
*/
function entity_translation_actions_menu() {
// Page with list of available views.
$items['admin/content/entity-translations'] = array(
'title' => 'Entity translations',
'description' => 'Manage entity translations.',
'page callback' => 'system_admin_menu_block_page',
'access arguments' => array('administer entity translation'),
'file' => 'system.admin.inc',
'file path' => drupal_get_path('module', 'system'),
);
return $items;
}
/**
* Implements hook_action_info().
*
* Registers custom VBO translation actions as Drupal actions.
*/
function entity_translation_actions_action_info() {
$actions['entity_translation_actions_modify_action'] = array(
'type' => 'entity',
'label' => t('Modify entity translations'),
'behavior' => array('changes_property'),
'configurable' => TRUE,
'vbo_configurable' => TRUE,
'triggers' => array('any'),
);
return $actions;
}
/**
* Implements hook_ctools_plugin_type().
*
* Define ctools plugin type.
*/
function entity_translation_actions_ctools_plugin_type() {
return array(
'et_action' => array(
'use hooks' => FALSE,
'classes' => array('handler'),
),
);
}
/**
* Implements hook_ctools_plugin_directory().
*
* Define plugins with basic translation actions.
*/
function entity_translation_actions_ctools_plugin_directory($owner, $plugin_type) {
switch ($owner) {
case 'entity_translation_actions':
if (in_array($plugin_type, array('et_action'))) {
return 'plugins/' . $plugin_type;
}
break;
}
}
/**
* Function returns plugin instance by it id.
*/
function entity_translation_actions_plugin($id) {
ctools_include('plugins');
$plugin = ctools_get_plugins('entity_translation_actions', 'et_action', $id);
return $plugin;
}
/**
* Function returns all plugin instances.
*/
function entity_translation_actions_plugins() {
ctools_include('plugins');
$plugins = ctools_get_plugins('entity_translation_actions', 'et_action');
// Order list of plugins according to propertie "weight".
uasort($plugins, '_user_sort');
return $plugins;
}
/**
* Action callback function.
*
* Goes through list of entities and apply action of selected handler.
*/
function entity_translation_actions_modify_action($entity, $context) {
$context['action_handler']->action($entity, $context);
}
/**
* Action form function.
*
* Form provide list of available actions. Form rebuilds according to selected
* plugin.
*/
function entity_translation_actions_modify_action_form($context, &$form_state) {
$form = array();
$form_state['entity_type'] = $entity_type = $context['entity_type'];
$settings = isset($form_state['values']['settings']) ? $form_state['values']['settings'] : array(
'action' => FALSE,
);
$form['settings'] = array(
'#tree' => 1,
);
$action_options = array(
'' => t('None'),
);
foreach (entity_translation_actions_plugins() as $plugin_name => $plugin) {
$class = ctools_plugin_get_class($plugin, 'handler');
if (!$class) {
continue;
}
$handler = new $class($entity_type, array());
if ($handler->available()) {
$action_options[$plugin_name] = $plugin['title'];
}
}
$form['settings']['action'] = array(
'#type' => 'radios',
'#title' => t('Select action'),
'#options' => $action_options,
'#default_value' => $settings['action'],
'#required' => TRUE,
'#multiple' => FALSE,
'#ajax' => array(
'callback' => 'entity_translation_actions_modify_action_ajax_callback',
),
);
// Check if any plugin selected.
if (!empty($settings['action'])) {
$settings += array($settings['action'] => array());
// Build plugin handler.
$plugin = entity_translation_actions_plugin($settings['action']);
$class = ctools_plugin_get_class($plugin, 'handler');
$handler = new $class($entity_type, $settings[$settings['action']]);
// Keep handler object during whole process,
$form_state['action_handler'] = $handler;
// Generate custom form for selected plugin.
$form['settings'][$settings['action']] = array(
'#type' => 'fieldset',
'#title' => $action_options[$settings['action']],
'#description' => $plugin['description'],
);
$handler->formBuild($form['settings'][$settings['action']], $form_state);
}
return $form;
}
/**
* Ajax callback for entity_translation_actions_modify_action_form.
*
* Function rebuilds form and returns replace command.
*/
function entity_translation_actions_modify_action_ajax_callback($form, &$form_state) {
$commands[] = ajax_command_replace('form:has(input[value="' . $form['#build_id'] . '"])', render($form));
return array('#type' => 'ajax', '#commands' => $commands);
}
/**
* Validate callback for entity_translation_actions_modify_action_form.
*
* Allow plugin to execute validate function.
*/
function entity_translation_actions_modify_action_validate($form, &$form_state) {
if (isset($form_state['action_handler'])) {
$form_state['action_handler']->formValidate($form, $form_state);
}
}
/**
* Submit callback for entity_translation_actions_modify_action_form.
*
* Allow plugin to execute submit function. Return context for VBO action.
*/
function entity_translation_actions_modify_action_submit($form, $form_state) {
if (isset($form_state['action_handler'])) {
$settings = &$form_state['values']['settings'];
// Name of selected action.
$action = $settings['action'];
// Options for selected action.
$options = isset($settings[$action]) ? $settings[$action] : array();
// Execute plugin specific actions on submit.
$form_state['action_handler']->formSubmit($form, $form_state, $options);
// Return handler object as context.
return array(
'action_handler' => $form_state['action_handler'],
);
}
}
@@ -0,0 +1,350 @@
<?php
/**
* @file
* Declaration of views fb_translate_dashboard.views_default.inc.
*/
/**
* Implements hook_views_default_views().
*/
function entity_translation_actions_views_default_views() {
$export = array();
$view = new view();
$view->name = 'entity_translation_actions_node';
$view->description = '';
$view->tag = 'default';
$view->base_table = 'node';
$view->human_name = 'Entity translation actions: Node';
$view->core = 7;
$view->api_version = '3.0';
$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */
/* Display: Master */
$handler = $view->new_display('default', 'Master', 'default');
$handler->display->display_options['title'] = 'Entity translation actions: Node';
$handler->display->display_options['use_more_always'] = FALSE;
$handler->display->display_options['access']['type'] = 'perm';
$handler->display->display_options['cache']['type'] = 'none';
$handler->display->display_options['query']['type'] = 'views_query';
$handler->display->display_options['exposed_form']['type'] = 'basic';
$handler->display->display_options['pager']['type'] = 'full';
$handler->display->display_options['pager']['options']['items_per_page'] = '50';
$handler->display->display_options['style_plugin'] = 'table';
$handler->display->display_options['style_options']['columns'] = array(
'views_bulk_operations' => 'views_bulk_operations',
'changed_1' => 'changed_1',
'type' => 'type',
'title' => 'title',
'language_1' => 'language_1',
'source' => 'source',
'language' => 'language',
'translate_link' => 'translate_link',
'edit_node' => 'edit_node',
);
$handler->display->display_options['style_options']['default'] = 'changed_1';
$handler->display->display_options['style_options']['info'] = array(
'views_bulk_operations' => array(
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'changed_1' => array(
'sortable' => 1,
'default_sort_order' => 'desc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'type' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'title' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'language_1' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'source' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'language' => array(
'sortable' => 0,
'default_sort_order' => 'asc',
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'translate_link' => array(
'align' => '',
'separator' => '',
'empty_column' => 0,
),
'edit_node' => array(
'align' => '',
'separator' => '',
'empty_column' => 0,
),
);
/* Relationship: Content: Entity translation: translations */
$handler->display->display_options['relationships']['entity_translations']['id'] = 'entity_translations';
$handler->display->display_options['relationships']['entity_translations']['table'] = 'node';
$handler->display->display_options['relationships']['entity_translations']['field'] = 'entity_translations';
/* Field: Bulk operations: Content */
$handler->display->display_options['fields']['views_bulk_operations']['id'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['table'] = 'node';
$handler->display->display_options['fields']['views_bulk_operations']['field'] = 'views_bulk_operations';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['display_type'] = '1';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['enable_select_all_pages'] = 1;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['force_single'] = 0;
$handler->display->display_options['fields']['views_bulk_operations']['vbo_settings']['entity_load_capacity'] = '10';
$handler->display->display_options['fields']['views_bulk_operations']['vbo_operations'] = array(
'action::node_assign_owner_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_delete_item' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_delete_revision' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_script_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_sticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_make_unsticky_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::entity_translation_actions_modify_action' => array(
'selected' => 1,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::views_bulk_operations_modify_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'show_all_tokens' => 1,
'display_values' => array(
'_all_' => '_all_',
),
),
),
'action::views_bulk_operations_argument_selector_action' => array(
'selected' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
'settings' => array(
'url' => '',
),
),
'action::node_promote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_publish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpromote_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_save_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::system_send_email_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
'action::node_unpublish_by_keyword_action' => array(
'selected' => 0,
'postpone_processing' => 0,
'skip_confirmation' => 0,
'override_label' => 0,
'label' => '',
),
);
/* Field: Content: Updated date */
$handler->display->display_options['fields']['changed_1']['id'] = 'changed_1';
$handler->display->display_options['fields']['changed_1']['table'] = 'node';
$handler->display->display_options['fields']['changed_1']['field'] = 'changed';
$handler->display->display_options['fields']['changed_1']['date_format'] = 'raw time ago';
/* Field: Content: Type */
$handler->display->display_options['fields']['type']['id'] = 'type';
$handler->display->display_options['fields']['type']['table'] = 'node';
$handler->display->display_options['fields']['type']['field'] = 'type';
/* Field: Content: Title */
$handler->display->display_options['fields']['title']['id'] = 'title';
$handler->display->display_options['fields']['title']['table'] = 'node';
$handler->display->display_options['fields']['title']['field'] = 'title';
$handler->display->display_options['fields']['title']['alter']['word_boundary'] = FALSE;
$handler->display->display_options['fields']['title']['alter']['ellipsis'] = FALSE;
/* Field: Content: Language */
$handler->display->display_options['fields']['language_1']['id'] = 'language_1';
$handler->display->display_options['fields']['language_1']['table'] = 'node';
$handler->display->display_options['fields']['language_1']['field'] = 'language';
$handler->display->display_options['fields']['language_1']['label'] = 'Node Language';
/* Field: Entity translation: Source */
$handler->display->display_options['fields']['source']['id'] = 'source';
$handler->display->display_options['fields']['source']['table'] = 'entity_translation';
$handler->display->display_options['fields']['source']['field'] = 'source';
$handler->display->display_options['fields']['source']['relationship'] = 'entity_translations';
$handler->display->display_options['fields']['source']['label'] = 'Source Language';
/* Field: Entity translation: Language */
$handler->display->display_options['fields']['language']['id'] = 'language';
$handler->display->display_options['fields']['language']['table'] = 'entity_translation';
$handler->display->display_options['fields']['language']['field'] = 'language';
$handler->display->display_options['fields']['language']['relationship'] = 'entity_translations';
$handler->display->display_options['fields']['language']['label'] = 'Translation Language';
/* Field: Entity translation: Translate link */
$handler->display->display_options['fields']['translate_link']['id'] = 'translate_link';
$handler->display->display_options['fields']['translate_link']['table'] = 'entity_translation';
$handler->display->display_options['fields']['translate_link']['field'] = 'translate_link';
$handler->display->display_options['fields']['translate_link']['relationship'] = 'entity_translations';
/* Field: Content: Edit link */
$handler->display->display_options['fields']['edit_node']['id'] = 'edit_node';
$handler->display->display_options['fields']['edit_node']['table'] = 'views_entity_node';
$handler->display->display_options['fields']['edit_node']['field'] = 'edit_node';
/* Filter criterion: Content: Title */
$handler->display->display_options['filters']['title']['id'] = 'title';
$handler->display->display_options['filters']['title']['table'] = 'node';
$handler->display->display_options['filters']['title']['field'] = 'title';
$handler->display->display_options['filters']['title']['operator'] = 'contains';
$handler->display->display_options['filters']['title']['group'] = 1;
$handler->display->display_options['filters']['title']['exposed'] = TRUE;
$handler->display->display_options['filters']['title']['expose']['operator_id'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['label'] = 'Title';
$handler->display->display_options['filters']['title']['expose']['operator'] = 'title_op';
$handler->display->display_options['filters']['title']['expose']['identifier'] = 'title';
/* Filter criterion: Content: Type */
$handler->display->display_options['filters']['type']['id'] = 'type';
$handler->display->display_options['filters']['type']['table'] = 'node';
$handler->display->display_options['filters']['type']['field'] = 'type';
$handler->display->display_options['filters']['type']['group'] = 1;
$handler->display->display_options['filters']['type']['exposed'] = TRUE;
$handler->display->display_options['filters']['type']['expose']['operator_id'] = 'type_op';
$handler->display->display_options['filters']['type']['expose']['label'] = 'Type';
$handler->display->display_options['filters']['type']['expose']['operator'] = 'type_op';
$handler->display->display_options['filters']['type']['expose']['identifier'] = 'type';
$handler->display->display_options['filters']['type']['expose']['multiple'] = TRUE;
/* Filter criterion: Content: Language */
$handler->display->display_options['filters']['language_1']['id'] = 'language_1';
$handler->display->display_options['filters']['language_1']['table'] = 'node';
$handler->display->display_options['filters']['language_1']['field'] = 'language';
$handler->display->display_options['filters']['language_1']['group'] = 1;
$handler->display->display_options['filters']['language_1']['exposed'] = TRUE;
$handler->display->display_options['filters']['language_1']['expose']['operator_id'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['label'] = 'Node Language';
$handler->display->display_options['filters']['language_1']['expose']['operator'] = 'language_1_op';
$handler->display->display_options['filters']['language_1']['expose']['identifier'] = 'language_1';
/* Filter criterion: Entity translation: Source */
$handler->display->display_options['filters']['source']['id'] = 'source';
$handler->display->display_options['filters']['source']['table'] = 'entity_translation';
$handler->display->display_options['filters']['source']['field'] = 'source';
$handler->display->display_options['filters']['source']['relationship'] = 'entity_translations';
$handler->display->display_options['filters']['source']['group'] = 1;
$handler->display->display_options['filters']['source']['exposed'] = TRUE;
$handler->display->display_options['filters']['source']['expose']['operator_id'] = 'source_op';
$handler->display->display_options['filters']['source']['expose']['label'] = 'Source Language';
$handler->display->display_options['filters']['source']['expose']['operator'] = 'source_op';
$handler->display->display_options['filters']['source']['expose']['identifier'] = 'source';
/* Filter criterion: Entity translation: Language */
$handler->display->display_options['filters']['language']['id'] = 'language';
$handler->display->display_options['filters']['language']['table'] = 'entity_translation';
$handler->display->display_options['filters']['language']['field'] = 'language';
$handler->display->display_options['filters']['language']['relationship'] = 'entity_translations';
$handler->display->display_options['filters']['language']['group'] = 1;
$handler->display->display_options['filters']['language']['exposed'] = TRUE;
$handler->display->display_options['filters']['language']['expose']['operator_id'] = 'language_op';
$handler->display->display_options['filters']['language']['expose']['label'] = 'Translation Language';
$handler->display->display_options['filters']['language']['expose']['operator'] = 'language_op';
$handler->display->display_options['filters']['language']['expose']['identifier'] = 'language';
/* Display: Page */
$handler = $view->new_display('page', 'Page', 'page');
$handler->display->display_options['path'] = 'admin/content/entity-translations/node';
$handler->display->display_options['menu']['type'] = 'normal';
$handler->display->display_options['menu']['title'] = 'Entity translation actions: Node';
$handler->display->display_options['menu']['name'] = 'management';
$export['entity_languages_node'] = $view;
return $export;
}
@@ -0,0 +1,123 @@
<?php
/**
* @file
* Et_action plugin class.
*/
class EntityTranslationActionsAdd extends EntityTranslationActionsBasic {
/**
* Function checks if plugin available for selected entity type.
*/
public function available() {
return entity_translation_enabled($this->entityType);
}
/**
* Function function executes plugin actions.
*/
public function action($entity, $context, $handler = NULL) {
$handler = $handler ? $handler : entity_translation_get_handler($this->entityType, $entity);
$lang_source = ($this->options['source'] == '-entity-') ? $this->entityLanguage($entity, $handler, FALSE) : $this->options['source'];
$language_target = ($this->options['language'] == '-entity-') ? $this->entityLanguage($entity, $handler, FALSE) : $this->options['language'];
if ($lang_source == $language_target) {
return;
}
$translations = $handler->getTranslations();
// No source translation;
if ((!$lang_source || !$translations || !isset($translations->data[$lang_source])) && $lang_source != LANGUAGE_NONE) {
return ENTITY_TRANSLATION_ACTIONS_RESULT_NO_SOURCE;
}
// Target translation exists;
if (isset($translations->data[$language_target]) && $this->options['translation_exists'] == ENTITY_TRANSLATION_ACTIONS_TRANSLATION_SKIP) {
return ENTITY_TRANSLATION_ACTIONS_RESULT_EXISTS;
}
list(,, $bundle_name) = entity_extract_ids($this->entityType, $entity);
// Clone field translations.
foreach (field_info_instances($this->entityType, $bundle_name) as $instance) {
$field_name = $instance['field_name'];
$field = field_info_field($field_name);
if ($field['translatable']) {
$original_item = isset($entity->{$field_name}[$lang_source]) ? $entity->{$field_name}[$lang_source] : NULL;
// MERGE. Add item if not empty and no value for language.
if ($original_item && !isset($entity->{$field_name}[$language_target])) {
$entity->{$field_name}[$language_target] = $original_item;
}
// REPLACE. Replace existing value for language.
// Or remode it if source values is empty.
elseif ($this->options['translation_exists'] == ENTITY_TRANSLATION_ACTIONS_TRANSLATION_REPLACE) {
if ($original_item) {
$entity->{$field_name}[$language_target] = $original_item;
}
else {
unset($entity->{$field_name}[$language_target]);
}
}
}
}
// Create new translation.
if ($language_target != LANGUAGE_NONE) {
$lang_entity = $handler->getLanguage();
$new_translation = isset($translations->data[$lang_source]) ? $translations->data[$lang_source] : array();
$new_translation['status'] = TRUE;
$new_translation['language'] = $language_target;
$new_translation['source'] = ($lang_entity == $language_target) ? FALSE : $lang_entity;
$handler->setTranslation($new_translation);
}
return ENTITY_TRANSLATION_ACTIONS_RESULT_CREATED;
}
/**
* Function builds form elements for action.
*/
public function formBuild(&$form, &$form_state) {
$options = $this->languagesOptions();
$options_keys = array_keys($options);
$form['source'] = array(
'#type' => 'radios',
'#options' => $options,
'#title' => t('Original language:'),
'#required' => TRUE,
'#default_value' => current($options_keys),
);
$form['language'] = array(
'#type' => 'radios',
'#options' => $options,
'#title' => t('New language:'),
'#required' => TRUE,
);
$form['translation_exists'] = array(
'#type' => 'select',
'#options' => $this->translationExistsOptions(),
'#title' => t('Actions for existing translation:'),
);
}
/**
* Function returns options ways for resolving conflicts in translations.
*/
protected function translationExistsOptions() {
return array(
ENTITY_TRANSLATION_ACTIONS_TRANSLATION_SKIP => t('SKIP ANY actions if translation exists'),
ENTITY_TRANSLATION_ACTIONS_TRANSLATION_REPLACE => t('REPLACE ALL existing values with new'),
ENTITY_TRANSLATION_ACTIONS_TRANSLATION_MERGE => t('ADD NEW values if existing translation is empty'),
);
}
}
@@ -0,0 +1,18 @@
<?php
/**
* @file
* Et_action plugin file.
*/
$plugin = array(
'title' => t('Translation Add'),
'description' => t('Copy existing translation for a new language.'),
'handler' => array(
'class' => 'EntityTranslationActionsAdd',
'parent' => 'EntityTranslationActionsBasic',
),
'single' => TRUE,
'icon' => 'no-icon.png',
'weight' => 1,
);

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