, and
+ similar tags in a larger font size, so it is easier to read.
+- Fixed a bug in the Search module that caused exceptions to be thrown during
+ searches if the server was not configured to represent decimal points as a
+ period.
+- Fixed a regression in the Image module that made image_style_url() not work
+ when a relative path (rather than a complete file URI) was passed to it.
+- Added an optional feature to the Statistics module to allow node views to be
+ tracked by Ajax requests rather than during the server-side generation of the
+ page. This allows the node counter to work on sites that use external page
+ caches (string change and new administrative option:
+ https://drupal.org/node/2164069).
+- Added a link to the drupal.org documentation page for cron to the Cron
+ settings page (string change).
+- Added a 'drupal_anonymous_user_object' variable to allow the anonymous user
+ object returned by drupal_anonymous_user() to be overridden with a classed
+ object (API addition).
+- Changed the database API to allow inserts based on a SELECT * query to work
+ correctly.
+- Changed the database schema of the {file_managed} table to allow Drupal to
+ manage files larger than 4 GB.
+- Changed the File module's hook_field_load() implementation to prevent file
+ entity properties which have the same name as file or image field properties
+ from overwriting the field properties (minor API change).
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.24, 2013-11-20
+----------------------
+- Fixed security issues (multiple vulnerabilities), see SA-CORE-2013-003.
+
+Drupal 7.23, 2013-08-07
+-----------------------
+- Fixed a fatal error on PostgreSQL databases when updating the Taxonomy module
+ from Drupal 6 to Drupal 7.
+- Fixed the default ordering of CSS files for sites using right-to-left
+ languages, to consistently place the right-to-left override file immediately
+ after the CSS it is overriding (API change: https://drupal.org/node/2058463).
+- Added a drupal_check_memory_limit() API function to allow the memory limit to
+ be checked consistently (API addition).
+- Changed the default web.config file for IIS servers to allow favicon.ico
+ files which are present in the filesystem to be accessed.
+- Fixed inconsistent support for the 'tel' protocol in Drupal's URL filtering
+ functions.
+- Performance improvement: Allowed all hooks to be included in the
+ module_implements() cache, even those that are only invoked on HTTP POST
+ requests.
+- Made the database system replace truncate queries with delete queries when
+ inside a transaction, to fix issues with PostgreSQL and other databases.
+- Fixed a bug which caused nested contextual links to display improperly.
+- Fixed a bug which prevented cached image derivatives from being flushed for
+ private files and other non-default file schemes.
+- Fixed drupal_render() to always return an empty string when there is no
+ output, rather than sometimes returning NULL (minor API change).
+- Added protection to cache_clear_all() to ensure that non-cache tables cannot
+ be truncated (API addition: a new isValidBin() method has been added to the
+ default database cache implementation).
+- Changed the default .htaccess file to support HTTP authorization in CGI
+ environments.
+- Changed the password reset form to pre-fill the username when requested via a
+ URL query parameter, and used this in the error message that appears after a
+ failed login attempt (minor data structure and behavior change).
+- Fixed broken support for foreign keys in the field API.
+- Fixed "No active batch" error when a user cancels their own account.
+- Added a description to the "access content overview" permission on the
+ permissions page (string change).
+- Added a drupal_array_diff_assoc_recursive() function to allow associative
+ arrays to be compared recursively (API addition).
+- Added human-readable labels to image styles, in addition to the existing
+ machine-readable name (API change: https://drupal.org/node/2058503).
+- Moved the drupal_get_hash_salt() function to bootstrap.inc and used it in
+ additional places in the code, for added security in the case where there is
+ no hash salt in settings.php.
+- Fixed a regression in Drupal 7.22 that caused internal server errors for
+ sites running on very old Apache 1.x web servers.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.22, 2013-04-03
+-----------------------
+- Allowed the drupal_http_request() function to be overridden so that
+ additional HTTP request capabilities can be added by contributed modules.
+- Changed the Simpletest module to allow PSR-0 test classes to be used in
+ Drupal 7.
+- Removed an unnecessary "Content-Disposition" header from private file
+ downloads; it prevented many private files from being viewed inline in a web
+ browser.
+- Changed various field API functions to allow them to optionally act on a
+ single field within an entity (API addition: http://drupal.org/node/1825844).
+- Fixed a bug which prevented Drupal's file transfer functionality from working
+ on some PHP 5.4 systems.
+- Fixed incorrect log message when theme() is called for a theme hook that does
+ not exist (minor string change).
+- Fixed Drupal's token-replacement system to allow spaces in the token value.
+- Changed the default behavior after a user creates a node they do not have
+ access to view. The user will now be redirected to the front page rather than
+ an access denied page.
+- Fixed a bug which prevented empty HTTP headers (such as "0") from being set.
+ (Minor behavior change: Callers of drupal_add_http_header() must now set
+ FALSE explicitly to prevent a header from being sent at all; this was already
+ indicated in the function's documentation.)
+- Fixed OpenID errors when more than one module implements hook_openid(). The
+ behavior is now changed so that if more than one module tries to set the same
+ parameter, the last module's change takes effect.
+- Fixed a serious documentation bug: The $name variable in the
+ taxonomy-term.tpl.php theme template was incorrectly documented as being
+ sanitized when in fact it is not.
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+ duplicate permission names in the User module's database tables.
+- Added an empty "datatype" attribute to taxonomy term and username links to
+ make the RDFa markup upward compatible with RDFa 1.1 (minor markup addition).
+- Fixed a bug which caused the denial-of-service protection added in Drupal
+ 7.20 to break certain valid image URLs that had an extra slash in them.
+- Fixed a bug with update queries in the SQLite database driver that prevented
+ Drupal from being installed with SQLite on PHP 5.4.
+- Fixed enforced dependencies errors updating to recent versions of Drupal 7 on
+ certain non-MySQL databases.
+- Refactored the Field module's caching behavior to obtain large improvements
+ in memory usage for sites with many fields and instances (API addition:
+ http://drupal.org/node/1915646).
+- Fixed entity argument not being passed to implementations of
+ hook_file_download_access_alter(). The fix adds an additional context
+ parameter that can be passed when calling drupal_alter() for any hook (API
+ change: http://drupal.org/node/1882722).
+- Fixed broken support for translatable comment fields (API change:
+ http://drupal.org/node/1874724).
+- Added an assertThemeOutput() method to Simpletest to allow tests to check
+ that themed output matches an expected HTML string (API addition).
+- Added a link to "Install another module" after a module has been successfully
+ downloaded via the Update Manager (UI change).
+- Added an optional "exclusive" flag to installation profile .info files which
+ allows Drupal distributions to force a profile to be selected during
+ installation (API addition: http://drupal.org/node/1961012).
+- Fixed a bug which caused the database API to not properly close database
+ connections.
+- Added a link to the URL for running cron from outside the site to the Cron
+ settings page (UI change).
+- Fixed a bug which prevented image styles from being reverted on PHP 5.4.
+- Made the default .htaccess rules protocol sensitive to improve security for
+ sites which use HTTPS and redirect between "www" and non-"www" versions of
+ the page.
+- Numerous small bug fixes.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.21, 2013-03-06
+-----------------------
+- Allowed sites using the 'image_allow_insecure_derivatives' variable to still
+ have partial protection from the security issues fixed in Drupal 7.20.
+
+Drupal 7.20, 2013-02-20
+-----------------------
+- Fixed security issues (denial of service). See SA-CORE-2013-002.
+
+Drupal 7.19, 2013-01-16
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2013-001.
+
+Drupal 7.18, 2012-12-19
+-----------------------
+- Fixed security issues (multiple vulnerabilities). See SA-CORE-2012-004.
+
+Drupal 7.17, 2012-11-07
+-----------------------
+- Changed the default value of the '404_fast_html' variable to have a DOCTYPE
+ declaration.
+- Made it possible to use associative arrays for the 'items' variable in
+ theme_item_list().
+- Fixed a bug which prevented required form elements without a title from being
+ given an "error" class when the form fails validation.
+- Prevented duplicate HTML IDs from appearing when two forms are displayed on
+ the same page and one of them is submitted with invalid data (minor markup
+ change).
+- Fixed a bug which prevented Drupal 6 to Drupal 7 upgrades on sites which had
+ stale data in the Upload module's database tables.
+- Fixed a bug in the States API which prevented certain types of form elements
+ from being disabled when requested.
+- Allowed aggregator feed items with author names longer than 255 characters to
+ have a truncated version saved to the database (rather than causing a fatal
+ error).
+- Allowed aggregator feed items to have URLs longer than 255 characters
+ (schema change which results in several columns in the Aggregator module's
+ database tables changing from VARCHAR to TEXT fields).
+- Added hook_taxonomy_term_view() and standardized the process for rendering
+ taxonomy terms to invoke hook_entity_view() and otherwise make it consistent
+ with other entities (API change: http://drupal.org/node/1808870).
+- Added hook_entity_view_mode_alter() to allow modules to change entity view
+ modes on display (API addition: http://drupal.org/node/1833086).
+- Fixed a bug which made database queries running a "LIKE" query on blob fields
+ fail on PostgreSQL databases. This caused errors during the Drupal 6 to
+ Drupal 7 upgrade.
+- Changed the hook_menu() entry for Drupal's rss.xml page to prevent extra path
+ components from being accidentally passed to the page callback function (data
+ structure change).
+- Removed a non-standard "name" attribute from Drupal's default Content-Type
+ header for file downloads.
+- Fixed the theme settings form to properly clean up submitted values in
+ $form_state['values'] when the form is submitted (data structure change).
+- Fixed an inconsistency by removing the colon from the end of the label on
+ multi-valued form fields (minor string change).
+- Added support for 'weight' in hook_field_widget_info() to allow modules to
+ control the order in which widgets are displayed in the Field UI.
+- Updated various tables in the OpenID and Book modules to use the default
+ "empty table" text pattern (string change).
+- Added proxy server support to drupal_http_request().
+- Added "lang" attributes to language links, to better support screen readers.
+- Fixed double occurrence of a "ul" HTML tag on secondary local tasks in the
+ Seven theme (markup change).
+- Fixed bugs which caused taxonomy vocabulary and shortcut set titles to be
+ double-escaped. The fix replaces the taxonomy vocabulary overview page and
+ "Edit shortcuts" menu items' title callback entries in hook_menu() with new
+ functions that do not escape HTML characters (data structure change).
+- Modified the Update manager module to allow drupal.org to collect usage
+ statistics for individual modules and themes, rather than only for entire
+ projects.
+- Modified the node listing database query on Drupal's default front page to
+ add table aliases for better query altering (this is a data structure change
+ affecting code which implements hook_query_alter() on this query).
+- Improved the translatability of the "Field type(s) in use" message on the
+ modules page (admin-facing string change).
+- Fixed a regression which caused a "call to undefined function
+ drupal_find_base_themes()" fatal error under rare circumstances.
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.16, 2012-10-17
+-----------------------
+- Fixed security issues (Arbitrary PHP code execution and information
+ disclosure). See SA-CORE-2012-003.
+
+Drupal 7.15, 2012-08-01
+-----------------------
+- Introduced a 'user_password_reset_timeout' variable to allow the 24-hour
+ expiration for user password reset links to be adjusted (API addition).
+- Fixed database errors due to ambiguous column names that occurred when
+ EntityFieldQuery was used in certain situations.
+- Changed the drupal_array_get_nested_value() function to return a reference
+ (API addition).
+- Changed the System module's hook_block_info() implementation to assign the
+ "Main page content" and "System help" blocks to appropriate regions by
+ default and prevent error messages on the block administration page (data
+ structure change).
+- Fixed regression: Non-node entities couldn't be accessed with
+ EntityFieldQuery.
+- Fixed regression: Optional radio buttons with an empty, non-NULL default
+ value led to an illegal choice error when none were selected.
+- Reorganized the testing framework to split setUp() into specific sub-methods
+ and fix several regressions in the process.
+- Fixed bug which made it impossible to search for strings that have not been
+ translated into a particular language.
+- Renamed the "Field" column on the Manage Fields screen to "Field type", since
+ the former was confusing and inaccurate (UI change).
+- Performance improvement: Removed needless call to system_rebuild_module_data()
+ in field_sync_field_status(), greatly speeding up bulk module enable/disable.
+- Fixed bug which prevented notifications from being sent when core, module, and
+ theme updates are available.
+- Fixed bug which prevented sub-themes from inheriting the default values of
+ theme settings defined by the base theme.
+- Fixed bug which prevented the jQuery UI Datepicker from being localized.
+- Made Ajax alert dialogs respect error reporting settings.
+- Fixed bug which prevented image styles from being deleted on PHP 5.4.
+- Fixed bug: Language detection by domain only worked on port 80.
+- Fixed regression: The first plural index on a page was not calculated
+ correctly.
+- Introduced generic entity language support. Entities may now declare their
+ language property in hook_entity_info(), and modules working with entities
+ may access the language using entity_language() (API change:
+ http://drupal.org/node/1626346).
+- Added EntityFieldQuery support for taxonomy bundles.
+- Fixed issue where field form structure was incomplete if field_access()
+ returned FALSE. Instead of being incomplete, the form structure now has
+ #access set to FALSE and field form validation is skipped (data structure
+ change: http://drupal.org/node/1663020).
+- Fixed data loss issue due to field_has_data() returning inconsistent results.
+ The fix adds an optional DANGEROUS_ACCESS_CHECK_OPT_OUT tag to entity field
+ queries which field storage engines can respond to (API addition:
+ http://drupal.org/node/1597378).
+- Fixed notice: Undefined index: default_image in image_field_prepare_view()
+- Numerous API documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.14 2012-05-02
+----------------------
+- Fixed "integrity constraint" fatal errors when rebuilding registry.
+- Fixed custom logo and favicon functionality referencing incorrect paths.
+- Fixed DB Case Sensitivity: Allow BINARY attribute in MySQL.
+- Split field_bundle_settings out per bundle.
+- Improve UX for machine names for fields (UI change).
+- Fixed User pictures are not removed properly.
+- Fixed HTTPS sessions not working in all cases.
+- Fixed Regression: Required radios throw illegal choice error when none
+ selected.
+- Fixed allow autocompletion requests to include slashes.
+- Eliminate $user->cache and {session}.cache in favor of
+ $_SESSION['cache_expiration'][$bin] (Performance).
+- Fixed focus jumps to tab when pressing enter on a form element within tab.
+- Fixed race condition in locale() - duplicates in {locales_source}.
+- Fixed Missing "Default image" per field instance.
+- Quit clobbering people's work when they click the filter tips link
+- Form API #states: Fix conditionals to allow OR and XOR constructions.
+- Fixed Focus jumps to tab when pressing enter on a form element within tab.
+ (Accessibility)
+- Improved performance of node_access queries.
+- Fixed Fieldsets inside vertical tabs have no title and can't be collapsed.
+- Reduce size of cache_menu table (Performance).
+- Fixed unnecessary aggregation of CSS/JS (Performance).
+- Fixed taxonomy_autocomplete() produces SQL error for nonexistent field.
+- Fixed HTML filter is not run first by default, despite default weight.
+- Fixed Overlay does not work with prefixed URL paths.
+- Better debug info for field errors (string change).
+- Fixed Data corruption in comment IDs (results in broken threading on
+ PostgreSQL).
+- Fixed machine name not editable if every character is replaced.
+- Fixed user picture not appearing in comment preview (Markup change).
+- Added optional vid argument for taxonomy_get_term_by_name().
+- Fixed Invalid Unicode code range in PREG_CLASS_UNICODE_WORD_BOUNDARY fails
+ with PCRE 8.30.
+- Fixed {trigger_assignments()}.hook has only 32 characters, is too short.
+- Numerous fixes to run-tests.sh.
+- Fixed Tests in profiles/[name]/modules cannot be run and cannot use a
+ different profile for running tests.
+- Numerous JavaScript performance fixes.
+- Numerous documentation fixes.
+- Fixed All pager links have an 'active' CSS class.
+- Numerous upgrade path fixes; notably:
+ - system_update_7061() fails on inserting files with same name but different
+ case.
+ - system_update_7061() converts filepaths too aggressively.
+ - Trigger upgrade path: Node triggers removed when upgrading to 7-x from 6.25.
+
+Drupal 7.13 2012-05-02
+----------------------
+- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-002.
+
+Drupal 7.12, 2012-02-01
+----------------------
+- Fixed bug preventing custom menus from receiving an active trail.
+- Fixed hook_field_delete() no longer invoked during field_purge_data().
+- Fixed bug causing entity info cache to not be cleared with the rest of caches.
+- Fixed file_unmanaged_copy() fails with Drupal 7.7+ and safe_mode() or
+ open_basedir().
+- Fixed Nested transactions throw exceptions when they got out of scope.
+- Fixed bugs with the Return-Path when sending mail on both Windows and
+ non-Windows systems.
+- Fixed bug with DrupalCacheArray property visibility preventing others from
+ extending it (API change: http://drupal.org/node/1422264).
+- Fixed bug with handling of non-ASCII characters in file names (API change:
+ http://drupal.org/node/1424840).
+- Reconciled field maximum length with database column size in image and
+ aggregator modules.
+- Fixes to various core JavaScript files to allow for minification and
+ aggregation.
+- Fixed Prevent tests from deleting main installation's tables when
+ parent::setUp() is not called.
+- Fixed several Poll module bugs.
+- Fixed several Shortcut module bugs.
+- Added new hook_system_theme_info() to provide ability for contributed modules
+ to test theme functionality.
+- Added ability to cancel mail sending from hook_mail_alter().
+- Added support for configurable PDO connection options, enabling master-master
+ database replication.
+- Numerous improvements to tests and test runner to pave the way for faster test
+ runs.
+- Expanded test coverage.
+- Numerous API documentation improvements.
+- Numerous performance improvements, including token replacement and render
+ cache.
+
+Drupal 7.11, 2012-02-01
+----------------------
+- Fixed security issues (Multiple vulnerabilities), see SA-CORE-2012-001.
+
+Drupal 7.10, 2011-12-05
+----------------------
+- Fixed Content-Language HTTP header to not cause issues with Drush 5.x.
+- Reduce memory usage of theme registry (performance).
+- Fixed PECL upload progress bar for FileField
+- Fixed running update.php doesn't always clear the cache.
+- Fixed PDO exceptions on long titles.
+- Fixed Overlay redirect does not include query string.
+- Fixed D6 modules satisfy D7 module dependencies.
+- Fixed the ordering of module hooks when using module_implements_alter().
+- Fixed "floating" submit buttons during AJAX requests.
+- Fixed timezone selected on install not propogating to admin account.
+- Added msgctx context to JS translation functions, for feature parity with t().
+- Profiles' .install files now available during hook_install_tasks().
+- Added test coverage of 7.0 -> 7.x upgrade path.
+- Numerous notice fixes.
+- Numerous documentation improvements.
+- Additional automated test coverage.
+
+Drupal 7.9, 2011-10-26
+----------------------
+- Critical fixes to OpenID to spec violations that could allow for
+ impersonation in certain scenarios. Existing OpenID users should see
+ http://drupal.org/node/1120290#comment-5092796 for more information on
+ transitioning.
+- Fixed files getting lost when adding multiple files to multiple file fields
+ at the same time.
+- Improved usability of the clean URL test screens.
+- Restored height/width attributes on images run through the theme system.
+- Fixed usability bug with first password field being pre-filled by certain
+ browser plugins.
+- Fixed file_usage_list() so that it can return more than one result.
+- Fixed bug preventing preview of private images on node form.
+- Fixed PDO error when inserting an aggregator title longer than 255 characters.
+- Spelled out what TRADITIONAL means in MySQL sql_mode.
+- Deprecated "!=" operator for DBTNG; should be "<>".
+- Added two new API functions (menu_tree_set_path()/menu_tree_get_path()) were
+ added in order to enable setting the active menu trail for dynamically
+ generated menu paths.
+- Added new "fast 404" capability in settings.php to bypass Drupal bootstrap
+ when serving 404 pages for certain file types.
+- Added format_string() function which can perform string munging ala the t()
+ function without the overhead of the translation system.
+- Numerous #states system fixes.
+- Numerous EntityFieldQuery, DBTNG, and SQLite fixes.
+- Numerous Shortcut module fixes.
+- Numerous language system fixes.
+- Numerous token fixes.
+- Numerous CSS fixes.
+- Numerous upgrade path fixes.
+- Numerous minor string fixes.
+- Numerous notice fixes.
+
+Drupal 7.8, 2011-08-31
+----------------------
+- Fixed critical upgrade path issue with multilingual sites, leading to lost
+ content.
+- Numerous fixes to upgrade path, preventing fatal errors due to incorrect
+ dependencies.
+- Fixed issue with saving files on hosts with open_basedir restrictions.
+- Fixed Update manger error when used with Overlay.
+- Fixed RTL support in Seven administration theme and Overlay.
+- Fixes to nested transaction support.
+- Introduced performance pattern to reduce Drupal core's RAM usage.
+- Added support for HTML 5 tags to filter_xss_admin().
+- Added exception handling to cron.
+- Added new hook hook_field_widget_form_alter() for contribtued modules.
+- element_validate_*() functions now available to contrib.
+- Added new maintainers for several subsystems.
+- Numerous testing system improvements.
+- Numerous markup and CSS fixes.
+- Numerous poll module fixes.
+- Numerous notice/warning fixes.
+- Numerous documentation fixes.
+- Numerous token fixes.
+
+Drupal 7.7, 2011-07-27
+----------------------
+- Fixed VERSION string.
+
+Drupal 7.6, 2011-07-27
+----------------------
+- Fixed support for remote streamwrappers.
+- AJAX now binds to 'click' instead of 'mousedown'.
+- 'Translatable' flag on fields created in UI now defaults to FALSE, to match those created via the API.
+- Performance enhancement to permissions page on large numbers of permissions.
+- More secure password generation.
+- Fix for temporary directory on Windows servers.
+- run-tests.sh now uses proc_open() instead of pcntl_fork() for better Windows support.
+- Numerous upgrade path fixes.
+- Numerous documentation fixes.
+- Numerous notice fixes.
+- Numerous fixes to improve PHP 5.4 support.
+- Numerous RTL improvements.
+
+Drupal 7.5, 2011-07-27
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-003.
+
+Drupal 7.4, 2011-06-29
+----------------------
+- Rolled back patch that caused fatal errors in CTools, Feeds, and other modules using the class registry.
+- Fixed critical bug with saving default images.
+- Fixed fatal errors when uninstalling some modules.
+- Added workaround for MySQL transaction support breaking on DDL statments.
+- Improved page caching with external caching systems.
+- Fix to Batch API, which was terminating too early.
+- Numerous upgrade path fixes.
+- Performance fixes.
+- Additional test coverage.
+- Numerous documentation fixes.
+
+Drupal 7.3, 2011-06-29
+----------------------
+- Fixed security issue (Access bypass), see SA-CORE-2011-002.
+
+Drupal 7.2, 2011-05-25
+----------------------
+- Added a default .gitignore file.
+- Improved PostgreSQL and SQLite support.
+- Numerous critical performance improvements.
+- Numerous critical fixes to the upgrade path.
+- Numerous fixes to language and translation systems.
+- Numerous fixes to AJAX and #states systems.
+- Improvements to the locking system.
+- Numerous documentation fixes.
+- Numerous styling and theme system fixes.
+- Numerous fixes for schema mis-matches between Drupal 6 and 7.
+- Minor internal API clean-ups.
+
+Drupal 7.1, 2011-05-25
+----------------------
+- Fixed security issues (Cross site scripting, File access bypass), see SA-CORE-2011-001.
+
+Drupal 7.0, 2011-01-05
+----------------------
+- Database:
+ * Fully rewritten database layer utilizing PHP 5's PDO abstraction layer.
+ * Drupal now requires MySQL >= 5.0.15 or PostgreSQL >= 8.3.
+ * Added query builders for INSERT, UPDATE, DELETE, MERGE, and SELECT queries.
+ * Support for master/slave replication, transactions, multi-insert queries,
+ and other features.
+ * Added support for the SQLite database engine.
+ * Default to InnoDB engine, rather than MyISAM, on MySQL when available.
+ This offers increased scalability and data integrity.
+- Security:
+ * Protected cron.php -- cron will only run if the proper key is provided.
+ * Implemented a pluggable password system and much stronger password hashes
+ that are compatible with the Portable PHP password hashing framework.
+ * Rate limited login attempts to prevent brute-force password guessing, and
+ improved the flood control API to allow variable time windows and
+ identifiers for limiting user access to resources.
+ * Transformed the "Update status" module into the "Update manager" which
+ can securely install or update modules and themes via a web interface.
+- Usability:
+ * Added contextual links (a.k.a. local tasks) to page elements, such as
+ blocks, nodes, or comments, which allows to perform the most common tasks
+ with a single click only.
+ * Improved installer requirements check.
+ * Improved support for integration of WYSIWYG editors.
+ * Implemented drag-and-drop positioning for input format listings.
+ * Implemented drag-and-drop positioning for language listing.
+ * Implemented drag-and-drop positioning for poll options.
+ * Provided descriptions and human-readable names for user permissions.
+ * Removed comment controls for users.
+ * Removed display order settings for comment module. Comment display
+ order can now be customized using the Views module.
+ * Removed the 'related terms' feature from taxonomy module since this can
+ now be achieved with Field API.
+ * Added additional features to the default installation profile, and
+ implemented a "slimmed down" profile designed for developers.
+ * Added a built-in, automated cron run feature, which is triggered by site
+ visitors.
+ * Added an administrator role which is assigned all permissions for
+ installed modules automatically.
+ * Image toolkits are now provided by modules (rather than requiring a
+ manual file copy to the includes directory).
+ * Added an edit tab to taxonomy term pages.
+ * Redesigned password strength validator.
+ * Redesigned the add content type screen.
+ * Highlight duplicate URL aliases.
+ * Renamed "input formats" to "text formats".
+ * Moved text format permissions to the main permissions page.
+ * Added configurable ability for users to cancel their own accounts.
+ * Added "vertical tabs", a reusable interface component that features
+ automatic summaries and increases usability.
+ * Replaced fieldsets on node edit and add pages with vertical tabs.
+- Performance:
+ * Improved performance on uncached page views by loading multiple core
+ objects in a single database query.
+ * Improved performance for logged-in users by reducing queries for path
+ alias lookups.
+ * Improved support for HTTP proxies (including reverse proxies), allowing
+ anonymous page views to be served entirely from the proxy.
+- Documentation:
+ * Hook API documentation now included in Drupal core.
+- News aggregator:
+ * Added OPML import functionality for RSS feeds.
+ * Optionally, RSS feeds may be configured to not automatically generate feed blocks.
+- Search:
+ * Added support for language-aware searches.
+- Aggregator:
+ * Introduced architecture that allows pluggable parsers and processors for
+ syndicating RSS and Atom feeds.
+ * Added options to suspend updating specific feeds and never discard feeds
+ items.
+- Testing:
+ * Added test framework and tests.
+- Improved time zone support:
+ * Drupal now uses PHP's time zone database when rendering dates in local
+ time. Site-wide and user-configured time zone offsets have been converted
+ to time zone names, e.g. Africa/Abidjan.
+ * In some cases the upgrade and install scripts do not choose the preferred
+ site default time zone. The automatically-selected time zone can be
+ corrected at admin/config/regional/settings.
+ * If your site is being upgraded from Drupal 6 and you do not have the
+ contributed date or event modules installed, user time zone settings will
+ fallback to the system time zone and will have to be reconfigured by each user.
+ * User-configured time zones now serve as the default time zone for PHP
+ date/time functions.
+- Filter system:
+ * Revamped the filter API and text format storage.
+ * Added support for default text formats to be assigned on a per-role basis.
+ * Refactored the HTML corrector to take advantage of PHP 5 features.
+- User system:
+ * Added clean API functions for creating, loading, updating, and deleting
+ user roles and permissions.
+ * Refactored the "access rules" component of user module: The user module
+ now provides a simple interface for blocking single IP addresses. The
+ previous functionality in the user module for restricting certain e-mail
+ addresses and usernames is now available as a contributed module. Further,
+ IP address range blocking is no longer supported and should be implemented
+ at the operating system level.
+ * Removed per-user themes: Contributed modules with similar functionality
+ are available.
+- OpenID:
+ * Added support for Gmail and Google Apps for Domain identifiers. Users can
+ now login with their user@example.com identifier when example.com is powered
+ by Google.
+ * Made the OpenID module more pluggable.
+- Added code registry:
+ * Using the registry, modules declare their includable files via their .info file,
+ allowing Drupal to lazy-load classes and interfaces as needed.
+- Theme system:
+ * Removed the Bluemarine, Chameleon and Pushbutton themes. These themes live
+ on as contributed themes (http://drupal.org/project/bluemarine,
+ http://drupal.org/project/chameleon and http://drupal.org/project/pushbutton).
+ * Added Stark theme to make analyzing Drupal's default HTML and CSS easier.
+ * Added Seven as the default administration theme.
+ * Variable preprocessing of theme hooks prior to template rendering now goes
+ through two phases: a 'preprocess' phase and a new 'process' phase. See
+ http://api.drupal.org/api/function/theme/7 for details.
+ * Theme hooks implemented as functions (rather than as templates) can now
+ also have preprocess (and process) functions. See
+ http://api.drupal.org/api/function/theme/7 for details.
+ * Added Bartik as the default theme.
+- File handling:
+ * Files are now first class Drupal objects with file_load(), file_save(),
+ and file_validate() functions and corresponding hooks.
+ * The file_move(), file_copy() and file_delete() functions now operate on
+ file objects and invoke file hooks so that modules are notified and can
+ respond to changes.
+ * For the occasions when only basic file manipulation are needed--such as
+ uploading a site logo--that don't require the overhead of databases and
+ hooks, the current unmanaged copy, move and delete operations have been
+ preserved but renamed to file_unmanaged_*().
+ * Rewrote file handling to use PHP stream wrappers to enable support for
+ both public and private files and to support pluggable storage mechanisms
+ and access to remote resources (e.g. S3 storage or Flickr photos).
+ * The mime_extension_mapping variable has been removed. Modules that need to
+ alter the default MIME type extension mappings should implement
+ hook_file_mimetype_mapping_alter().
+ * Added the hook_file_url_alter() hook, which makes it possible to serve
+ files from a CDN.
+ * Added a field specifically for uploading files, previously provided by
+ the contributed module FileField.
+- Image handling:
+ * Improved image handling, including better support for add-on image
+ libraries.
+ * Added API and interface for creating advanced image thumbnails.
+ * Inclusion of additional effects such as rotate and desaturate.
+ * Added a field specifically for uploading images, previously provided by
+ the contributed module ImageField.
+- Added aliased multi-site support:
+ * Added support for mapping domain names to sites directories.
+- Added RDF support:
+ * Modules can declare RDF namespaces which are serialized in the tag
+ for RDFa support.
+ * Modules can specify how their data structure maps to RDF.
+ * Added support for RDFa export of nodes, comments, terms, users, etc. and
+ their fields.
+- Search engine optimization and web linking:
+ * Added a rel="canonical" link on node and comment pages to prevent
+ duplicate content indexing by search engines.
+ * Added a default rel="shortlink" link on node and comment pages that
+ advertises a short link as an alternative URL to third-party services.
+ * Meta information is now alterable by all modules before rendering.
+- Field API:
+ * Custom data fields may be attached to nodes, users, comments and taxonomy
+ terms.
+ * Node bodies and teasers are now Field API fields instead of
+ being a hard-coded property of node objects.
+ * In addition, any other object type may register with Field API
+ and allow custom data fields to be attached to itself.
+ * Provides most of the features of the former Content Construction
+ Kit (CCK) module.
+ * Taxonomy terms are now Field API fields that can be added to any fieldable
+ object.
+- Installer:
+ * Refactored the installer into an API that allows Drupal to be installed
+ via a command line script.
+- Page organization
+ * Made the help text area a full featured region with blocks.
+ * Site mission is replaced with the highlighted content block region and
+ separate RSS feed description settings.
+ * The footer message setting was removed in favor of custom blocks.
+ * Made the main page content a block which can be moved and ordered
+ with other blocks in the same region.
+ * Blocks can now return structured arrays for later rendering just
+ like page callbacks.
+- Translation system
+ * The translation system now supports message context (msgctxt).
+ * Added support for translatable fields to Field API.
+- JavaScript changes
+ * Upgraded the core JavaScript library to jQuery version 1.4.4.
+ * Upgraded the jQuery Forms library to 2.52.
+ * Added jQuery UI 1.8.7, which allows improvements to Drupal's user
+ experience.
+- Better module version support
+ * Modules now can specify which version of another module they depend on.
+- Removed modules from core
+ * The following modules have been removed from core, because contributed
+ modules with similar functionality are available:
+ * Blog API module
+ * Ping module
+ * Throttle module
+- Improved node access control system.
+ * All modules may now influence the access to a node at runtime, not just
+ the module that defined a node.
+ * Users may now be allowed to bypass node access restrictions without giving
+ them complete access to the site.
+ * Access control affects both published and unpublished nodes.
+ * Numerous other improvements to the node access system.
+- Actions system
+ * Simplified definitions of actions and triggers.
+ * Removed dependency on the combination of hooks and operations. Triggers
+ now directly map to module hooks.
+- Task handling
+ * Added a queue API to process many or long-running tasks.
+ * Added queue API support to cron API.
+ * Added a locking framework to coordinate long-running operations across
+ requests.
+
+Drupal 6.23-dev, xxxx-xx-xx (development release)
+-----------------------
+
+Drupal 6.22, 2011-05-25
+-----------------------
+- Made Drupal 6 work better with IIS and Internet Explorer.
+- Fixed .po file imports to work better with custom textgroups.
+- Improved code documentation at various places.
+- Fixed a variety of other bugs.
+
+Drupal 6.21, 2011-05-25
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2011-001.
+
+Drupal 6.20, 2010-12-15
+----------------------
+- Fixed a variety of small bugs, improved code documentation.
+
+Drupal 6.19, 2010-08-11
+----------------------
+- Fixed a variety of small bugs, improved code documentation.
+
+Drupal 6.18, 2010-08-11
+----------------------
+- Fixed security issues (OpenID authentication bypass, File download access
+ bypass, Comment unpublishing bypass, Actions cross site scripting),
+ see SA-CORE-2010-002.
+
+Drupal 6.17, 2010-06-02
+----------------------
+- Improved PostgreSQL compatibility
+- Better PHP 5.3 and PHP 4 compatibility
+- Better browser compatibility of CSS and JS aggregation
+- Improved logging for login failures
+- Fixed an incompatibility with some contributed modules and the locking system
+- Fixed a variety of other bugs.
+
+Drupal 6.16, 2010-03-03
+----------------------
+- Fixed security issues (Installation cross site scripting, Open redirection,
+ Locale module cross site scripting, Blocked user session regeneration),
+ see SA-CORE-2010-001.
+- Better support for updated jQuery versions.
+- Reduced resource usage of update.module.
+- Fixed several issues relating to support of installation profiles and
+ distributions.
+- Added a locking framework to avoid data corruption on long operations.
+- Fixed a variety of other bugs.
+
+Drupal 6.15, 2009-12-16
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-009.
+- Fixed a variety of other bugs.
+
+Drupal 6.14, 2009-09-16
+----------------------
+- Fixed security issues (OpenID association cross site request forgeries,
+ OpenID impersonation and File upload), see SA-CORE-2009-008.
+- Changed the system modules page to not run all cache rebuilds; use the
+ button on the performance settings page to achieve the same effect.
+- Added support for PHP 5.3.0 out of the box.
+- Fixed a variety of small bugs.
+
+Drupal 6.13, 2009-07-01
+----------------------
+- Fixed security issues (Cross site scripting, Input format access bypass and
+ Password leakage in URL), see SA-CORE-2009-007.
+- Fixed a variety of small bugs.
+
+Drupal 6.12, 2009-05-13
+----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
+- Fixed a variety of small bugs.
+
+Drupal 6.11, 2009-04-29
+----------------------
+- Fixed security issues (Cross site scripting and limited information
+ disclosure), see SA-CORE-2009-005
+- Fixed performance issues with the menu router cache, the update
+ status cache and improved cache invalidation
+- Fixed a variety of small bugs.
+
+Drupal 6.10, 2009-02-25
+----------------------
+- Fixed a security issue, (Local file inclusion on Windows),
+ see SA-CORE-2009-003
+- Fixed node_feed() so custom fields can show up in RSS feeds.
+- Improved PostgreSQL compatibility.
+- Fixed a variety of small bugs.
+
+Drupal 6.9, 2009-01-14
+----------------------
+- Fixed security issues, (Access Bypass, Validation Bypass and Hardening
+ against SQL injection), see SA-CORE-2009-001
+- Made HTTP request checking more robust and informative.
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and
+ basic shell scripts.
+- Removed t() calls from all schema documentation. Suggested best practice
+ changed for contributed modules, see http://drupal.org/node/322731.
+- Fixed a variety of small bugs.
+
+Drupal 6.8, 2008-12-11
+----------------------
+- Removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 6.7, 2008-12-10
+----------------------
+- Fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- Updated robots.txt and .htaccess to match current file use.
+- Fixed a variety of small bugs.
+
+Drupal 6.6, 2008-10-22
+----------------------
+- Fixed security issues, (File inclusion, Cross site scripting), see SA-2008-067
+- Fixed a variety of small bugs.
+
+Drupal 6.5, 2008-10-08
+----------------------
+- Fixed security issues, (File upload access bypass, Access rules bypass,
+ BlogAPI access bypass), see SA-2008-060.
+- Fixed a variety of small bugs.
+
+Drupal 6.4, 2008-08-13
+----------------------
+- Fixed a security issue (Cross site scripting, Arbitrary file uploads via
+ BlogAPI, Cross site request forgeries and Various Upload module
+ vulnerabilities), see SA-2008-047.
+- Improved error messages during installation.
+- Fixed a bug that prevented AHAH handlers to be attached to radios widgets.
+- Fixed a variety of small bugs.
+
+Drupal 6.3, 2008-07-09
+----------------------
+- Fixed security issues, (Cross site scripting, cross site request forgery,
+ session fixation and SQL injection), see SA-2008-044.
+- Slightly modified installation process to prevent file ownership issues on
+ shared hosts.
+- Improved PostgreSQL compatibility (rewritten queries; custom blocks).
+- Upgraded to jQuery 1.2.6.
+- Performance improvements to search, menu handling and form API caches.
+- Fixed Views compatibility issues (Views for Drupal 6 requires Drupal 6.3+).
+- Fixed a variety of small bugs.
+
+Drupal 6.2, 2008-04-09
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Access bypasses), see SA-2008-026.
+
+Drupal 6.1, 2008-02-27
+----------------------
+- Fixed a variety of small bugs.
+- Fixed a security issue (Cross site scripting), see SA-2008-018.
+
+Drupal 6.0, 2008-02-13
+----------------------
+- New, faster and better menu system.
+- New watchdog as a hook functionality.
+ * New hook_watchdog that can be implemented by any module to route log
+ messages to various destinations.
+ * Expands the severity levels from 3 (Error, Warning, Notice) to the 8
+ levels defined in RFC 3164.
+ * The watchdog module is now called dblog, and is optional, but enabled by
+ default in the default installation profile.
+ * Extended the database log module so log messages can be filtered.
+ * Added syslog module: useful for monitoring large Drupal installations.
+- Added optional e-mail notifications when users are approved, blocked, or
+ deleted.
+- Drupal works with error reporting set to E_ALL.
+- Added scripts/drupal.sh to execute Drupal code from the command line. Useful
+ to use Drupal as a framework to build command-line tools.
+- Made signature support optional and made it possible to theme signatures.
+- Made it possible to filter the URL aliases on the URL alias administration
+ screen.
+- Language system improvements:
+ * Support for right to left languages.
+ * Language detection based on parts of the URL.
+ * Browser based language detection.
+ * Made it possible to specify a node's language.
+ * Support for translating posts on the site to different languages.
+ * Language dependent path aliases.
+ * Automatically import translations when adding a new language.
+ * JavaScript interface translation.
+ * Automatically import a module's translation upon enabling that module.
+- Moved "PHP input filter" to a standalone module so it can be deleted for
+ security reasons.
+- Usability:
+ * Improved handling of teasers in posts.
+ * Added sticky table headers.
+ * Check for clean URL support automatically with JavaScript.
+ * Removed default/settings.php. Instead the installer will create it from
+ default.settings.php.
+ * Made it possible to configure your own date formats.
+ * Remember anonymous comment posters.
+ * Only allow modules and themes to be enabled that have explicitly been
+ ported to the correct core API version.
+ * Can now specify the minimum PHP version required for a module within the
+ .info file.
+ * Drupal core no longer requires CREATE TEMPORARY TABLES or LOCK TABLES
+ database rights.
+ * Dynamically check password strength and confirmation.
+ * Refactored poll administration.
+ * Implemented drag-and-drop positioning for blocks, menu items, taxonomy
+ vocabularies and terms, forums, profile fields, and input format filters.
+- Theme system:
+ * Added .info files to themes and made it easier to specify regions and
+ features.
+ * Added theme registry: modules can directly provide .tpl.php files for
+ their themes without having to create theme_ functions.
+ * Used the Garland theme for the installation and maintenance pages.
+ * Added theme preprocess functions for themes that are templates.
+ * Added support for themeable functions in JavaScript.
+- Refactored update.php to a generic batch API to be able to run time-consuming
+ operations in multiple subsequent HTTP requests.
+- Installer:
+ * Themed the installer with the Garland theme.
+ * Added form to provide initial site information during installation.
+ * Added ability to provide extra installation steps programmatically.
+ * Made it possible to import interface translations during installation.
+- Added the HTML corrector filter:
+ * Fixes faulty and chopped off HTML in postings.
+ * Tags are now automatically closed at the end of the teaser.
+- Performance:
+ * Made it easier to conditionally load .include files and split up many core
+ modules.
+ * Added a JavaScript aggregator.
+ * Added block-level caching, improving performance for both authenticated
+ and anonymous users.
+ * Made Drupal work correctly when running behind a reverse proxy like
+ Squid or Pound.
+- File handling improvements:
+ * Entries in the files table are now keyed to a user instead of a node.
+ * Added reusable validation functions to check for uploaded file sizes,
+ extensions, and image resolution.
+ * Added ability to create and remove temporary files during a cron job.
+- Forum improvements:
+ * Any node type may now be posted in a forum.
+- Taxonomy improvements:
+ * Descriptions for terms are now shown on taxonomy/term pages as well
+ as RSS feeds.
+ * Added versioning support to categories by associating them with node
+ revisions.
+- Added support for OpenID.
+- Added support for triggering configurable actions.
+- Added the Update status module to automatically check for available updates
+ and warn sites if they are missing security updates or newer versions.
+ Sites deploying from CVS should use http://drupal.org/project/cvs_deploy.
+ Advanced settings provided by http://drupal.org/project/update_advanced.
+- Upgraded the core JavaScript library to jQuery version 1.2.3.
+- Added a new Schema API, which provides built-in support for core and
+ contributed modules to work with databases other than MySQL.
+- Removed drupal.module. The functionality lives on as the Site network
+ contributed module (http://drupal.org/project/site_network).
+- Removed old system updates. Updates from Drupal versions prior to 5.x will
+ require upgrading to 5.x before upgrading to 6.x.
+
+Drupal 5.23, 2010-08-11
+-----------------------
+- Fixed security issues (File download access bypass, Comment unpublishing
+ bypass), see SA-CORE-2010-002.
+
+Drupal 5.22, 2010-03-03
+-----------------------
+- Fixed security issues (Open redirection, Locale module cross site scripting,
+ Blocked user session regeneration), see SA-CORE-2010-001.
+
+Drupal 5.21, 2009-12-16
+-----------------------
+- Fixed a security issue (Cross site scripting), see SA-CORE-2009-009.
+- Fixed a variety of small bugs.
+
+Drupal 5.20, 2009-09-16
+-----------------------
+- Avoid security problems resulting from writing Drupal 6-style menu
+ declarations.
+- Fixed security issues (session fixation), see SA-CORE-2009-008.
+- Fixed a variety of small bugs.
+
+Drupal 5.19, 2009-07-01
+-----------------------
+- Fixed security issues (Cross site scripting and Password leakage in URL), see
+ SA-CORE-2009-007.
+- Fixed a variety of small bugs.
+
+Drupal 5.18, 2009-05-13
+-----------------------
+- Fixed security issues (Cross site scripting), see SA-CORE-2009-006.
+- Fixed a variety of small bugs.
+
+Drupal 5.17, 2009-04-29
+-----------------------
+- Fixed security issues (Cross site scripting and limited information
+ disclosure) see SA-CORE-2009-005.
+- Fixed a variety of small bugs.
+
+Drupal 5.16, 2009-02-25
+-----------------------
+- Fixed a security issue, (Local file inclusion on Windows), see SA-CORE-2009-004.
+- Fixed a variety of small bugs.
+
+Drupal 5.15, 2009-01-14
+-----------------------
+- Fixed security issues, (Hardening against SQL injection), see
+ SA-CORE-2009-001
+- Fixed HTTP_HOST checking to work again with HTTP 1.0 clients and basic shell
+ scripts.
+- Fixed a variety of small bugs.
+
+Drupal 5.14, 2008-12-11
+-----------------------
+- removed a previous change incompatible with PHP 5.1.x and lower.
+
+Drupal 5.13, 2008-12-10
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site request forgery and Cross site scripting), see SA-2008-073
+- updated robots.txt and .htaccess to match current file use.
+
+Drupal 5.12, 2008-10-22
+-----------------------
+- fixed security issues, (File inclusion), see SA-2008-067
+
+Drupal 5.11, 2008-10-08
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (File upload access bypass, Access rules bypass,
+ BlogAPI access bypass, Node validation bypass), see SA-2008-060
+
+Drupal 5.10, 2008-08-13
+-----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, Arbitrary file uploads via
+ BlogAPI and Cross site request forgery), see SA-2008-047
+
+Drupal 5.9, 2008-07-23
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Session fixation), see SA-2008-046
+
+Drupal 5.8, 2008-07-09
+----------------------
+- fixed a variety of small bugs.
+- fixed security issues, (Cross site scripting, cross site request forgery, and
+ session fixation), see SA-2008-044
+
+Drupal 5.7, 2008-01-28
+----------------------
+- fixed the input format configuration page.
+- fixed a variety of small bugs.
+
+Drupal 5.6, 2008-01-10
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (Cross site request forgery), see SA-2008-005
+- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
+- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
+
+Drupal 5.5, 2007-12-06
+----------------------
+- fixed missing missing brackets in a query in the user module.
+- fixed taxonomy feed bug introduced by SA-2007-031
+
+Drupal 5.4, 2007-12-05
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (SQL injection), see SA-2007-031
+
+Drupal 5.3, 2007-10-17
+----------------------
+- fixed a variety of small bugs.
+- fixed a security issue (HTTP response splitting), see SA-2007-024
+- fixed a security issue (Arbitrary code execution via installer), see SA-2007-025
+- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
+- fixed a security issue (User deletion cross site request forgery), see SA-2007-029
+- fixed a security issue (API handling of unpublished comment), see SA-2007-030
+
+Drupal 5.2, 2007-07-26
+----------------------
+- changed hook_link() $teaser argument to match documentation.
+- fixed a variety of small bugs.
+- fixed a security issue (cross-site request forgery), see SA-2007-017
+- fixed a security issue (cross-site scripting), see SA-2007-018
+
+Drupal 5.1, 2007-01-29
+----------------------
+- fixed security issue (code execution), see SA-2007-005
+- fixed a variety of small bugs.
+
+Drupal 5.0, 2007-01-15
+----------------------
+- Completely retooled the administration page
+ * /Admin now contains an administration page which may be themed
+ * Reorganised administration menu items by task and by module
+ * Added a status report page with detailed PHP/MySQL/Drupal information
+- Added web-based installer which can:
+ * Check installation and run-time requirements
+ * Automatically generate the database configuration file
+ * Install pre-made installation profiles or distributions
+ * Import the database structure with automatic table prefixing
+ * Be localized
+- Added new default Garland theme
+- Added color module to change some themes' color schemes
+- Included the jQuery JavaScript library 1.0.4 and converted all core JavaScript to use it
+- Introduced the ability to alter mail sent from system
+- Module system:
+ * Added .info files for module meta-data
+ * Added support for module dependencies
+ * Improved module installation screen
+ * Moved core modules to their own directories
+ * Added support for module uninstalling
+- Added support for different cache backends
+- Added support for a generic "sites/all" directory.
+- Usability:
+ * Added support for auto-complete forms (AJAX) to user profiles.
+ * Made it possible to instantly assign roles to newly created user accounts.
+ * Improved configurability of the contact forms.
+ * Reorganized the settings pages.
+ * Made it easy to investigate popular search terms.
+ * Added a 'select all' checkbox and a range select feature to administration tables.
+ * Simplified the 'break' tag to split teasers from body.
+ * Use proper capitalization for titles, menu items and operations.
+- Integrated urlfilter.module into filter.module
+- Block system:
+ * Extended the block visibility settings with a role specific setting.
+ * Made it possible to customize all block titles.
+- Poll module:
+ * Optionally allow people to inspect all votes.
+ * Optionally allow people to cancel their vote.
+- Distributed authentication:
+ * Added default server option.
+- Added default robots.txt to control crawlers.
+- Database API:
+ * Added db_table_exists().
+- Blogapi module:
+ * 'Blogapi new' and 'blogapi edit' nodeapi operations.
+- User module:
+ * Added hook_profile_alter().
+ * E-mail verification is made optional.
+ * Added mass editing and filtering on admin/user/user.
+- PHP Template engine:
+ * Add the ability to look for a series of suggested templates.
+ * Look for page templates based upon the path.
+ * Look for block templates based upon the region, module, and delta.
+- Content system:
+ * Made it easier for node access modules to work well with each other.
+ * Added configurable content types.
+ * Changed node rendering to work with structured arrays.
+- Performance:
+ * Improved session handling: reduces database overhead.
+ * Improved access checking: reduces database overhead.
+ * Made it possible to do memcached based session management.
+ * Omit sidebars when serving a '404 - Page not found': saves CPU cycles and bandwidth.
+ * Added an 'aggressive' caching policy.
+ * Added a CSS aggregator and compressor (up to 40% faster page loads).
+- Removed the archive module.
+- Upgrade system:
+ * Created space for update branches.
+- Form API:
+ * Made it possible to programmatically submit forms.
+ * Improved api for multistep forms.
+- Theme system:
+ * Split up and removed drupal.css.
+ * Added nested lists generation.
+ * Added a self-clearing block class.
+
+Drupal 4.7.11, 2008-01-10
+-------------------------
+- fixed a security issue (Cross site request forgery), see SA-2008-005
+- fixed a security issue (Cross site scripting, UTF8), see SA-2008-006
+- fixed a security issue (Cross site scripting, register_globals), see SA-2008-007
+
+Drupal 4.7.10, 2007-12-06
+-------------------------
+- fixed taxonomy feed bug introduced by SA-2007-031
+
+Drupal 4.7.9, 2007-12-05
+------------------------
+- fixed a security issue (SQL injection), see SA-2007-031
+
+Drupal 4.7.8, 2007-10-17
+----------------------
+- fixed a security issue (HTTP response splitting), see SA-2007-024
+- fixed a security issue (Cross site scripting via uploads), see SA-2007-026
+- fixed a security issue (API handling of unpublished comment), see SA-2007-030
+
+Drupal 4.7.7, 2007-07-26
+------------------------
+- fixed security issue (XSS), see SA-2007-018
+
+Drupal 4.7.6, 2007-01-29
+------------------------
+- fixed security issue (code execution), see SA-2007-005
+
+Drupal 4.7.5, 2007-01-05
+------------------------
+- Fixed security issue (XSS), see SA-2007-001
+- Fixed security issue (DoS), see SA-2007-002
+
+Drupal 4.7.4, 2006-10-18
+------------------------
+- Fixed security issue (XSS), see SA-2006-024
+- Fixed security issue (CSRF), see SA-2006-025
+- Fixed security issue (Form action attribute injection), see SA-2006-026
+
+Drupal 4.7.3, 2006-08-02
+------------------------
+- Fixed security issue (XSS), see SA-2006-011
+
+Drupal 4.7.2, 2006-06-01
+------------------------
+- Fixed critical upload issue, see SA-2006-007
+- Fixed taxonomy XSS issue, see SA-2006-008
+- Fixed a variety of small bugs.
+
+Drupal 4.7.1, 2006-05-24
+------------------------
+- Fixed critical SQL issue, see SA-2006-005
+- Fixed a serious upgrade related bug.
+- Fixed a variety of small bugs.
+
+Drupal 4.7.0, 2006-05-01
+------------------------
+- Added free tagging support.
+- Added a site-wide contact form.
+- Theme system:
+ * Added the PHPTemplate theme engine and removed the Xtemplate engine.
+ * Converted the bluemarine theme from XTemplate to PHPTemplate.
+ * Converted the pushbutton theme from XTemplate to PHPTemplate.
+- Usability:
+ * Reworked the 'request new password' functionality.
+ * Reworked the node and comment edit forms.
+ * Made it easy to add nodes to the navigation menu.
+ * Added site 'offline for maintenance' feature.
+ * Added support for auto-complete forms (AJAX).
+ * Added support for collapsible page sections (JS).
+ * Added support for resizable text fields (JS).
+ * Improved file upload functionality (AJAX).
+ * Reorganized some settings pages.
+ * Added friendly database error screens.
+ * Improved styling of update.php.
+- Refactored the forms API.
+ * Made it possible to alter, extend or theme forms.
+- Comment system:
+ * Added support for "mass comment operations" to ease repetitive tasks.
+ * Comment moderation has been removed.
+- Node system:
+ * Reworked the revision functionality.
+ * Removed the bookmarklet code. Third-party modules can now handle
+ This.
+- Upgrade system:
+ * Allows contributed modules to plug into the upgrade system.
+- Profiles:
+ * Added a block to display author information along with posts.
+ * Added support for private profile fields.
+- Statistics module:
+ * Added the ability to track page generation times.
+ * Made it possible to block certain IPs/hostnames.
+- Block system:
+ * Added support for theme-specific block regions.
+- Syndication:
+ * Made the aggregator module parse Atom feeds.
+ * Made the aggregator generate RSS feeds.
+ * Added RSS feed settings.
+- XML-RPC:
+ * Replaced the XML-RPC library by a better one.
+- Performance:
+ * Added 'loose caching' option for high-traffic sites.
+ * Improved performance of path aliasing.
+ * Added the ability to track page generation times.
+- Internationalization:
+ * Improved Unicode string handling API.
+ * Added support for PHP's multibyte string module.
+- Added support for PHP5's 'mysqli' extension.
+- Search module:
+ * Made indexer smarter and more robust.
+ * Added advanced search operators (e.g. phrase, node type, ...).
+ * Added customizable result ranking.
+- PostgreSQL support:
+ * Removed dependency on PL/pgSQL procedural language.
+- Menu system:
+ * Added support for external URLs.
+- Queue module:
+ * Removed from core.
+- HTTP handling:
+ * Added support for a tolerant Base URL.
+ * Output URIs relative to the root, without a base tag.
+
+Drupal 4.6.11, 2007-01-05
+-------------------------
+- Fixed security issue (XSS), see SA-2007-001
+- Fixed security issue (DoS), see SA-2007-002
+
+Drupal 4.6.10, 2006-10-18
+------------------------
+- Fixed security issue (XSS), see SA-2006-024
+- Fixed security issue (CSRF), see SA-2006-025
+- Fixed security issue (Form action attribute injection), see SA-2006-026
+
+Drupal 4.6.9, 2006-08-02
+------------------------
+- Fixed security issue (XSS), see SA-2006-011
+
+Drupal 4.6.8, 2006-06-01
+------------------------
+- Fixed critical upload issue, see SA-2006-007
+- Fixed taxonomy XSS issue, see SA-2006-008
+
+Drupal 4.6.7, 2006-05-24
+------------------------
+- Fixed critical SQL issue, see SA-2006-005
+
+Drupal 4.6.6, 2006-03-13
+------------------------
+- Fixed bugs, including 4 security vulnerabilities.
+
+Drupal 4.6.5, 2005-12-12
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.6.4, 2005-11-30
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.6.3, 2005-08-15
+------------------------
+- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
+
+Drupal 4.6.2, 2005-06-29
+------------------------
+- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
+
+Drupal 4.6.1, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.6.0, 2005-04-15
+------------------------
+- PHP5 compliance
+- Search:
+ * Added UTF-8 support to make it work with all languages.
+ * Improved search indexing algorithm.
+ * Improved search output.
+ * Impose a throttle on indexing of large sites.
+ * Added search block.
+- Syndication:
+ * Made the ping module ping pingomatic.com which, in turn, will ping all the major ping services.
+ * Made Drupal generate RSS 2.0 feeds.
+ * Made RSS feeds extensible.
+ * Added categories to RSS feeds.
+ * Added enclosures to RSS feeds.
+- Flood control mechanism:
+ * Added a mechanism to throttle certain operations.
+- Usability:
+ * Refactored the block configuration pages.
+ * Refactored the statistics pages.
+ * Refactored the watchdog pages.
+ * Refactored the throttle module configuration.
+ * Refactored the access rules page.
+ * Refactored the content administration page.
+ * Introduced forum configuration pages.
+ * Added a 'add child page' link to book pages.
+- Contact module:
+ * Added a simple contact module that allows users to contact each other using e-mail.
+- Multi-site configuration:
+ * Made it possible to run multiple sites from a single code base.
+- Added an image API: enables better image handling.
+- Block system:
+ * Extended the block visibility settings.
+- Theme system:
+ * Added new theme functions.
+- Database backend:
+ * The PEAR database backend is no longer supported.
+- Performance:
+ * Improved performance of the forum topics block.
+ * Improved performance of the tracker module.
+ * Improved performance of the node pages.
+- Documentation:
+ * Improved and extended PHPDoc/Doxygen comments.
+
+Drupal 4.5.8, 2006-03-13
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.5.7, 2005-12-12
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.5.6, 2005-11-30
+------------------------
+- Fixed bugs, including 3 security vulnerabilities.
+
+Drupal 4.5.5, 2005-08-15
+------------------------
+- Fixed bugs, including a critical "arbitrary PHP code execution" bug.
+
+Drupal 4.5.4, 2005-06-29
+------------------------
+- Fixed bugs, including two critical "arbitrary PHP code execution" bugs.
+
+Drupal 4.5.3, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.5.2, 2005-01-15
+------------------------
+- Fixed bugs: a cross-site scripting (XSS) vulnerability has been fixed.
+
+Drupal 4.5.1, 2004-12-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.5.0, 2004-10-18
+------------------------
+- Navigation:
+ * Made it possible to add, delete, rename and move menu items.
+ * Introduced tabs and subtabs for local tasks.
+ * Reorganized the navigation menus.
+- User management:
+ * Added support for multiple roles per user.
+ * Made it possible to add custom profile fields.
+ * Made it possible to browse user profiles by field.
+- Node system:
+ * Added support for node-level permissions.
+- Comment module:
+ * Made it possible to leave contact information without having to register.
+- Upload module:
+ * Added support for uploading documents (includes images).
+- Forum module:
+ * Added support for sticky forum topics.
+ * Made it possible to track forum topics.
+- Syndication:
+ * Added support for RSS ping-notifications of http://technorati.com/.
+ * Refactored the categorization of syndicated news items.
+ * Added an URL alias for 'rss.xml'.
+ * Improved date parsing.
+- Database backend:
+ * Added support for multiple database connections.
+ * The PostgreSQL backend does no longer require PEAR.
+- Theme system:
+ * Changed all GIFs to PNGs.
+ * Reorganised the handling of themes, template engines, templates and styles.
+ * Unified and extended the available theme settings.
+ * Added theme screenshots.
+- Blocks:
+ * Added 'recent comments' block.
+ * Added 'categories' block.
+- Blogger API:
+ * Added support for auto-discovery of blogger API via RSD.
+- Performance:
+ * Added support for sending gzip compressed pages.
+ * Improved performance of the forum module.
+- Accessibility:
+ * Improved the accessibility of the archive module's calendar.
+ * Improved form handling and error reporting.
+ * Added HTTP redirects to prevent submitting twice when refreshing right after a form submission.
+- Refactored 403 (forbidden) handling and added support for custom 403 pages.
+- Documentation:
+ * Added PHPDoc/Doxygen comments.
+- Filter system:
+ * Added support for using multiple input formats on the site
+ * Expanded the embedded PHP-code feature so it can be used everywhere
+ * Added support for role-dependent filtering, through input formats
+- UI translation:
+ * Managing translations is now completely done through the administration interface
+ * Added support for importing/exporting gettext .po files
+
+Drupal 4.4.3, 2005-06-01
+------------------------
+- Fixed bugs, including a critical input validation bug.
+
+Drupal 4.4.2, 2004-07-04
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.4.1, 2004-05-01
+------------------------
+- Fixed bugs: no critical bugs were identified.
+
+Drupal 4.4.0, 2004-04-01
+------------------------
+- Added support for the MetaWeblog API and MovableType extensions.
+- Added a file API: enables better document management.
+- Improved the watchdog and search module to log search keys.
+- News aggregator:
+ * Added support for conditional GET.
+ * Added OPML feed subscription list.
+ * Added support for , , , , and .
+- Comment module:
+ * Made it possible to disable the "comment viewing controls".
+- Performance:
+ * Improved module loading when serving cached pages.
+ * Made it possible to automatically disable modules when under heavy load.
+ * Made it possible to automatically disable blocks when under heavy load.
+ * Improved performance and memory footprint of the locale module.
+- Theme system:
+ * Made all theme functions start with 'theme_'.
+ * Made all theme functions return their output.
+ * Migrated away from using the BaseTheme class.
+ * Added many new theme functions and refactored existing theme functions.
+ * Added avatar support to 'Xtemplate'.
+ * Replaced theme 'UnConeD' by 'Chameleon'.
+ * Replaced theme 'Marvin' by 'Pushbutton'.
+- Usability:
+ * Added breadcrumb navigation to all pages.
+ * Made it possible to add context-sensitive help to all pages.
+ * Replaced drop-down menus by radio buttons where appropriate.
+ * Removed the 'magic_quotes_gpc = 0' requirement.
+ * Added a 'book navigation' block.
+- Accessibility:
+ * Made themes degrade gracefully in absence of CSS.
+ * Grouped form elements using '
' => 0);
+
+ // If no complete paragraph then treat line breaks as paragraphs.
+ $line_breaks = array(' ' => 6, ' ' => 4);
+ // Newline only indicates a line break if line break converter
+ // filter is present.
+ if (isset($filters['filter_autop'])) {
+ $line_breaks["\n"] = 1;
+ }
+ $break_points[] = $line_breaks;
+
+ // If the first paragraph is too long, split at the end of a sentence.
+ $break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
+
+ // Iterate over the groups of break points until a break point is found.
+ foreach ($break_points as $points) {
+ // Look for each break point, starting at the end of the summary.
+ foreach ($points as $point => $offset) {
+ // The summary is already reversed, but the break point isn't.
+ $rpos = strpos($reversed, strrev($point));
+ if ($rpos !== FALSE) {
+ $min_rpos = min($rpos + $offset, $min_rpos);
+ }
+ }
+
+ // If a break point was found in this group, slice and stop searching.
+ if ($min_rpos !== $max_rpos) {
+ // Don't slice with length 0. Length must be <0 to slice from RHS.
+ $summary = ($min_rpos === 0) ? $summary : substr($summary, 0, 0 - $min_rpos);
+ break;
+ }
+ }
+
+ // If the htmlcorrector filter is present, apply it to the generated summary.
+ if (isset($filters['filter_htmlcorrector'])) {
+ $summary = _filter_htmlcorrector($summary);
+ }
+
+ return $summary;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function text_field_widget_info() {
+ return array(
+ 'text_textfield' => array(
+ 'label' => t('Text field'),
+ 'field types' => array('text'),
+ 'settings' => array('size' => 60),
+ ),
+ 'text_textarea' => array(
+ 'label' => t('Text area (multiple rows)'),
+ 'field types' => array('text_long'),
+ 'settings' => array('rows' => 5),
+ ),
+ 'text_textarea_with_summary' => array(
+ 'label' => t('Text area with a summary'),
+ 'field types' => array('text_with_summary'),
+ 'settings' => array('rows' => 20, 'summary_rows' => 5),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function text_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'];
+
+ if ($widget['type'] == 'text_textfield') {
+ $form['size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Size of textfield'),
+ '#default_value' => $settings['size'],
+ '#required' => TRUE,
+ '#element_validate' => array('element_validate_integer_positive'),
+ );
+ }
+ else {
+ $form['rows'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Rows'),
+ '#default_value' => $settings['rows'],
+ '#required' => TRUE,
+ '#element_validate' => array('element_validate_integer_positive'),
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function text_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ $summary_widget = array();
+ $main_widget = array();
+
+ switch ($instance['widget']['type']) {
+ case 'text_textfield':
+ $main_widget = $element + array(
+ '#type' => 'textfield',
+ '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL,
+ '#size' => $instance['widget']['settings']['size'],
+ '#maxlength' => $field['settings']['max_length'],
+ '#attributes' => array('class' => array('text-full')),
+ );
+ break;
+
+ case 'text_textarea_with_summary':
+ $display = !empty($items[$delta]['summary']) || !empty($instance['settings']['display_summary']);
+ $summary_widget = array(
+ '#type' => $display ? 'textarea' : 'value',
+ '#default_value' => isset($items[$delta]['summary']) ? $items[$delta]['summary'] : NULL,
+ '#title' => t('Summary'),
+ '#rows' => $instance['widget']['settings']['summary_rows'],
+ '#description' => t('Leave blank to use trimmed value of full text as the summary.'),
+ '#attached' => array(
+ 'js' => array(drupal_get_path('module', 'text') . '/text.js'),
+ ),
+ '#attributes' => array('class' => array('text-summary')),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ '#weight' => -10,
+ );
+ // Fall through to the next case.
+
+ case 'text_textarea':
+ $main_widget = $element + array(
+ '#type' => 'textarea',
+ '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL,
+ '#rows' => $instance['widget']['settings']['rows'],
+ '#attributes' => array('class' => array('text-full')),
+ );
+ break;
+ }
+
+ if ($main_widget) {
+ // Conditionally alter the form element's type if text processing is enabled.
+ if ($instance['settings']['text_processing']) {
+ $element = $main_widget;
+ $element['#type'] = 'text_format';
+ $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
+ $element['#base_type'] = $main_widget['#type'];
+ }
+ else {
+ $element['value'] = $main_widget;
+ }
+ }
+ if ($summary_widget) {
+ $element['summary'] = $summary_widget;
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function text_field_widget_error($element, $error, $form, &$form_state) {
+ switch ($error['error']) {
+ case 'text_summary_max_length':
+ $error_element = $element[$element['#columns'][1]];
+ break;
+
+ default:
+ $error_element = $element[$element['#columns'][0]];
+ break;
+ }
+
+ form_error($error_element, $error['message']);
+}
+
+/**
+ * Implements hook_field_prepare_translation().
+ */
+function text_field_prepare_translation($entity_type, $entity, $field, $instance, $langcode, &$items, $source_entity, $source_langcode) {
+ // If the translating user is not permitted to use the assigned text format,
+ // we must not expose the source values.
+ $field_name = $field['field_name'];
+ if (!empty($source_entity->{$field_name}[$source_langcode])) {
+ $formats = filter_formats();
+ foreach ($source_entity->{$field_name}[$source_langcode] as $delta => $item) {
+ $format_id = $item['format'];
+ if (!empty($format_id) && !filter_access($formats[$format_id])) {
+ unset($items[$delta]);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_filter_format_update().
+ */
+function text_filter_format_update($format) {
+ field_cache_clear();
+}
+
+/**
+ * Implements hook_filter_format_disable().
+ */
+function text_filter_format_disable($format) {
+ field_cache_clear();
+}
diff --git a/drupal-dev/modules/field/modules/text/text.test b/drupal-dev/modules/field/modules/text/text.test
new file mode 100644
index 0000000..2f14738
--- /dev/null
+++ b/drupal-dev/modules/field/modules/text/text.test
@@ -0,0 +1,517 @@
+ 'Text field',
+ 'description' => "Test the creation of text fields.",
+ 'group' => 'Field types'
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+
+ $this->admin_user = $this->drupalCreateUser(array('administer filters'));
+ $this->web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
+ $this->drupalLogin($this->web_user);
+ }
+
+ // Test fields.
+
+ /**
+ * Test text field validation.
+ */
+ function testTextFieldValidation() {
+ // Create a field with settings to validate.
+ $max_length = 3;
+ $this->field = array(
+ 'field_name' => drupal_strtolower($this->randomName()),
+ 'type' => 'text',
+ 'settings' => array(
+ 'max_length' => $max_length,
+ )
+ );
+ field_create_field($this->field);
+ $this->instance = array(
+ 'field_name' => $this->field['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ ),
+ 'display' => array(
+ 'default' => array(
+ 'type' => 'text_default',
+ ),
+ ),
+ );
+ field_create_instance($this->instance);
+ // Test valid and invalid values with field_attach_validate().
+ $entity = field_test_create_stub_entity();
+ $langcode = LANGUAGE_NONE;
+ for ($i = 0; $i <= $max_length + 2; $i++) {
+ $entity->{$this->field['field_name']}[$langcode][0]['value'] = str_repeat('x', $i);
+ try {
+ field_attach_validate('test_entity', $entity);
+ $this->assertTrue($i <= $max_length, "Length $i does not cause validation error when max_length is $max_length");
+ }
+ catch (FieldValidationException $e) {
+ $this->assertTrue($i > $max_length, "Length $i causes validation error when max_length is $max_length");
+ }
+ }
+ }
+
+ /**
+ * Test widgets.
+ */
+ function testTextfieldWidgets() {
+ $this->_testTextfieldWidgets('text', 'text_textfield');
+ $this->_testTextfieldWidgets('text_long', 'text_textarea');
+ }
+
+ /**
+ * Helper function for testTextfieldWidgets().
+ */
+ function _testTextfieldWidgets($field_type, $widget_type) {
+ // Setup a field and instance
+ $entity_type = 'test_entity';
+ $this->field_name = drupal_strtolower($this->randomName());
+ $this->field = array('field_name' => $this->field_name, 'type' => $field_type);
+ field_create_field($this->field);
+ $this->instance = array(
+ 'field_name' => $this->field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->randomName() . '_label',
+ 'settings' => array(
+ 'text_processing' => TRUE,
+ ),
+ 'widget' => array(
+ 'type' => $widget_type,
+ ),
+ 'display' => array(
+ 'full' => array(
+ 'type' => 'text_default',
+ ),
+ ),
+ );
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Display creation form.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
+ $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '1', 'Format selector is not displayed');
+
+ // Submit with some value.
+ $value = $this->randomName();
+ $edit = array(
+ "{$this->field_name}[$langcode][0][value]" => $value,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+ $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
+
+ // Display the entity.
+ $entity = field_test_entity_test_load($id);
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $this->content = drupal_render($entity->content);
+ $this->assertText($value, 'Filtered tags are not displayed');
+ }
+
+ /**
+ * Test widgets + 'formatted_text' setting.
+ */
+ function testTextfieldWidgetsFormatted() {
+ $this->_testTextfieldWidgetsFormatted('text', 'text_textfield');
+ $this->_testTextfieldWidgetsFormatted('text_long', 'text_textarea');
+ }
+
+ /**
+ * Helper function for testTextfieldWidgetsFormatted().
+ */
+ function _testTextfieldWidgetsFormatted($field_type, $widget_type) {
+ // Setup a field and instance
+ $entity_type = 'test_entity';
+ $this->field_name = drupal_strtolower($this->randomName());
+ $this->field = array('field_name' => $this->field_name, 'type' => $field_type);
+ field_create_field($this->field);
+ $this->instance = array(
+ 'field_name' => $this->field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->randomName() . '_label',
+ 'settings' => array(
+ 'text_processing' => TRUE,
+ ),
+ 'widget' => array(
+ 'type' => $widget_type,
+ ),
+ 'display' => array(
+ 'full' => array(
+ 'type' => 'text_default',
+ ),
+ ),
+ );
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Disable all text formats besides the plain text fallback format.
+ $this->drupalLogin($this->admin_user);
+ foreach (filter_formats() as $format) {
+ if ($format->format != filter_fallback_format()) {
+ $this->drupalPost('admin/config/content/formats/' . $format->format . '/disable', array(), t('Disable'));
+ }
+ }
+ $this->drupalLogin($this->web_user);
+
+ // Display the creation form. Since the user only has access to one format,
+ // no format selector will be displayed.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
+ $this->assertNoFieldByName("{$this->field_name}[$langcode][0][format]", '', 'Format selector is not displayed');
+
+ // Submit with data that should be filtered.
+ $value = '' . $this->randomName() . '';
+ $edit = array(
+ "{$this->field_name}[$langcode][0][value]" => $value,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+ $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
+
+ // Display the entity.
+ $entity = field_test_entity_test_load($id);
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $this->content = drupal_render($entity->content);
+ $this->assertNoRaw($value, 'HTML tags are not displayed.');
+ $this->assertRaw(check_plain($value), 'Escaped HTML is displayed correctly.');
+
+ // Create a new text format that does not escape HTML, and grant the user
+ // access to it.
+ $this->drupalLogin($this->admin_user);
+ $edit = array(
+ 'format' => drupal_strtolower($this->randomName()),
+ 'name' => $this->randomName(),
+ );
+ $this->drupalPost('admin/config/content/formats/add', $edit, t('Save configuration'));
+ filter_formats_reset();
+ $this->checkPermissions(array(), TRUE);
+ $format = filter_format_load($edit['format']);
+ $format_id = $format->format;
+ $permission = filter_permission_name($format);
+ $rid = max(array_keys($this->web_user->roles));
+ user_role_grant_permissions($rid, array($permission));
+ $this->drupalLogin($this->web_user);
+
+ // Display edition form.
+ // We should now have a 'text format' selector.
+ $this->drupalGet('test-entity/manage/' . $id . '/edit');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", NULL, 'Widget is displayed');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][format]", NULL, 'Format selector is displayed');
+
+ // Edit and change the text format to the new one that was created.
+ $edit = array(
+ "{$this->field_name}[$langcode][0][format]" => $format_id,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
+
+ // Display the entity.
+ $entity = field_test_entity_test_load($id);
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $this->content = drupal_render($entity->content);
+ $this->assertRaw($value, 'Value is displayed unfiltered');
+ }
+}
+
+class TextSummaryTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Text summary',
+ 'description' => 'Test text_summary() with different strings and lengths.',
+ 'group' => 'Field types',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+ $this->article_creator = $this->drupalCreateUser(array('create article content', 'edit own article content'));
+ }
+
+ /**
+ * Tests an edge case where the first sentence is a question and
+ * subsequent sentences are not. This edge case is documented at
+ * http://drupal.org/node/180425.
+ */
+ function testFirstSentenceQuestion() {
+ $text = 'A question? A sentence. Another sentence.';
+ $expected = 'A question? A sentence.';
+ $this->callTextSummary($text, $expected, NULL, 30);
+ }
+
+ /**
+ * Test summary with long example.
+ */
+ function testLongSentence() {
+ $text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' . // 125
+ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' . // 108
+ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . // 103
+ 'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; // 110
+ $expected = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
+ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
+ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
+ // First three sentences add up to: 336, so add one for space and then 3 to get half-way into next word.
+ $this->callTextSummary($text, $expected, NULL, 340);
+ }
+
+ /**
+ * Test various summary length edge cases.
+ */
+ function testLength() {
+ // This string tests a number of edge cases.
+ $text = "
\nHi\n
\n
\nfolks\n \n!\n
";
+
+ // The summaries we expect text_summary() to return when $size is the index
+ // of each array item.
+ // Using no text format:
+ $expected = array(
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "<",
+ "
",
+ "
\n",
+ "
\nH",
+ "
\nHi",
+ "
\nHi\n",
+ "
\nHi\n<",
+ "
\nHi\n",
+ "
\nHi\n
\nHi\n",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ );
+
+ // And using a text format WITH the line-break and htmlcorrector filters.
+ $expected_lb = array(
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "
\nHi
",
+ "
\nHi
",
+ "
\nHi
",
+ "
\nHi
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ "
\nHi\n
\n
\nfolks\n \n!\n
",
+ );
+
+ // Test text_summary() for different sizes.
+ for ($i = 0; $i <= 37; $i++) {
+ $this->callTextSummary($text, $expected[$i], NULL, $i);
+ $this->callTextSummary($text, $expected_lb[$i], 'plain_text', $i);
+ $this->callTextSummary($text, $expected_lb[$i], 'filtered_html', $i);
+ }
+ }
+
+ /**
+ * Calls text_summary() and asserts that the expected teaser is returned.
+ */
+ function callTextSummary($text, $expected, $format = NULL, $size = NULL) {
+ $summary = text_summary($text, $format, $size);
+ $this->assertIdentical($summary, $expected, format_string('Generated summary "@summary" matches expected "@expected".', array('@summary' => $summary, '@expected' => $expected)));
+ }
+
+ /**
+ * Test sending only summary.
+ */
+ function testOnlyTextSummary() {
+ // Login as article creator.
+ $this->drupalLogin($this->article_creator);
+ // Create article with summary but empty body.
+ $summary = $this->randomName();
+ $edit = array(
+ "title" => $this->randomName(),
+ "body[und][0][summary]" => $summary,
+ );
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ $node = $this->drupalGetNodeByTitle($edit['title']);
+
+ $this->assertIdentical($node->body['und'][0]['summary'], $summary, 'Article with with summary and no body has been submitted.');
+ }
+}
+
+class TextTranslationTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Text translation',
+ 'description' => 'Check if the text field is correctly prepared for translation.',
+ 'group' => 'Field types',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('locale', 'translation');
+
+ $full_html_format = filter_format_load('full_html');
+ $this->format = $full_html_format->format;
+ $this->admin = $this->drupalCreateUser(array(
+ 'administer languages',
+ 'administer content types',
+ 'access administration pages',
+ 'bypass node access',
+ filter_permission_name($full_html_format),
+ ));
+ $this->translator = $this->drupalCreateUser(array('create article content', 'edit own article content', 'translate content'));
+
+ // Enable an additional language.
+ $this->drupalLogin($this->admin);
+ $edit = array('langcode' => 'fr');
+ $this->drupalPost('admin/config/regional/language/add', $edit, t('Add language'));
+
+ // Set "Article" content type to use multilingual support with translation.
+ $edit = array('language_content_type' => 2);
+ $this->drupalPost('admin/structure/types/manage/article', $edit, t('Save content type'));
+ $this->assertRaw(t('The content type %type has been updated.', array('%type' => 'Article')), 'Article content type has been updated.');
+ }
+
+ /**
+ * Test that a plaintext textfield widget is correctly populated.
+ */
+ function testTextField() {
+ // Disable text processing for body.
+ $edit = array('instance[settings][text_processing]' => 0);
+ $this->drupalPost('admin/structure/types/manage/article/fields/body', $edit, t('Save settings'));
+
+ // Login as translator.
+ $this->drupalLogin($this->translator);
+
+ // Create content.
+ $langcode = LANGUAGE_NONE;
+ $body = $this->randomName();
+ $edit = array(
+ "title" => $this->randomName(),
+ "language" => 'en',
+ "body[$langcode][0][value]" => $body,
+ );
+
+ // Translate the article in french.
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ $node = $this->drupalGetNodeByTitle($edit['title']);
+ $this->drupalGet("node/$node->nid/translate");
+ $this->clickLink(t('add translation'));
+ $this->assertFieldByXPath("//textarea[@name='body[$langcode][0][value]']", $body, 'The textfield widget is populated.');
+ }
+
+ /**
+ * Check that user that does not have access the field format cannot see the
+ * source value when creating a translation.
+ */
+ function testTextFieldFormatted() {
+ // Make node body multiple.
+ $edit = array('field[cardinality]' => -1);
+ $this->drupalPost('admin/structure/types/manage/article/fields/body', $edit, t('Save settings'));
+ $this->drupalGet('node/add/article');
+ $this->assertFieldByXPath("//input[@name='body_add_more']", t('Add another item'), 'Body field cardinality set to multiple.');
+
+ $body = array(
+ $this->randomName(),
+ $this->randomName(),
+ );
+
+ // Create an article with the first body input format set to "Full HTML".
+ $title = $this->randomName();
+ $edit = array(
+ 'title' => $title,
+ 'language' => 'en',
+ );
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+
+ // Populate the body field: the first item gets the "Full HTML" input
+ // format, the second one "Filtered HTML".
+ $formats = array('full_html', 'filtered_html');
+ $langcode = LANGUAGE_NONE;
+ foreach ($body as $delta => $value) {
+ $edit = array(
+ "body[$langcode][$delta][value]" => $value,
+ "body[$langcode][$delta][format]" => array_shift($formats),
+ );
+ $this->drupalPost('node/1/edit', $edit, t('Save'));
+ $this->assertText($body[$delta], format_string('The body field with delta @delta has been saved.', array('@delta' => $delta)));
+ }
+
+ // Login as translator.
+ $this->drupalLogin($this->translator);
+
+ // Translate the article in french.
+ $node = $this->drupalGetNodeByTitle($title);
+ $this->drupalGet("node/$node->nid/translate");
+ $this->clickLink(t('add translation'));
+ $this->assertNoText($body[0], format_string('The body field with delta @delta is hidden.', array('@delta' => 0)));
+ $this->assertText($body[1], format_string('The body field with delta @delta is shown.', array('@delta' => 1)));
+ }
+}
diff --git a/drupal-dev/modules/field/tests/field.test b/drupal-dev/modules/field/tests/field.test
new file mode 100644
index 0000000..1e59315
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field.test
@@ -0,0 +1,3709 @@
+default_storage);
+ }
+
+ /**
+ * Generate random values for a field_test field.
+ *
+ * @param $cardinality
+ * Number of values to generate.
+ * @return
+ * An array of random values, in the format expected for field values.
+ */
+ function _generateTestFieldValues($cardinality) {
+ $values = array();
+ for ($i = 0; $i < $cardinality; $i++) {
+ // field_test fields treat 0 as 'empty value'.
+ $values[$i]['value'] = mt_rand(1, 127);
+ }
+ return $values;
+ }
+
+ /**
+ * Assert that a field has the expected values in an entity.
+ *
+ * This function only checks a single column in the field values.
+ *
+ * @param $entity
+ * The entity to test.
+ * @param $field_name
+ * The name of the field to test
+ * @param $langcode
+ * The language code for the values.
+ * @param $expected_values
+ * The array of expected values.
+ * @param $column
+ * (Optional) the name of the column to check.
+ */
+ function assertFieldValues($entity, $field_name, $langcode, $expected_values, $column = 'value') {
+ $e = clone $entity;
+ field_attach_load('test_entity', array($e->ftid => $e));
+ $values = isset($e->{$field_name}[$langcode]) ? $e->{$field_name}[$langcode] : array();
+ $this->assertEqual(count($values), count($expected_values), 'Expected number of values were saved.');
+ foreach ($expected_values as $key => $value) {
+ $this->assertEqual($values[$key][$column], $value, format_string('Value @value was saved correctly.', array('@value' => $value)));
+ }
+ }
+}
+
+class FieldAttachTestCase extends FieldTestCase {
+ function setUp() {
+ // Since this is a base class for many test cases, support the same
+ // flexibility that DrupalWebTestCase::setUp() has for the modules to be
+ // passed in as either an array or a variable number of string arguments.
+ $modules = func_get_args();
+ if (isset($modules[0]) && is_array($modules[0])) {
+ $modules = $modules[0];
+ }
+ if (!in_array('field_test', $modules)) {
+ $modules[] = 'field_test';
+ }
+ parent::setUp($modules);
+
+ $this->createFieldWithInstance();
+ }
+
+ /**
+ * Create a field and an instance of it.
+ *
+ * @param string $suffix
+ * (optional) A string that should only contain characters that are valid in
+ * PHP variable names as well.
+ */
+ function createFieldWithInstance($suffix = '') {
+ $field_name = 'field_name' . $suffix;
+ $field = 'field' . $suffix;
+ $field_id = 'field_id' . $suffix;
+ $instance = 'instance' . $suffix;
+
+ $this->$field_name = drupal_strtolower($this->randomName() . '_field_name' . $suffix);
+ $this->$field = array('field_name' => $this->$field_name, 'type' => 'test_field', 'cardinality' => 4);
+ $this->$field = field_create_field($this->$field);
+ $this->$field_id = $this->{$field}['id'];
+ $this->$instance = array(
+ 'field_name' => $this->$field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->randomName() . '_label',
+ 'description' => $this->randomName() . '_description',
+ 'weight' => mt_rand(0, 127),
+ 'settings' => array(
+ 'test_instance_setting' => $this->randomName(),
+ ),
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ 'label' => 'Test Field',
+ 'settings' => array(
+ 'test_widget_setting' => $this->randomName(),
+ )
+ )
+ );
+ field_create_instance($this->$instance);
+ }
+}
+
+/**
+ * Unit test class for storage-related field_attach_* functions.
+ *
+ * All field_attach_* test work with all field_storage plugins and
+ * all hook_field_attach_pre_{load,insert,update}() hooks.
+ */
+class FieldAttachStorageTestCase extends FieldAttachTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field attach tests (storage-related)',
+ 'description' => 'Test storage-related Field Attach API functions.',
+ 'group' => 'Field API',
+ );
+ }
+
+ /**
+ * Check field values insert, update and load.
+ *
+ * Works independently of the underlying field storage backend. Inserts or
+ * updates random field data and then loads and verifies the data.
+ */
+ function testFieldAttachSaveLoad() {
+ // Configure the instance so that we test hook_field_load() (see
+ // field_test_field_load() in field_test.module).
+ $this->instance['settings']['test_hook_field_load'] = TRUE;
+ field_update_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ $entity_type = 'test_entity';
+ $values = array();
+
+ // TODO : test empty values filtering and "compression" (store consecutive deltas).
+
+ // Preparation: create three revisions and store them in $revision array.
+ for ($revision_id = 0; $revision_id < 3; $revision_id++) {
+ $revision[$revision_id] = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']);
+ // Note: we try to insert one extra value.
+ $values[$revision_id] = $this->_generateTestFieldValues($this->field['cardinality'] + 1);
+ $current_revision = $revision_id;
+ // If this is the first revision do an insert.
+ if (!$revision_id) {
+ $revision[$revision_id]->{$this->field_name}[$langcode] = $values[$revision_id];
+ field_attach_insert($entity_type, $revision[$revision_id]);
+ }
+ else {
+ // Otherwise do an update.
+ $revision[$revision_id]->{$this->field_name}[$langcode] = $values[$revision_id];
+ field_attach_update($entity_type, $revision[$revision_id]);
+ }
+ }
+
+ // Confirm current revision loads the correct data.
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ field_attach_load($entity_type, array(0 => $entity));
+ // Number of values per field loaded equals the field cardinality.
+ $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], 'Current revision: expected number of values');
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ // The field value loaded matches the one inserted or updated.
+ $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'] , $values[$current_revision][$delta]['value'], format_string('Current revision: expected value %delta was found.', array('%delta' => $delta)));
+ // The value added in hook_field_load() is found.
+ $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Current revision: extra information for value %delta was found', array('%delta' => $delta)));
+ }
+
+ // Confirm each revision loads the correct data.
+ foreach (array_keys($revision) as $revision_id) {
+ $entity = field_test_create_stub_entity(0, $revision_id, $this->instance['bundle']);
+ field_attach_load_revision($entity_type, array(0 => $entity));
+ // Number of values per field loaded equals the field cardinality.
+ $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], format_string('Revision %revision_id: expected number of values.', array('%revision_id' => $revision_id)));
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ // The field value loaded matches the one inserted or updated.
+ $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['value'], $values[$revision_id][$delta]['value'], format_string('Revision %revision_id: expected value %delta was found.', array('%revision_id' => $revision_id, '%delta' => $delta)));
+ // The value added in hook_field_load() is found.
+ $this->assertEqual($entity->{$this->field_name}[$langcode][$delta]['additional_key'], 'additional_value', format_string('Revision %revision_id: extra information for value %delta was found', array('%revision_id' => $revision_id, '%delta' => $delta)));
+ }
+ }
+ }
+
+ /**
+ * Test the 'multiple' load feature.
+ */
+ function testFieldAttachLoadMultiple() {
+ $entity_type = 'test_entity';
+ $langcode = LANGUAGE_NONE;
+
+ // Define 2 bundles.
+ $bundles = array(
+ 1 => 'test_bundle_1',
+ 2 => 'test_bundle_2',
+ );
+ field_test_create_bundle($bundles[1]);
+ field_test_create_bundle($bundles[2]);
+ // Define 3 fields:
+ // - field_1 is in bundle_1 and bundle_2,
+ // - field_2 is in bundle_1,
+ // - field_3 is in bundle_2.
+ $field_bundles_map = array(
+ 1 => array(1, 2),
+ 2 => array(1),
+ 3 => array(2),
+ );
+ for ($i = 1; $i <= 3; $i++) {
+ $field_names[$i] = 'field_' . $i;
+ $field = array('field_name' => $field_names[$i], 'type' => 'test_field');
+ $field = field_create_field($field);
+ $field_ids[$i] = $field['id'];
+ foreach ($field_bundles_map[$i] as $bundle) {
+ $instance = array(
+ 'field_name' => $field_names[$i],
+ 'entity_type' => 'test_entity',
+ 'bundle' => $bundles[$bundle],
+ 'settings' => array(
+ // Configure the instance so that we test hook_field_load()
+ // (see field_test_field_load() in field_test.module).
+ 'test_hook_field_load' => TRUE,
+ ),
+ );
+ field_create_instance($instance);
+ }
+ }
+
+ // Create one test entity per bundle, with random values.
+ foreach ($bundles as $index => $bundle) {
+ $entities[$index] = field_test_create_stub_entity($index, $index, $bundle);
+ $entity = clone($entities[$index]);
+ $instances = field_info_instances('test_entity', $bundle);
+ foreach ($instances as $field_name => $instance) {
+ $values[$index][$field_name] = mt_rand(1, 127);
+ $entity->$field_name = array($langcode => array(array('value' => $values[$index][$field_name])));
+ }
+ field_attach_insert($entity_type, $entity);
+ }
+
+ // Check that a single load correctly loads field values for both entities.
+ field_attach_load($entity_type, $entities);
+ foreach ($entities as $index => $entity) {
+ $instances = field_info_instances($entity_type, $bundles[$index]);
+ foreach ($instances as $field_name => $instance) {
+ // The field value loaded matches the one inserted.
+ $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], $values[$index][$field_name], format_string('Entity %index: expected value was found.', array('%index' => $index)));
+ // The value added in hook_field_load() is found.
+ $this->assertEqual($entity->{$field_name}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => $index)));
+ }
+ }
+
+ // Check that the single-field load option works.
+ $entity = field_test_create_stub_entity(1, 1, $bundles[1]);
+ field_attach_load($entity_type, array(1 => $entity), FIELD_LOAD_CURRENT, array('field_id' => $field_ids[1]));
+ $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['value'], $values[1][$field_names[1]], format_string('Entity %index: expected value was found.', array('%index' => 1)));
+ $this->assertEqual($entity->{$field_names[1]}[$langcode][0]['additional_key'], 'additional_value', format_string('Entity %index: extra information was found', array('%index' => 1)));
+ $this->assert(!isset($entity->{$field_names[2]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 2, '%field_name' => $field_names[2])));
+ $this->assert(!isset($entity->{$field_names[3]}), format_string('Entity %index: field %field_name is not loaded.', array('%index' => 3, '%field_name' => $field_names[3])));
+ }
+
+ /**
+ * Test saving and loading fields using different storage backends.
+ */
+ function testFieldAttachSaveLoadDifferentStorage() {
+ $entity_type = 'test_entity';
+ $langcode = LANGUAGE_NONE;
+
+ // Create two fields using different storage backends, and their instances.
+ $fields = array(
+ array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'storage' => array('type' => 'field_sql_storage')
+ ),
+ array(
+ 'field_name' => 'field_2',
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'storage' => array('type' => 'field_test_storage')
+ ),
+ );
+ foreach ($fields as $field) {
+ field_create_field($field);
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance);
+ }
+
+ $entity_init = field_test_create_stub_entity();
+
+ // Create entity and insert random values.
+ $entity = clone($entity_init);
+ $values = array();
+ foreach ($fields as $field) {
+ $values[$field['field_name']] = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity->{$field['field_name']}[$langcode] = $values[$field['field_name']];
+ }
+ field_attach_insert($entity_type, $entity);
+
+ // Check that values are loaded as expected.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ foreach ($fields as $field) {
+ $this->assertEqual($values[$field['field_name']], $entity->{$field['field_name']}[$langcode], format_string('%storage storage: expected values were found.', array('%storage' => $field['storage']['type'])));
+ }
+ }
+
+ /**
+ * Test storage details alteration.
+ *
+ * @see field_test_storage_details_alter()
+ */
+ function testFieldStorageDetailsAlter() {
+ $field_name = 'field_test_change_my_details';
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'storage' => array('type' => 'field_test_storage'),
+ );
+ $field = field_create_field($field);
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance);
+
+ $field = field_info_field($instance['field_name']);
+ $instance = field_info_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
+
+ // The storage details are indexed by a storage engine type.
+ $this->assertTrue(array_key_exists('drupal_variables', $field['storage']['details']), 'The storage type is Drupal variables.');
+
+ $details = $field['storage']['details']['drupal_variables'];
+
+ // The field_test storage details are indexed by variable name. The details
+ // are altered, so moon and mars are correct for this test.
+ $this->assertTrue(array_key_exists('moon', $details[FIELD_LOAD_CURRENT]), 'Moon is available in the instance array.');
+ $this->assertTrue(array_key_exists('mars', $details[FIELD_LOAD_REVISION]), 'Mars is available in the instance array.');
+
+ // Test current and revision storage details together because the columns
+ // are the same.
+ foreach ((array) $field['columns'] as $column_name => $attributes) {
+ $this->assertEqual($details[FIELD_LOAD_CURRENT]['moon'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'moon[FIELD_LOAD_CURRENT]')));
+ $this->assertEqual($details[FIELD_LOAD_REVISION]['mars'][$column_name], $column_name, format_string('Column name %value matches the definition in %bin.', array('%value' => $column_name, '%bin' => 'mars[FIELD_LOAD_REVISION]')));
+ }
+ }
+
+ /**
+ * Tests insert and update with missing or NULL fields.
+ */
+ function testFieldAttachSaveMissingData() {
+ $entity_type = 'test_entity';
+ $entity_init = field_test_create_stub_entity();
+ $langcode = LANGUAGE_NONE;
+
+ // Insert: Field is missing.
+ $entity = clone($entity_init);
+ field_attach_insert($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}), 'Insert: missing field results in no value saved');
+
+ // Insert: Field is NULL.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $entity->{$this->field_name} = NULL;
+ field_attach_insert($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}), 'Insert: NULL field results in no value saved');
+
+ // Add some real data.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $values = $this->_generateTestFieldValues(1);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_insert($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved');
+
+ // Update: Field is missing. Data should survive.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Update: missing field leaves existing values in place');
+
+ // Update: Field is NULL. Data should be wiped.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $entity->{$this->field_name} = NULL;
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}), 'Update: NULL field removes existing values');
+
+ // Re-add some data.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $values = $this->_generateTestFieldValues(1);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Field data saved');
+
+ // Update: Field is empty array. Data should be wiped.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ $entity->{$this->field_name} = array();
+ field_attach_update($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}), 'Update: empty array removes existing values');
+ }
+
+ /**
+ * Test insert with missing or NULL fields, with default value.
+ */
+ function testFieldAttachSaveMissingDataDefaultValue() {
+ // Add a default value function.
+ $this->instance['default_value_function'] = 'field_test_default_value';
+ field_update_instance($this->instance);
+
+ $entity_type = 'test_entity';
+ $entity_init = field_test_create_stub_entity();
+ $langcode = LANGUAGE_NONE;
+
+ // Insert: Field is NULL.
+ $entity = clone($entity_init);
+ $entity->{$this->field_name}[$langcode] = NULL;
+ field_attach_insert($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertTrue(empty($entity->{$this->field_name}[$langcode]), 'Insert: NULL field results in no value saved');
+
+ // Insert: Field is missing.
+ field_cache_clear();
+ $entity = clone($entity_init);
+ field_attach_insert($entity_type, $entity);
+
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $values = field_test_default_value($entity_type, $entity, $this->field, $this->instance);
+ $this->assertEqual($entity->{$this->field_name}[$langcode], $values, 'Insert: missing field results in default value saved');
+ }
+
+ /**
+ * Test field_attach_delete().
+ */
+ function testFieldAttachDelete() {
+ $entity_type = 'test_entity';
+ $langcode = LANGUAGE_NONE;
+ $rev[0] = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+
+ // Create revision 0
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $rev[0]->{$this->field_name}[$langcode] = $values;
+ field_attach_insert($entity_type, $rev[0]);
+
+ // Create revision 1
+ $rev[1] = field_test_create_stub_entity(0, 1, $this->instance['bundle']);
+ $rev[1]->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $rev[1]);
+
+ // Create revision 2
+ $rev[2] = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
+ $rev[2]->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $rev[2]);
+
+ // Confirm each revision loads
+ foreach (array_keys($rev) as $vid) {
+ $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
+ field_attach_load_revision($entity_type, array(0 => $read));
+ $this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity revision $vid has {$this->field['cardinality']} values.");
+ }
+
+ // Delete revision 1, confirm the other two still load.
+ field_attach_delete_revision($entity_type, $rev[1]);
+ foreach (array(0, 2) as $vid) {
+ $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
+ field_attach_load_revision($entity_type, array(0 => $read));
+ $this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity revision $vid has {$this->field['cardinality']} values.");
+ }
+
+ // Confirm the current revision still loads
+ $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
+ field_attach_load($entity_type, array(0 => $read));
+ $this->assertEqual(count($read->{$this->field_name}[$langcode]), $this->field['cardinality'], "The test entity current revision has {$this->field['cardinality']} values.");
+
+ // Delete all field data, confirm nothing loads
+ field_attach_delete($entity_type, $rev[2]);
+ foreach (array(0, 1, 2) as $vid) {
+ $read = field_test_create_stub_entity(0, $vid, $this->instance['bundle']);
+ field_attach_load_revision($entity_type, array(0 => $read));
+ $this->assertIdentical($read->{$this->field_name}, array(), "The test entity revision $vid is deleted.");
+ }
+ $read = field_test_create_stub_entity(0, 2, $this->instance['bundle']);
+ field_attach_load($entity_type, array(0 => $read));
+ $this->assertIdentical($read->{$this->field_name}, array(), 'The test entity current revision is deleted.');
+ }
+
+ /**
+ * Test field_attach_create_bundle() and field_attach_rename_bundle().
+ */
+ function testFieldAttachCreateRenameBundle() {
+ // Create a new bundle. This has to be initiated by the module so that its
+ // hook_entity_info() is consistent.
+ $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
+ field_test_create_bundle($new_bundle);
+
+ // Add an instance to that bundle.
+ $this->instance['bundle'] = $new_bundle;
+ field_create_instance($this->instance);
+
+ // Save an entity with data in the field.
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity->{$this->field_name}[$langcode] = $values;
+ $entity_type = 'test_entity';
+ field_attach_insert($entity_type, $entity);
+
+ // Verify the field data is present on load.
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ field_attach_load($entity_type, array(0 => $entity));
+ $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Data is retrieved for the new bundle");
+
+ // Rename the bundle. This has to be initiated by the module so that its
+ // hook_entity_info() is consistent.
+ $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
+ field_test_rename_bundle($this->instance['bundle'], $new_bundle);
+
+ // Check that the instance definition has been updated.
+ $this->instance = field_info_instance($entity_type, $this->field_name, $new_bundle);
+ $this->assertIdentical($this->instance['bundle'], $new_bundle, "Bundle name has been updated in the instance.");
+
+ // Verify the field data is present on load.
+ $entity = field_test_create_stub_entity(0, 0, $new_bundle);
+ field_attach_load($entity_type, array(0 => $entity));
+ $this->assertEqual(count($entity->{$this->field_name}[$langcode]), $this->field['cardinality'], "Bundle name has been updated in the field storage");
+ }
+
+ /**
+ * Test field_attach_delete_bundle().
+ */
+ function testFieldAttachDeleteBundle() {
+ // Create a new bundle. This has to be initiated by the module so that its
+ // hook_entity_info() is consistent.
+ $new_bundle = 'test_bundle_' . drupal_strtolower($this->randomName());
+ field_test_create_bundle($new_bundle);
+
+ // Add an instance to that bundle.
+ $this->instance['bundle'] = $new_bundle;
+ field_create_instance($this->instance);
+
+ // Create a second field for the test bundle
+ $field_name = drupal_strtolower($this->randomName() . '_field_name');
+ $field = array('field_name' => $field_name, 'type' => 'test_field', 'cardinality' => 1);
+ field_create_field($field);
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => $this->instance['bundle'],
+ 'label' => $this->randomName() . '_label',
+ 'description' => $this->randomName() . '_description',
+ 'weight' => mt_rand(0, 127),
+ // test_field has no instance settings
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ 'settings' => array(
+ 'size' => mt_rand(0, 255))));
+ field_create_instance($instance);
+
+ // Save an entity with data for both fields
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity->{$this->field_name}[$langcode] = $values;
+ $entity->{$field_name}[$langcode] = $this->_generateTestFieldValues(1);
+ field_attach_insert('test_entity', $entity);
+
+ // Verify the fields are present on load
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ field_attach_load('test_entity', array(0 => $entity));
+ $this->assertEqual(count($entity->{$this->field_name}[$langcode]), 4, 'First field got loaded');
+ $this->assertEqual(count($entity->{$field_name}[$langcode]), 1, 'Second field got loaded');
+
+ // Delete the bundle. This has to be initiated by the module so that its
+ // hook_entity_info() is consistent.
+ field_test_delete_bundle($this->instance['bundle']);
+
+ // Verify no data gets loaded
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ field_attach_load('test_entity', array(0 => $entity));
+ $this->assertFalse(isset($entity->{$this->field_name}[$langcode]), 'No data for first field');
+ $this->assertFalse(isset($entity->{$field_name}[$langcode]), 'No data for second field');
+
+ // Verify that the instances are gone
+ $this->assertFalse(field_read_instance('test_entity', $this->field_name, $this->instance['bundle']), "First field is deleted");
+ $this->assertFalse(field_read_instance('test_entity', $field_name, $instance['bundle']), "Second field is deleted");
+ }
+}
+
+/**
+ * Unit test class for non-storage related field_attach_* functions.
+ */
+class FieldAttachOtherTestCase extends FieldAttachTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field attach tests (other)',
+ 'description' => 'Test other Field Attach API functions.',
+ 'group' => 'Field API',
+ );
+ }
+
+ /**
+ * Test field_attach_view() and field_attach_prepare_view().
+ */
+ function testFieldAttachView() {
+ $this->createFieldWithInstance('_2');
+
+ $entity_type = 'test_entity';
+ $entity_init = field_test_create_stub_entity();
+ $langcode = LANGUAGE_NONE;
+ $options = array('field_name' => $this->field_name_2);
+
+ // Populate values to be displayed.
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity_init->{$this->field_name}[$langcode] = $values;
+ $values_2 = $this->_generateTestFieldValues($this->field_2['cardinality']);
+ $entity_init->{$this->field_name_2}[$langcode] = $values_2;
+
+ // Simple formatter, label displayed.
+ $entity = clone($entity_init);
+ $formatter_setting = $this->randomName();
+ $this->instance['display'] = array(
+ 'full' => array(
+ 'label' => 'above',
+ 'type' => 'field_test_default',
+ 'settings' => array(
+ 'test_formatter_setting' => $formatter_setting,
+ )
+ ),
+ );
+ field_update_instance($this->instance);
+ $formatter_setting_2 = $this->randomName();
+ $this->instance_2['display'] = array(
+ 'full' => array(
+ 'label' => 'above',
+ 'type' => 'field_test_default',
+ 'settings' => array(
+ 'test_formatter_setting' => $formatter_setting_2,
+ )
+ ),
+ );
+ field_update_instance($this->instance_2);
+ // View all fields.
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $output = drupal_render($entity->content);
+ $this->content = $output;
+ $this->assertRaw($this->instance['label'], "First field's label is displayed.");
+ foreach ($values as $delta => $value) {
+ $this->content = $output;
+ $this->assertRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
+ }
+ $this->assertRaw($this->instance_2['label'], "Second field's label is displayed.");
+ foreach ($values_2 as $delta => $value) {
+ $this->content = $output;
+ $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
+ }
+ // View single field (the second field).
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full', $langcode, $options);
+ $entity->content = field_attach_view($entity_type, $entity, 'full', $langcode, $options);
+ $output = drupal_render($entity->content);
+ $this->content = $output;
+ $this->assertNoRaw($this->instance['label'], "First field's label is not displayed.");
+ foreach ($values as $delta => $value) {
+ $this->content = $output;
+ $this->assertNoRaw("$formatter_setting|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
+ }
+ $this->assertRaw($this->instance_2['label'], "Second field's label is displayed.");
+ foreach ($values_2 as $delta => $value) {
+ $this->content = $output;
+ $this->assertRaw("$formatter_setting_2|{$value['value']}", "Value $delta is displayed, formatter settings are applied.");
+ }
+
+ // Label hidden.
+ $entity = clone($entity_init);
+ $this->instance['display']['full']['label'] = 'hidden';
+ field_update_instance($this->instance);
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $output = drupal_render($entity->content);
+ $this->content = $output;
+ $this->assertNoRaw($this->instance['label'], "Hidden label: label is not displayed.");
+
+ // Field hidden.
+ $entity = clone($entity_init);
+ $this->instance['display'] = array(
+ 'full' => array(
+ 'label' => 'above',
+ 'type' => 'hidden',
+ ),
+ );
+ field_update_instance($this->instance);
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $output = drupal_render($entity->content);
+ $this->content = $output;
+ $this->assertNoRaw($this->instance['label'], "Hidden field: label is not displayed.");
+ foreach ($values as $delta => $value) {
+ $this->assertNoRaw("$formatter_setting|{$value['value']}", "Hidden field: value $delta is not displayed.");
+ }
+
+ // Multiple formatter.
+ $entity = clone($entity_init);
+ $formatter_setting = $this->randomName();
+ $this->instance['display'] = array(
+ 'full' => array(
+ 'label' => 'above',
+ 'type' => 'field_test_multiple',
+ 'settings' => array(
+ 'test_formatter_setting_multiple' => $formatter_setting,
+ )
+ ),
+ );
+ field_update_instance($this->instance);
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $output = drupal_render($entity->content);
+ $display = $formatter_setting;
+ foreach ($values as $delta => $value) {
+ $display .= "|$delta:{$value['value']}";
+ }
+ $this->content = $output;
+ $this->assertRaw($display, "Multiple formatter: all values are displayed, formatter settings are applied.");
+
+ // Test a formatter that uses hook_field_formatter_prepare_view().
+ $entity = clone($entity_init);
+ $formatter_setting = $this->randomName();
+ $this->instance['display'] = array(
+ 'full' => array(
+ 'label' => 'above',
+ 'type' => 'field_test_with_prepare_view',
+ 'settings' => array(
+ 'test_formatter_setting_additional' => $formatter_setting,
+ )
+ ),
+ );
+ field_update_instance($this->instance);
+ field_attach_prepare_view($entity_type, array($entity->ftid => $entity), 'full');
+ $entity->content = field_attach_view($entity_type, $entity, 'full');
+ $output = drupal_render($entity->content);
+ $this->content = $output;
+ foreach ($values as $delta => $value) {
+ $this->content = $output;
+ $expected = $formatter_setting . '|' . $value['value'] . '|' . ($value['value'] + 1);
+ $this->assertRaw($expected, "Value $delta is displayed, formatter settings are applied.");
+ }
+
+ // TODO:
+ // - check display order with several fields
+
+ // Preprocess template.
+ $variables = array();
+ field_attach_preprocess($entity_type, $entity, $entity->content, $variables);
+ $result = TRUE;
+ foreach ($values as $delta => $item) {
+ if ($variables[$this->field_name][$delta]['value'] !== $item['value']) {
+ $result = FALSE;
+ break;
+ }
+ }
+ $this->assertTrue($result, format_string('Variable $@field_name correctly populated.', array('@field_name' => $this->field_name)));
+ }
+
+ /**
+ * Tests the 'multiple entity' behavior of field_attach_prepare_view().
+ */
+ function testFieldAttachPrepareViewMultiple() {
+ $entity_type = 'test_entity';
+ $langcode = LANGUAGE_NONE;
+
+ // Set the instance to be hidden.
+ $this->instance['display']['full']['type'] = 'hidden';
+ field_update_instance($this->instance);
+
+ // Set up a second instance on another bundle, with a formatter that uses
+ // hook_field_formatter_prepare_view().
+ field_test_create_bundle('test_bundle_2');
+ $formatter_setting = $this->randomName();
+ $this->instance2 = $this->instance;
+ $this->instance2['bundle'] = 'test_bundle_2';
+ $this->instance2['display']['full'] = array(
+ 'type' => 'field_test_with_prepare_view',
+ 'settings' => array(
+ 'test_formatter_setting_additional' => $formatter_setting,
+ )
+ );
+ field_create_instance($this->instance2);
+
+ // Create one entity in each bundle.
+ $entity1_init = field_test_create_stub_entity(1, 1, 'test_bundle');
+ $values1 = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity1_init->{$this->field_name}[$langcode] = $values1;
+
+ $entity2_init = field_test_create_stub_entity(2, 2, 'test_bundle_2');
+ $values2 = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity2_init->{$this->field_name}[$langcode] = $values2;
+
+ // Run prepare_view, and check that the entities come out as expected.
+ $entity1 = clone($entity1_init);
+ $entity2 = clone($entity2_init);
+ field_attach_prepare_view($entity_type, array($entity1->ftid => $entity1, $entity2->ftid => $entity2), 'full');
+ $this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.');
+ $this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.');
+
+ // Same thing, reversed order.
+ $entity1 = clone($entity1_init);
+ $entity2 = clone($entity2_init);
+ field_attach_prepare_view($entity_type, array($entity2->ftid => $entity2, $entity1->ftid => $entity1), 'full');
+ $this->assertFalse(isset($entity1->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 1 did not run through the prepare_view hook.');
+ $this->assertTrue(isset($entity2->{$this->field_name}[$langcode][0]['additional_formatter_value']), 'Entity 2 ran through the prepare_view hook.');
+ }
+
+ /**
+ * Test field cache.
+ */
+ function testFieldAttachCache() {
+ // Initialize random values and a test entity.
+ $entity_init = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+
+ // Non-cacheable entity type.
+ $entity_type = 'test_entity';
+ $cid = "field:$entity_type:{$entity_init->ftid}";
+
+ // Check that no initial cache entry is present.
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no initial cache entry');
+
+ // Save, and check that no cache entry is present.
+ $entity = clone($entity_init);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_insert($entity_type, $entity);
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on insert');
+
+ // Load, and check that no cache entry is present.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Non-cached: no cache entry on load');
+
+
+ // Cacheable entity type.
+ $entity_type = 'test_cacheable_entity';
+ $cid = "field:$entity_type:{$entity_init->ftid}";
+ $instance = $this->instance;
+ $instance['entity_type'] = $entity_type;
+ field_create_instance($instance);
+
+ // Check that no initial cache entry is present.
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no initial cache entry');
+
+ // Save, and check that no cache entry is present.
+ $entity = clone($entity_init);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_insert($entity_type, $entity);
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on insert');
+
+ // Load a single field, and check that no cache entry is present.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity), FIELD_LOAD_CURRENT, array('field_id' => $this->field_id));
+ $cache = cache_get($cid, 'cache_field');
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on loading a single field');
+
+ // Load, and check that a cache entry is present with the expected values.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $cache = cache_get($cid, 'cache_field');
+ $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
+
+ // Update with different values, and check that the cache entry is wiped.
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity = clone($entity_init);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $entity);
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on update');
+
+ // Load, and check that a cache entry is present with the expected values.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $cache = cache_get($cid, 'cache_field');
+ $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
+
+ // Create a new revision, and check that the cache entry is wiped.
+ $entity_init = field_test_create_stub_entity(1, 2, $this->instance['bundle']);
+ $values = $this->_generateTestFieldValues($this->field['cardinality']);
+ $entity = clone($entity_init);
+ $entity->{$this->field_name}[$langcode] = $values;
+ field_attach_update($entity_type, $entity);
+ $cache = cache_get($cid, 'cache_field');
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry on new revision creation');
+
+ // Load, and check that a cache entry is present with the expected values.
+ $entity = clone($entity_init);
+ field_attach_load($entity_type, array($entity->ftid => $entity));
+ $cache = cache_get($cid, 'cache_field');
+ $this->assertEqual($cache->data[$this->field_name][$langcode], $values, 'Cached: correct cache entry on load');
+
+ // Delete, and check that the cache entry is wiped.
+ field_attach_delete($entity_type, $entity);
+ $this->assertFalse(cache_get($cid, 'cache_field'), 'Cached: no cache entry after delete');
+ }
+
+ /**
+ * Test field_attach_validate().
+ *
+ * Verify that field_attach_validate() invokes the correct
+ * hook_field_validate.
+ */
+ function testFieldAttachValidate() {
+ $this->createFieldWithInstance('_2');
+
+ $entity_type = 'test_entity';
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+
+ // Set up all but one values of the first field to generate errors.
+ $values = array();
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ $values[$delta]['value'] = -1;
+ }
+ // Arrange for item 1 not to generate an error
+ $values[1]['value'] = 1;
+ $entity->{$this->field_name}[$langcode] = $values;
+
+ // Set up all values of the second field to generate errors.
+ $values_2 = array();
+ for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
+ $values_2[$delta]['value'] = -1;
+ }
+ $entity->{$this->field_name_2}[$langcode] = $values_2;
+
+ // Validate all fields.
+ try {
+ field_attach_validate($entity_type, $entity);
+ }
+ catch (FieldValidationException $e) {
+ $errors = $e->errors;
+ }
+
+ foreach ($values as $delta => $value) {
+ if ($value['value'] != 1) {
+ $this->assertIdentical($errors[$this->field_name][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on first field's value $delta");
+ $this->assertEqual(count($errors[$this->field_name][$langcode][$delta]), 1, "Only one error set on first field's value $delta");
+ unset($errors[$this->field_name][$langcode][$delta]);
+ }
+ else {
+ $this->assertFalse(isset($errors[$this->field_name][$langcode][$delta]), "No error set on first field's value $delta");
+ }
+ }
+ foreach ($values_2 as $delta => $value) {
+ $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta");
+ $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta");
+ unset($errors[$this->field_name_2][$langcode][$delta]);
+ }
+ $this->assertEqual(count($errors[$this->field_name][$langcode]), 0, 'No extraneous errors set for first field');
+ $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field');
+
+ // Validate a single field.
+ $options = array('field_name' => $this->field_name_2);
+ try {
+ field_attach_validate($entity_type, $entity, $options);
+ }
+ catch (FieldValidationException $e) {
+ $errors = $e->errors;
+ }
+
+ foreach ($values_2 as $delta => $value) {
+ $this->assertIdentical($errors[$this->field_name_2][$langcode][$delta][0]['error'], 'field_test_invalid', "Error set on second field's value $delta");
+ $this->assertEqual(count($errors[$this->field_name_2][$langcode][$delta]), 1, "Only one error set on second field's value $delta");
+ unset($errors[$this->field_name_2][$langcode][$delta]);
+ }
+ $this->assertFalse(isset($errors[$this->field_name]), 'No validation errors are set for the first field, despite it having errors');
+ $this->assertEqual(count($errors[$this->field_name_2][$langcode]), 0, 'No extraneous errors set for second field');
+
+ // Check that cardinality is validated.
+ $entity->{$this->field_name_2}[$langcode] = $this->_generateTestFieldValues($this->field_2['cardinality'] + 1);
+ // When validating all fields.
+ try {
+ field_attach_validate($entity_type, $entity);
+ }
+ catch (FieldValidationException $e) {
+ $errors = $e->errors;
+ }
+ $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.');
+ // When validating a single field (the second field).
+ try {
+ field_attach_validate($entity_type, $entity, $options);
+ }
+ catch (FieldValidationException $e) {
+ $errors = $e->errors;
+ }
+ $this->assertEqual($errors[$this->field_name_2][$langcode][0][0]['error'], 'field_cardinality', 'Cardinality validation failed.');
+ }
+
+ /**
+ * Test field_attach_form().
+ *
+ * This could be much more thorough, but it does verify that the correct
+ * widgets show up.
+ */
+ function testFieldAttachForm() {
+ $this->createFieldWithInstance('_2');
+
+ $entity_type = 'test_entity';
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+
+ // When generating form for all fields.
+ $form = array();
+ $form_state = form_state_defaults();
+ field_attach_form($entity_type, $entity, $form, $form_state);
+
+ $this->assertEqual($form[$this->field_name][$langcode]['#title'], $this->instance['label'], "First field's form title is {$this->instance['label']}");
+ $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}");
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ // field_test_widget uses 'textfield'
+ $this->assertEqual($form[$this->field_name][$langcode][$delta]['value']['#type'], 'textfield', "First field's form delta $delta widget is textfield");
+ }
+ for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
+ // field_test_widget uses 'textfield'
+ $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
+ }
+
+ // When generating form for a single field (the second field).
+ $options = array('field_name' => $this->field_name_2);
+ $form = array();
+ $form_state = form_state_defaults();
+ field_attach_form($entity_type, $entity, $form, $form_state, NULL, $options);
+
+ $this->assertFalse(isset($form[$this->field_name]), 'The first field does not exist in the form');
+ $this->assertEqual($form[$this->field_name_2][$langcode]['#title'], $this->instance_2['label'], "Second field's form title is {$this->instance_2['label']}");
+ for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
+ // field_test_widget uses 'textfield'
+ $this->assertEqual($form[$this->field_name_2][$langcode][$delta]['value']['#type'], 'textfield', "Second field's form delta $delta widget is textfield");
+ }
+ }
+
+ /**
+ * Test field_attach_submit().
+ */
+ function testFieldAttachSubmit() {
+ $this->createFieldWithInstance('_2');
+
+ $entity_type = 'test_entity';
+ $entity_init = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+
+ // Build the form for all fields.
+ $form = array();
+ $form_state = form_state_defaults();
+ field_attach_form($entity_type, $entity_init, $form, $form_state);
+
+ // Simulate incoming values.
+ // First field.
+ $values = array();
+ $weights = array();
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ $values[$delta]['value'] = mt_rand(1, 127);
+ // Assign random weight.
+ do {
+ $weight = mt_rand(0, $this->field['cardinality']);
+ } while (in_array($weight, $weights));
+ $weights[$delta] = $weight;
+ $values[$delta]['_weight'] = $weight;
+ }
+ // Leave an empty value. 'field_test' fields are empty if empty().
+ $values[1]['value'] = 0;
+ // Second field.
+ $values_2 = array();
+ $weights_2 = array();
+ for ($delta = 0; $delta < $this->field_2['cardinality']; $delta++) {
+ $values_2[$delta]['value'] = mt_rand(1, 127);
+ // Assign random weight.
+ do {
+ $weight = mt_rand(0, $this->field_2['cardinality']);
+ } while (in_array($weight, $weights_2));
+ $weights_2[$delta] = $weight;
+ $values_2[$delta]['_weight'] = $weight;
+ }
+ // Leave an empty value. 'field_test' fields are empty if empty().
+ $values_2[1]['value'] = 0;
+ // Pretend the form has been built.
+ drupal_prepare_form('field_test_entity_form', $form, $form_state);
+ drupal_process_form('field_test_entity_form', $form, $form_state);
+ $form_state['values'][$this->field_name][$langcode] = $values;
+ $form_state['values'][$this->field_name_2][$langcode] = $values_2;
+
+ // Call field_attach_submit() for all fields.
+ $entity = clone($entity_init);
+ field_attach_submit($entity_type, $entity, $form, $form_state);
+
+ asort($weights);
+ asort($weights_2);
+ $expected_values = array();
+ $expected_values_2 = array();
+ foreach ($weights as $key => $value) {
+ if ($key != 1) {
+ $expected_values[] = array('value' => $values[$key]['value']);
+ }
+ }
+ $this->assertIdentical($entity->{$this->field_name}[$langcode], $expected_values, 'Submit filters empty values');
+ foreach ($weights_2 as $key => $value) {
+ if ($key != 1) {
+ $expected_values_2[] = array('value' => $values_2[$key]['value']);
+ }
+ }
+ $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values');
+
+ // Call field_attach_submit() for a single field (the second field).
+ $options = array('field_name' => $this->field_name_2);
+ $entity = clone($entity_init);
+ field_attach_submit($entity_type, $entity, $form, $form_state, $options);
+ $expected_values_2 = array();
+ foreach ($weights_2 as $key => $value) {
+ if ($key != 1) {
+ $expected_values_2[] = array('value' => $values_2[$key]['value']);
+ }
+ }
+ $this->assertFalse(isset($entity->{$this->field_name}), 'The first field does not exist in the entity object');
+ $this->assertIdentical($entity->{$this->field_name_2}[$langcode], $expected_values_2, 'Submit filters empty values');
+ }
+}
+
+class FieldInfoTestCase extends FieldTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field info tests',
+ 'description' => 'Get information about existing fields, instances and bundles.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+ }
+
+ /**
+ * Test that field types and field definitions are correcly cached.
+ */
+ function testFieldInfo() {
+ // Test that field_test module's fields, widgets, and formatters show up.
+
+ $field_test_info = field_test_field_info();
+ // We need to account for the existence of user_field_info_alter().
+ foreach (array_keys($field_test_info) as $name) {
+ $field_test_info[$name]['instance_settings']['user_register_form'] = FALSE;
+ }
+ $info = field_info_field_types();
+ foreach ($field_test_info as $t_key => $field_type) {
+ foreach ($field_type as $key => $val) {
+ $this->assertEqual($info[$t_key][$key], $val, format_string('Field type %t_key key %key is %value', array('%t_key' => $t_key, '%key' => $key, '%value' => print_r($val, TRUE))));
+ }
+ $this->assertEqual($info[$t_key]['module'], 'field_test', "Field type field_test module appears");
+ }
+
+ $formatter_info = field_test_field_formatter_info();
+ $info = field_info_formatter_types();
+ foreach ($formatter_info as $f_key => $formatter) {
+ foreach ($formatter as $key => $val) {
+ $this->assertEqual($info[$f_key][$key], $val, format_string('Formatter type %f_key key %key is %value', array('%f_key' => $f_key, '%key' => $key, '%value' => print_r($val, TRUE))));
+ }
+ $this->assertEqual($info[$f_key]['module'], 'field_test', "Formatter type field_test module appears");
+ }
+
+ $widget_info = field_test_field_widget_info();
+ $info = field_info_widget_types();
+ foreach ($widget_info as $w_key => $widget) {
+ foreach ($widget as $key => $val) {
+ $this->assertEqual($info[$w_key][$key], $val, format_string('Widget type %w_key key %key is %value', array('%w_key' => $w_key, '%key' => $key, '%value' => print_r($val, TRUE))));
+ }
+ $this->assertEqual($info[$w_key]['module'], 'field_test', "Widget type field_test module appears");
+ }
+
+ $storage_info = field_test_field_storage_info();
+ $info = field_info_storage_types();
+ foreach ($storage_info as $s_key => $storage) {
+ foreach ($storage as $key => $val) {
+ $this->assertEqual($info[$s_key][$key], $val, format_string('Storage type %s_key key %key is %value', array('%s_key' => $s_key, '%key' => $key, '%value' => print_r($val, TRUE))));
+ }
+ $this->assertEqual($info[$s_key]['module'], 'field_test', "Storage type field_test module appears");
+ }
+
+ // Verify that no unexpected instances exist.
+ $instances = field_info_instances('test_entity');
+ $expected = array('test_bundle' => array());
+ $this->assertIdentical($instances, $expected, format_string("field_info_instances('test_entity') returns %expected.", array('%expected' => var_export($expected, TRUE))));
+ $instances = field_info_instances('test_entity', 'test_bundle');
+ $this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'test_bundle') returns an empty array.");
+
+ // Create a field, verify it shows up.
+ $core_fields = field_info_fields();
+ $field = array(
+ 'field_name' => drupal_strtolower($this->randomName()),
+ 'type' => 'test_field',
+ );
+ field_create_field($field);
+ $fields = field_info_fields();
+ $this->assertEqual(count($fields), count($core_fields) + 1, 'One new field exists');
+ $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], 'info fields contains field name');
+ $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], 'info fields contains field type');
+ $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', 'info fields contains field module');
+ $settings = array('test_field_setting' => 'dummy test string');
+ foreach ($settings as $key => $val) {
+ $this->assertEqual($fields[$field['field_name']]['settings'][$key], $val, format_string('Field setting %key has correct default value %value', array('%key' => $key, '%value' => $val)));
+ }
+ $this->assertEqual($fields[$field['field_name']]['cardinality'], 1, 'info fields contains cardinality 1');
+ $this->assertEqual($fields[$field['field_name']]['active'], 1, 'info fields contains active 1');
+
+ // Create an instance, verify that it shows up
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->randomName(),
+ 'description' => $this->randomName(),
+ 'weight' => mt_rand(0, 127),
+ // test_field has no instance settings
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ 'settings' => array(
+ 'test_setting' => 999)));
+ field_create_instance($instance);
+
+ $info = entity_get_info('test_entity');
+ $instances = field_info_instances('test_entity', $instance['bundle']);
+ $this->assertEqual(count($instances), 1, format_string('One instance shows up in info when attached to a bundle on a @label.', array(
+ '@label' => $info['label']
+ )));
+ $this->assertTrue($instance < $instances[$instance['field_name']], 'Instance appears in info correctly');
+
+ // Test a valid entity type but an invalid bundle.
+ $instances = field_info_instances('test_entity', 'invalid_bundle');
+ $this->assertIdentical($instances, array(), "field_info_instances('test_entity', 'invalid_bundle') returns an empty array.");
+
+ // Test invalid entity type and bundle.
+ $instances = field_info_instances('invalid_entity', $instance['bundle']);
+ $this->assertIdentical($instances, array(), "field_info_instances('invalid_entity', 'test_bundle') returns an empty array.");
+
+ // Test invalid entity type, no bundle provided.
+ $instances = field_info_instances('invalid_entity');
+ $this->assertIdentical($instances, array(), "field_info_instances('invalid_entity') returns an empty array.");
+
+ // Test with an entity type that has no bundles.
+ $instances = field_info_instances('user');
+ $expected = array('user' => array());
+ $this->assertIdentical($instances, $expected, format_string("field_info_instances('user') returns %expected.", array('%expected' => var_export($expected, TRUE))));
+ $instances = field_info_instances('user', 'user');
+ $this->assertIdentical($instances, array(), "field_info_instances('user', 'user') returns an empty array.");
+
+ // Test that querying for invalid entity types does not add entries in the
+ // list returned by field_info_instances().
+ field_info_cache_clear();
+ field_info_instances('invalid_entity', 'invalid_bundle');
+ // Simulate new request by clearing static caches.
+ drupal_static_reset();
+ field_info_instances('invalid_entity', 'invalid_bundle');
+ $instances = field_info_instances();
+ $this->assertFalse(isset($instances['invalid_entity']), 'field_info_instances() does not contain entries for the invalid entity type that was queried before');
+ }
+
+ /**
+ * Test that cached field definitions are ready for current runtime context.
+ */
+ function testFieldPrepare() {
+ $field_definition = array(
+ 'field_name' => 'field',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+
+ // Simulate a stored field definition missing a field setting (e.g. a
+ // third-party module adding a new field setting has been enabled, and
+ // existing fields do not know the setting yet).
+ $data = db_query('SELECT data FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name']))->fetchField();
+ $data = unserialize($data);
+ $data['settings'] = array();
+ db_update('field_config')
+ ->fields(array('data' => serialize($data)))
+ ->condition('field_name', $field_definition['field_name'])
+ ->execute();
+
+ field_cache_clear();
+
+ // Read the field back.
+ $field = field_info_field($field_definition['field_name']);
+
+ // Check that all expected settings are in place.
+ $field_type = field_info_field_types($field_definition['type']);
+ $this->assertIdentical($field['settings'], $field_type['settings'], 'All expected default field settings are present.');
+ }
+
+ /**
+ * Test that cached instance definitions are ready for current runtime context.
+ */
+ function testInstancePrepare() {
+ $field_definition = array(
+ 'field_name' => 'field',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $instance_definition = array(
+ 'field_name' => $field_definition['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance_definition);
+
+ // Simulate a stored instance definition missing various settings (e.g. a
+ // third-party module adding instance, widget or display settings has been
+ // enabled, but existing instances do not know the new settings).
+ $data = db_query('SELECT data FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $instance_definition['field_name'], ':bundle' => $instance_definition['bundle']))->fetchField();
+ $data = unserialize($data);
+ $data['settings'] = array();
+ $data['widget']['settings'] = 'unavailable_widget';
+ $data['widget']['settings'] = array();
+ $data['display']['default']['type'] = 'unavailable_formatter';
+ $data['display']['default']['settings'] = array();
+ db_update('field_config_instance')
+ ->fields(array('data' => serialize($data)))
+ ->condition('field_name', $instance_definition['field_name'])
+ ->condition('bundle', $instance_definition['bundle'])
+ ->execute();
+
+ field_cache_clear();
+
+ // Read the instance back.
+ $instance = field_info_instance($instance_definition['entity_type'], $instance_definition['field_name'], $instance_definition['bundle']);
+
+ // Check that all expected instance settings are in place.
+ $field_type = field_info_field_types($field_definition['type']);
+ $this->assertIdentical($instance['settings'], $field_type['instance_settings'] , 'All expected instance settings are present.');
+
+ // Check that the default widget is used and expected settings are in place.
+ $this->assertIdentical($instance['widget']['type'], $field_type['default_widget'], 'Unavailable widget replaced with default widget.');
+ $widget_type = field_info_widget_types($instance['widget']['type']);
+ $this->assertIdentical($instance['widget']['settings'], $widget_type['settings'] , 'All expected widget settings are present.');
+
+ // Check that display settings are set for the 'default' mode.
+ $display = $instance['display']['default'];
+ $this->assertIdentical($display['type'], $field_type['default_formatter'], "Formatter is set for the 'default' view mode");
+ $formatter_type = field_info_formatter_types($display['type']);
+ $this->assertIdentical($display['settings'], $formatter_type['settings'] , "Formatter settings are set for the 'default' view mode");
+ }
+
+ /**
+ * Test that instances on disabled entity types are filtered out.
+ */
+ function testInstanceDisabledEntityType() {
+ // For this test the field type and the entity type must be exposed by
+ // different modules.
+ $field_definition = array(
+ 'field_name' => 'field',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $instance_definition = array(
+ 'field_name' => 'field',
+ 'entity_type' => 'comment',
+ 'bundle' => 'comment_node_article',
+ );
+ field_create_instance($instance_definition);
+
+ // Disable coment module. This clears field_info cache.
+ module_disable(array('comment'));
+ $this->assertNull(field_info_instance('comment', 'field', 'comment_node_article'), 'No instances are returned on disabled entity types.');
+ }
+
+ /**
+ * Test field_info_field_map().
+ */
+ function testFieldMap() {
+ // We will overlook fields created by the 'standard' install profile.
+ $exclude = field_info_field_map();
+
+ // Create a new bundle for 'test_entity' entity type.
+ field_test_create_bundle('test_bundle_2');
+
+ // Create a couple fields.
+ $fields = array(
+ array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ ),
+ array(
+ 'field_name' => 'field_2',
+ 'type' => 'hidden_test_field',
+ ),
+ );
+ foreach ($fields as $field) {
+ field_create_field($field);
+ }
+
+ // Create a couple instances.
+ $instances = array(
+ array(
+ 'field_name' => 'field_1',
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ ),
+ array(
+ 'field_name' => 'field_1',
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle_2',
+ ),
+ array(
+ 'field_name' => 'field_2',
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ ),
+ array(
+ 'field_name' => 'field_2',
+ 'entity_type' => 'test_cacheable_entity',
+ 'bundle' => 'test_bundle',
+ ),
+ );
+ foreach ($instances as $instance) {
+ field_create_instance($instance);
+ }
+
+ $expected = array(
+ 'field_1' => array(
+ 'type' => 'test_field',
+ 'bundles' => array(
+ 'test_entity' => array('test_bundle', 'test_bundle_2'),
+ ),
+ ),
+ 'field_2' => array(
+ 'type' => 'hidden_test_field',
+ 'bundles' => array(
+ 'test_entity' => array('test_bundle'),
+ 'test_cacheable_entity' => array('test_bundle'),
+ ),
+ ),
+ );
+
+ // Check that the field map is correct.
+ $map = field_info_field_map();
+ $map = array_diff_key($map, $exclude);
+ $this->assertEqual($map, $expected);
+ }
+
+ /**
+ * Test that the field_info settings convenience functions work.
+ */
+ function testSettingsInfo() {
+ $info = field_test_field_info();
+ // We need to account for the existence of user_field_info_alter().
+ foreach (array_keys($info) as $name) {
+ $info[$name]['instance_settings']['user_register_form'] = FALSE;
+ }
+ foreach ($info as $type => $data) {
+ $this->assertIdentical(field_info_field_settings($type), $data['settings'], format_string("field_info_field_settings returns %type's field settings", array('%type' => $type)));
+ $this->assertIdentical(field_info_instance_settings($type), $data['instance_settings'], format_string("field_info_field_settings returns %type's field instance settings", array('%type' => $type)));
+ }
+
+ $info = field_test_field_widget_info();
+ foreach ($info as $type => $data) {
+ $this->assertIdentical(field_info_widget_settings($type), $data['settings'], format_string("field_info_widget_settings returns %type's widget settings", array('%type' => $type)));
+ }
+
+ $info = field_test_field_formatter_info();
+ foreach ($info as $type => $data) {
+ $this->assertIdentical(field_info_formatter_settings($type), $data['settings'], format_string("field_info_formatter_settings returns %type's formatter settings", array('%type' => $type)));
+ }
+ }
+
+ /**
+ * Tests that the field info cache can be built correctly.
+ */
+ function testFieldInfoCache() {
+ // Create a test field and ensure it's in the array returned by
+ // field_info_fields().
+ $field_name = drupal_strtolower($this->randomName());
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'test_field',
+ );
+ field_create_field($field);
+ $fields = field_info_fields();
+ $this->assertTrue(isset($fields[$field_name]), 'The test field is initially found in the array returned by field_info_fields().');
+
+ // Now rebuild the field info cache, and set a variable which will cause
+ // the cache to be cleared while it's being rebuilt; see
+ // field_test_entity_info(). Ensure the test field is still in the returned
+ // array.
+ field_info_cache_clear();
+ variable_set('field_test_clear_info_cache_in_hook_entity_info', TRUE);
+ $fields = field_info_fields();
+ $this->assertTrue(isset($fields[$field_name]), 'The test field is found in the array returned by field_info_fields() even if its cache is cleared while being rebuilt.');
+ }
+}
+
+class FieldFormTestCase extends FieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field form tests',
+ 'description' => 'Test Field form handling.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+
+ $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
+ $this->drupalLogin($web_user);
+
+ $this->field_single = array('field_name' => 'field_single', 'type' => 'test_field');
+ $this->field_multiple = array('field_name' => 'field_multiple', 'type' => 'test_field', 'cardinality' => 4);
+ $this->field_unlimited = array('field_name' => 'field_unlimited', 'type' => 'test_field', 'cardinality' => FIELD_CARDINALITY_UNLIMITED);
+
+ $this->instance = array(
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->randomName() . '_label',
+ 'description' => $this->randomName() . '_description',
+ 'weight' => mt_rand(0, 127),
+ 'settings' => array(
+ 'test_instance_setting' => $this->randomName(),
+ ),
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ 'label' => 'Test Field',
+ 'settings' => array(
+ 'test_widget_setting' => $this->randomName(),
+ )
+ )
+ );
+ }
+
+ function testFieldFormSingle() {
+ $this->field = $this->field_single;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Display creation form.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget is displayed');
+ $this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
+ // TODO : check that the widget is populated with default value ?
+
+ // Submit with invalid value (field-level validation).
+ $edit = array("{$this->field_name}[$langcode][0][value]" => -1);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw(t('%name does not accept the value -1.', array('%name' => $this->instance['label'])), 'Field validation fails with invalid input.');
+ // TODO : check that the correct field is flagged for error.
+
+ // Create an entity
+ $value = mt_rand(1, 127);
+ $edit = array("{$this->field_name}[$langcode][0][value]" => $value);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+ $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
+ $entity = field_test_entity_test_load($id);
+ $this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was saved');
+
+ // Display edit form.
+ $this->drupalGet('test-entity/manage/' . $id . '/edit');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", $value, 'Widget is displayed with the correct default value');
+ $this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
+
+ // Update the entity.
+ $value = mt_rand(1, 127);
+ $edit = array("{$this->field_name}[$langcode][0][value]" => $value);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
+ $entity = field_test_entity_test_load($id);
+ $this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was updated');
+
+ // Empty the field.
+ $value = '';
+ $edit = array("{$this->field_name}[$langcode][0][value]" => $value);
+ $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
+ $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), 'Entity was updated');
+ $entity = field_test_entity_test_load($id);
+ $this->assertIdentical($entity->{$this->field_name}, array(), 'Field was emptied');
+
+ }
+
+ function testFieldFormSingleRequired() {
+ $this->field = $this->field_single;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ $this->instance['required'] = TRUE;
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Submit with missing required value.
+ $edit = array();
+ $this->drupalPost('test-entity/add/test-bundle', $edit, t('Save'));
+ $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation');
+
+ // Create an entity
+ $value = mt_rand(1, 127);
+ $edit = array("{$this->field_name}[$langcode][0][value]" => $value);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+ $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
+ $entity = field_test_entity_test_load($id);
+ $this->assertEqual($entity->{$this->field_name}[$langcode][0]['value'], $value, 'Field value was saved');
+
+ // Edit with missing required value.
+ $value = '';
+ $edit = array("{$this->field_name}[$langcode][0][value]" => $value);
+ $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
+ $this->assertRaw(t('!name field is required.', array('!name' => $this->instance['label'])), 'Required field with no value fails validation');
+ }
+
+// function testFieldFormMultiple() {
+// $this->field = $this->field_multiple;
+// $this->field_name = $this->field['field_name'];
+// $this->instance['field_name'] = $this->field_name;
+// field_create_field($this->field);
+// field_create_instance($this->instance);
+// }
+
+ function testFieldFormUnlimited() {
+ $this->field = $this->field_unlimited;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Display creation form -> 1 widget.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
+ $this->assertNoField("{$this->field_name}[$langcode][1][value]", 'No extraneous widget is displayed');
+
+ // Press 'add more' button -> 2 widgets.
+ $this->drupalPost(NULL, array(), t('Add another item'));
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
+ $this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed');
+ $this->assertNoField("{$this->field_name}[$langcode][2][value]", 'No extraneous widget is displayed');
+ // TODO : check that non-field inpurs are preserved ('title')...
+
+ // Yet another time so that we can play with more values -> 3 widgets.
+ $this->drupalPost(NULL, array(), t('Add another item'));
+
+ // Prepare values and weights.
+ $count = 3;
+ $delta_range = $count - 1;
+ $values = $weights = $pattern = $expected_values = $edit = array();
+ for ($delta = 0; $delta <= $delta_range; $delta++) {
+ // Assign unique random values and weights.
+ do {
+ $value = mt_rand(1, 127);
+ } while (in_array($value, $values));
+ do {
+ $weight = mt_rand(-$delta_range, $delta_range);
+ } while (in_array($weight, $weights));
+ $edit["$this->field_name[$langcode][$delta][value]"] = $value;
+ $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
+ // We'll need three slightly different formats to check the values.
+ $values[$delta] = $value;
+ $weights[$delta] = $weight;
+ $field_values[$weight]['value'] = (string) $value;
+ $pattern[$weight] = "]*value=\"$value\" [^>]*";
+ }
+
+ // Press 'add more' button -> 4 widgets
+ $this->drupalPost(NULL, $edit, t('Add another item'));
+ for ($delta = 0; $delta <= $delta_range; $delta++) {
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
+ }
+ ksort($pattern);
+ $pattern = implode('.*', array_values($pattern));
+ $this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", '', "New widget is displayed");
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight");
+ $this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
+
+ // Submit the form and create the entity.
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+ $this->assertRaw(t('test_entity @id has been created.', array('@id' => $id)), 'Entity was created');
+ $entity = field_test_entity_test_load($id);
+ ksort($field_values);
+ $field_values = array_values($field_values);
+ $this->assertIdentical($entity->{$this->field_name}[$langcode], $field_values, 'Field values were saved in the correct order');
+
+ // Display edit form: check that the expected number of widgets is
+ // displayed, with correct values change values, reorder, leave an empty
+ // value in the middle.
+ // Submit: check that the entity is updated with correct values
+ // Re-submit: check that the field can be emptied.
+
+ // Test with several multiple fields in a form
+ }
+
+ /**
+ * Tests widget handling of multiple required radios.
+ */
+ function testFieldFormMultivalueWithRequiredRadio() {
+ // Create a multivalue test field.
+ $this->field = $this->field_unlimited;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Add a required radio field.
+ field_create_field(array(
+ 'field_name' => 'required_radio_test',
+ 'type' => 'list_text',
+ 'settings' => array(
+ 'allowed_values' => array('yes' => 'yes', 'no' => 'no'),
+ ),
+ ));
+ field_create_instance(array(
+ 'field_name' => 'required_radio_test',
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'required' => TRUE,
+ 'widget' => array(
+ 'type' => 'options_buttons',
+ ),
+ ));
+
+ // Display creation form.
+ $this->drupalGet('test-entity/add/test-bundle');
+
+ // Press the 'Add more' button.
+ $this->drupalPost(NULL, array(), t('Add another item'));
+
+ // Verify that no error is thrown by the radio element.
+ $this->assertNoFieldByXpath('//div[contains(@class, "error")]', FALSE, 'No error message is displayed.');
+
+ // Verify that the widget is added.
+ $this->assertFieldByName("{$this->field_name}[$langcode][0][value]", '', 'Widget 1 is displayed');
+ $this->assertFieldByName("{$this->field_name}[$langcode][1][value]", '', 'New widget is displayed');
+ $this->assertNoField("{$this->field_name}[$langcode][2][value]", 'No extraneous widget is displayed');
+ }
+
+ function testFieldFormJSAddMore() {
+ $this->field = $this->field_unlimited;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Display creation form -> 1 widget.
+ $this->drupalGet('test-entity/add/test-bundle');
+
+ // Press 'add more' button a couple times -> 3 widgets.
+ // drupalPostAJAX() will not work iteratively, so we add those through
+ // non-JS submission.
+ $this->drupalPost(NULL, array(), t('Add another item'));
+ $this->drupalPost(NULL, array(), t('Add another item'));
+
+ // Prepare values and weights.
+ $count = 3;
+ $delta_range = $count - 1;
+ $values = $weights = $pattern = $expected_values = $edit = array();
+ for ($delta = 0; $delta <= $delta_range; $delta++) {
+ // Assign unique random values and weights.
+ do {
+ $value = mt_rand(1, 127);
+ } while (in_array($value, $values));
+ do {
+ $weight = mt_rand(-$delta_range, $delta_range);
+ } while (in_array($weight, $weights));
+ $edit["$this->field_name[$langcode][$delta][value]"] = $value;
+ $edit["$this->field_name[$langcode][$delta][_weight]"] = $weight;
+ // We'll need three slightly different formats to check the values.
+ $values[$delta] = $value;
+ $weights[$delta] = $weight;
+ $field_values[$weight]['value'] = (string) $value;
+ $pattern[$weight] = "]*value=\"$value\" [^>]*";
+ }
+ // Press 'add more' button through Ajax, and place the expected HTML result
+ // as the tested content.
+ $commands = $this->drupalPostAJAX(NULL, $edit, $this->field_name . '_add_more');
+ $this->content = $commands[1]['data'];
+
+ for ($delta = 0; $delta <= $delta_range; $delta++) {
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", $values[$delta], "Widget $delta is displayed and has the right value");
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $weights[$delta], "Widget $delta has the right weight");
+ }
+ ksort($pattern);
+ $pattern = implode('.*', array_values($pattern));
+ $this->assertPattern("|$pattern|s", 'Widgets are displayed in the correct order');
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][value]", '', "New widget is displayed");
+ $this->assertFieldByName("$this->field_name[$langcode][$delta][_weight]", $delta, "New widget has the right weight");
+ $this->assertNoField("$this->field_name[$langcode][" . ($delta + 1) . '][value]', 'No extraneous widget is displayed');
+ }
+
+ /**
+ * Tests widgets handling multiple values.
+ */
+ function testFieldFormMultipleWidget() {
+ // Create a field with fixed cardinality and an instance using a multiple
+ // widget.
+ $this->field = $this->field_multiple;
+ $this->field_name = $this->field['field_name'];
+ $this->instance['field_name'] = $this->field_name;
+ $this->instance['widget']['type'] = 'test_field_widget_multiple';
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+ $langcode = LANGUAGE_NONE;
+
+ // Display creation form.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertFieldByName("{$this->field_name}[$langcode]", '', 'Widget is displayed.');
+
+ // Create entity with three values.
+ $edit = array("{$this->field_name}[$langcode]" => '1, 2, 3');
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+
+ // Check that the values were saved.
+ $entity_init = field_test_create_stub_entity($id);
+ $this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3));
+
+ // Display the form, check that the values are correctly filled in.
+ $this->drupalGet('test-entity/manage/' . $id . '/edit');
+ $this->assertFieldByName("{$this->field_name}[$langcode]", '1, 2, 3', 'Widget is displayed.');
+
+ // Submit the form with more values than the field accepts.
+ $edit = array("{$this->field_name}[$langcode]" => '1, 2, 3, 4, 5');
+ $this->drupalPost(NULL, $edit, t('Save'));
+ $this->assertRaw('this field cannot hold more than 4 values', 'Form validation failed.');
+ // Check that the field values were not submitted.
+ $this->assertFieldValues($entity_init, $this->field_name, $langcode, array(1, 2, 3));
+ }
+
+ /**
+ * Tests fields with no 'edit' access.
+ */
+ function testFieldFormAccess() {
+ // Create a "regular" field.
+ $field = $this->field_single;
+ $field_name = $field['field_name'];
+ $instance = $this->instance;
+ $instance['field_name'] = $field_name;
+ field_create_field($field);
+ field_create_instance($instance);
+
+ // Create a field with no edit access - see field_test_field_access().
+ $field_no_access = array(
+ 'field_name' => 'field_no_edit_access',
+ 'type' => 'test_field',
+ );
+ $field_name_no_access = $field_no_access['field_name'];
+ $instance_no_access = array(
+ 'field_name' => $field_name_no_access,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'default_value' => array(0 => array('value' => 99)),
+ );
+ field_create_field($field_no_access);
+ field_create_instance($instance_no_access);
+
+ $langcode = LANGUAGE_NONE;
+
+ // Test that the form structure includes full information for each delta
+ // apart from #access.
+ $entity_type = 'test_entity';
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+
+ $form = array();
+ $form_state = form_state_defaults();
+ field_attach_form($entity_type, $entity, $form, $form_state);
+
+ $this->assertEqual($form[$field_name_no_access][$langcode][0]['value']['#entity_type'], $entity_type, 'The correct entity type is set in the field structure.');
+ $this->assertFalse($form[$field_name_no_access]['#access'], 'Field #access is FALSE for the field without edit access.');
+
+ // Display creation form.
+ $this->drupalGet('test-entity/add/test-bundle');
+ $this->assertNoFieldByName("{$field_name_no_access}[$langcode][0][value]", '', 'Widget is not displayed if field access is denied.');
+
+ // Create entity.
+ $edit = array("{$field_name}[$langcode][0][value]" => 1);
+ $this->drupalPost(NULL, $edit, t('Save'));
+ preg_match('|test-entity/manage/(\d+)/edit|', $this->url, $match);
+ $id = $match[1];
+
+ // Check that the default value was saved.
+ $entity = field_test_entity_test_load($id);
+ $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'Default value was saved for the field with no edit access.');
+ $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 1, 'Entered value vas saved for the field with edit access.');
+
+ // Create a new revision.
+ $edit = array("{$field_name}[$langcode][0][value]" => 2, 'revision' => TRUE);
+ $this->drupalPost('test-entity/manage/' . $id . '/edit', $edit, t('Save'));
+
+ // Check that the new revision has the expected values.
+ $entity = field_test_entity_test_load($id);
+ $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.');
+ $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.');
+
+ // Check that the revision is also saved in the revisions table.
+ $entity = field_test_entity_test_load($id, $entity->ftvid);
+ $this->assertEqual($entity->{$field_name_no_access}[$langcode][0]['value'], 99, 'New revision has the expected value for the field with no edit access.');
+ $this->assertEqual($entity->{$field_name}[$langcode][0]['value'], 2, 'New revision has the expected value for the field with edit access.');
+ }
+
+ /**
+ * Tests Field API form integration within a subform.
+ */
+ function testNestedFieldForm() {
+ // Add two instances on the 'test_bundle'
+ field_create_field($this->field_single);
+ field_create_field($this->field_unlimited);
+ $this->instance['field_name'] = 'field_single';
+ $this->instance['label'] = 'Single field';
+ field_create_instance($this->instance);
+ $this->instance['field_name'] = 'field_unlimited';
+ $this->instance['label'] = 'Unlimited field';
+ field_create_instance($this->instance);
+
+ // Create two entities.
+ $entity_1 = field_test_create_stub_entity(1, 1);
+ $entity_1->is_new = TRUE;
+ $entity_1->field_single[LANGUAGE_NONE][] = array('value' => 0);
+ $entity_1->field_unlimited[LANGUAGE_NONE][] = array('value' => 1);
+ field_test_entity_save($entity_1);
+
+ $entity_2 = field_test_create_stub_entity(2, 2);
+ $entity_2->is_new = TRUE;
+ $entity_2->field_single[LANGUAGE_NONE][] = array('value' => 10);
+ $entity_2->field_unlimited[LANGUAGE_NONE][] = array('value' => 11);
+ field_test_entity_save($entity_2);
+
+ // Display the 'combined form'.
+ $this->drupalGet('test-entity/nested/1/2');
+ $this->assertFieldByName('field_single[und][0][value]', 0, 'Entity 1: field_single value appears correctly is the form.');
+ $this->assertFieldByName('field_unlimited[und][0][value]', 1, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
+ $this->assertFieldByName('entity_2[field_single][und][0][value]', 10, 'Entity 2: field_single value appears correctly is the form.');
+ $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 11, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
+
+ // Submit the form and check that the entities are updated accordingly.
+ $edit = array(
+ 'field_single[und][0][value]' => 1,
+ 'field_unlimited[und][0][value]' => 2,
+ 'field_unlimited[und][1][value]' => 3,
+ 'entity_2[field_single][und][0][value]' => 11,
+ 'entity_2[field_unlimited][und][0][value]' => 12,
+ 'entity_2[field_unlimited][und][1][value]' => 13,
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+ field_cache_clear();
+ $entity_1 = field_test_create_stub_entity(1);
+ $entity_2 = field_test_create_stub_entity(2);
+ $this->assertFieldValues($entity_1, 'field_single', LANGUAGE_NONE, array(1));
+ $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(2, 3));
+ $this->assertFieldValues($entity_2, 'field_single', LANGUAGE_NONE, array(11));
+ $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(12, 13));
+
+ // Submit invalid values and check that errors are reported on the
+ // correct widgets.
+ $edit = array(
+ 'field_unlimited[und][1][value]' => -1,
+ );
+ $this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
+ $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 1: the field validation error was reported.');
+ $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-field-unlimited-und-1-value'));
+ $this->assertTrue($error_field, 'Entity 1: the error was flagged on the correct element.');
+ $edit = array(
+ 'entity_2[field_unlimited][und][1][value]' => -1,
+ );
+ $this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
+ $this->assertRaw(t('%label does not accept the value -1', array('%label' => 'Unlimited field')), 'Entity 2: the field validation error was reported.');
+ $error_field = $this->xpath('//input[@id=:id and contains(@class, "error")]', array(':id' => 'edit-entity-2-field-unlimited-und-1-value'));
+ $this->assertTrue($error_field, 'Entity 2: the error was flagged on the correct element.');
+
+ // Test that reordering works on both entities.
+ $edit = array(
+ 'field_unlimited[und][0][_weight]' => 0,
+ 'field_unlimited[und][1][_weight]' => -1,
+ 'entity_2[field_unlimited][und][0][_weight]' => 0,
+ 'entity_2[field_unlimited][und][1][_weight]' => -1,
+ );
+ $this->drupalPost('test-entity/nested/1/2', $edit, t('Save'));
+ field_cache_clear();
+ $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2));
+ $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 12));
+
+ // Test the 'add more' buttons. Only Ajax submission is tested, because
+ // the two 'add more' buttons present in the form have the same #value,
+ // which confuses drupalPost().
+ // 'Add more' button in the first entity:
+ $this->drupalGet('test-entity/nested/1/2');
+ $this->drupalPostAJAX(NULL, array(), 'field_unlimited_add_more');
+ $this->assertFieldByName('field_unlimited[und][0][value]', 3, 'Entity 1: field_unlimited value 0 appears correctly is the form.');
+ $this->assertFieldByName('field_unlimited[und][1][value]', 2, 'Entity 1: field_unlimited value 1 appears correctly is the form.');
+ $this->assertFieldByName('field_unlimited[und][2][value]', '', 'Entity 1: field_unlimited value 2 appears correctly is the form.');
+ $this->assertFieldByName('field_unlimited[und][3][value]', '', 'Entity 1: an empty widget was added for field_unlimited value 3.');
+ // 'Add more' button in the first entity (changing field values):
+ $edit = array(
+ 'entity_2[field_unlimited][und][0][value]' => 13,
+ 'entity_2[field_unlimited][und][1][value]' => 14,
+ 'entity_2[field_unlimited][und][2][value]' => 15,
+ );
+ $this->drupalPostAJAX(NULL, $edit, 'entity_2_field_unlimited_add_more');
+ $this->assertFieldByName('entity_2[field_unlimited][und][0][value]', 13, 'Entity 2: field_unlimited value 0 appears correctly is the form.');
+ $this->assertFieldByName('entity_2[field_unlimited][und][1][value]', 14, 'Entity 2: field_unlimited value 1 appears correctly is the form.');
+ $this->assertFieldByName('entity_2[field_unlimited][und][2][value]', 15, 'Entity 2: field_unlimited value 2 appears correctly is the form.');
+ $this->assertFieldByName('entity_2[field_unlimited][und][3][value]', '', 'Entity 2: an empty widget was added for field_unlimited value 3.');
+ // Save the form and check values are saved correclty.
+ $this->drupalPost(NULL, array(), t('Save'));
+ field_cache_clear();
+ $this->assertFieldValues($entity_1, 'field_unlimited', LANGUAGE_NONE, array(3, 2));
+ $this->assertFieldValues($entity_2, 'field_unlimited', LANGUAGE_NONE, array(13, 14, 15));
+ }
+}
+
+class FieldDisplayAPITestCase extends FieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field Display API tests',
+ 'description' => 'Test the display API.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+
+ // Create a field and instance.
+ $this->field_name = 'test_field';
+ $this->label = $this->randomName();
+ $this->cardinality = 4;
+
+ $this->field = array(
+ 'field_name' => $this->field_name,
+ 'type' => 'test_field',
+ 'cardinality' => $this->cardinality,
+ );
+ $this->instance = array(
+ 'field_name' => $this->field_name,
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'label' => $this->label,
+ 'display' => array(
+ 'default' => array(
+ 'type' => 'field_test_default',
+ 'settings' => array(
+ 'test_formatter_setting' => $this->randomName(),
+ ),
+ ),
+ 'teaser' => array(
+ 'type' => 'field_test_default',
+ 'settings' => array(
+ 'test_formatter_setting' => $this->randomName(),
+ ),
+ ),
+ ),
+ );
+ field_create_field($this->field);
+ field_create_instance($this->instance);
+
+ // Create an entity with values.
+ $this->values = $this->_generateTestFieldValues($this->cardinality);
+ $this->entity = field_test_create_stub_entity();
+ $this->is_new = TRUE;
+ $this->entity->{$this->field_name}[LANGUAGE_NONE] = $this->values;
+ field_test_entity_save($this->entity);
+ }
+
+ /**
+ * Test the field_view_field() function.
+ */
+ function testFieldViewField() {
+ // No display settings: check that default display settings are used.
+ $output = field_view_field('test_entity', $this->entity, $this->field_name);
+ $this->drupalSetContent(drupal_render($output));
+ $settings = field_info_formatter_settings('field_test_default');
+ $setting = $settings['test_formatter_setting'];
+ $this->assertText($this->label, 'Label was displayed.');
+ foreach ($this->values as $delta => $value) {
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // Check that explicit display settings are used.
+ $display = array(
+ 'label' => 'hidden',
+ 'type' => 'field_test_multiple',
+ 'settings' => array(
+ 'test_formatter_setting_multiple' => $this->randomName(),
+ 'alter' => TRUE,
+ ),
+ );
+ $output = field_view_field('test_entity', $this->entity, $this->field_name, $display);
+ $this->drupalSetContent(drupal_render($output));
+ $setting = $display['settings']['test_formatter_setting_multiple'];
+ $this->assertNoText($this->label, 'Label was not displayed.');
+ $this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.');
+ $array = array();
+ foreach ($this->values as $delta => $value) {
+ $array[] = $delta . ':' . $value['value'];
+ }
+ $this->assertText($setting . '|' . implode('|', $array), 'Values were displayed with expected setting.');
+
+ // Check the prepare_view steps are invoked.
+ $display = array(
+ 'label' => 'hidden',
+ 'type' => 'field_test_with_prepare_view',
+ 'settings' => array(
+ 'test_formatter_setting_additional' => $this->randomName(),
+ ),
+ );
+ $output = field_view_field('test_entity', $this->entity, $this->field_name, $display);
+ $view = drupal_render($output);
+ $this->drupalSetContent($view);
+ $setting = $display['settings']['test_formatter_setting_additional'];
+ $this->assertNoText($this->label, 'Label was not displayed.');
+ $this->assertNoText('field_test_field_attach_view_alter', 'Alter not fired.');
+ foreach ($this->values as $delta => $value) {
+ $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // View mode: check that display settings specified in the instance are
+ // used.
+ $output = field_view_field('test_entity', $this->entity, $this->field_name, 'teaser');
+ $this->drupalSetContent(drupal_render($output));
+ $setting = $this->instance['display']['teaser']['settings']['test_formatter_setting'];
+ $this->assertText($this->label, 'Label was displayed.');
+ foreach ($this->values as $delta => $value) {
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // Unknown view mode: check that display settings for 'default' view mode
+ // are used.
+ $output = field_view_field('test_entity', $this->entity, $this->field_name, 'unknown_view_mode');
+ $this->drupalSetContent(drupal_render($output));
+ $setting = $this->instance['display']['default']['settings']['test_formatter_setting'];
+ $this->assertText($this->label, 'Label was displayed.');
+ foreach ($this->values as $delta => $value) {
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+ }
+
+ /**
+ * Test the field_view_value() function.
+ */
+ function testFieldViewValue() {
+ // No display settings: check that default display settings are used.
+ $settings = field_info_formatter_settings('field_test_default');
+ $setting = $settings['test_formatter_setting'];
+ foreach ($this->values as $delta => $value) {
+ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
+ $output = field_view_value('test_entity', $this->entity, $this->field_name, $item);
+ $this->drupalSetContent(drupal_render($output));
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // Check that explicit display settings are used.
+ $display = array(
+ 'type' => 'field_test_multiple',
+ 'settings' => array(
+ 'test_formatter_setting_multiple' => $this->randomName(),
+ ),
+ );
+ $setting = $display['settings']['test_formatter_setting_multiple'];
+ $array = array();
+ foreach ($this->values as $delta => $value) {
+ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
+ $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display);
+ $this->drupalSetContent(drupal_render($output));
+ $this->assertText($setting . '|0:' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // Check that prepare_view steps are invoked.
+ $display = array(
+ 'type' => 'field_test_with_prepare_view',
+ 'settings' => array(
+ 'test_formatter_setting_additional' => $this->randomName(),
+ ),
+ );
+ $setting = $display['settings']['test_formatter_setting_additional'];
+ $array = array();
+ foreach ($this->values as $delta => $value) {
+ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
+ $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, $display);
+ $this->drupalSetContent(drupal_render($output));
+ $this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // View mode: check that display settings specified in the instance are
+ // used.
+ $setting = $this->instance['display']['teaser']['settings']['test_formatter_setting'];
+ foreach ($this->values as $delta => $value) {
+ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
+ $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'teaser');
+ $this->drupalSetContent(drupal_render($output));
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+
+ // Unknown view mode: check that display settings for 'default' view mode
+ // are used.
+ $setting = $this->instance['display']['default']['settings']['test_formatter_setting'];
+ foreach ($this->values as $delta => $value) {
+ $item = $this->entity->{$this->field_name}[LANGUAGE_NONE][$delta];
+ $output = field_view_value('test_entity', $this->entity, $this->field_name, $item, 'unknown_view_mode');
+ $this->drupalSetContent(drupal_render($output));
+ $this->assertText($setting . '|' . $value['value'], format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
+ }
+ }
+}
+
+class FieldCrudTestCase extends FieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field CRUD tests',
+ 'description' => 'Test field create, read, update, and delete.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ // field_update_field() tests use number.module
+ parent::setUp('field_test', 'number');
+ }
+
+ // TODO : test creation with
+ // - a full fledged $field structure, check that all the values are there
+ // - a minimal $field structure, check all default values are set
+ // defer actual $field comparison to a helper function, used for the two cases above
+
+ /**
+ * Test the creation of a field.
+ */
+ function testCreateField() {
+ $field_definition = array(
+ 'field_name' => 'field_2',
+ 'type' => 'test_field',
+ );
+ field_test_memorize();
+ $field_definition = field_create_field($field_definition);
+ $mem = field_test_memorize();
+ $this->assertIdentical($mem['field_test_field_create_field'][0][0], $field_definition, 'hook_field_create_field() called with correct arguments.');
+
+ // Read the raw record from the {field_config_instance} table.
+ $result = db_query('SELECT * FROM {field_config} WHERE field_name = :field_name', array(':field_name' => $field_definition['field_name']));
+ $record = $result->fetchAssoc();
+ $record['data'] = unserialize($record['data']);
+
+ // Ensure that basic properties are preserved.
+ $this->assertEqual($record['field_name'], $field_definition['field_name'], 'The field name is properly saved.');
+ $this->assertEqual($record['type'], $field_definition['type'], 'The field type is properly saved.');
+
+ // Ensure that cardinality defaults to 1.
+ $this->assertEqual($record['cardinality'], 1, 'Cardinality defaults to 1.');
+
+ // Ensure that default settings are present.
+ $field_type = field_info_field_types($field_definition['type']);
+ $this->assertIdentical($record['data']['settings'], $field_type['settings'], 'Default field settings have been written.');
+
+ // Ensure that default storage was set.
+ $this->assertEqual($record['storage_type'], variable_get('field_storage_default'), 'The field type is properly saved.');
+
+ // Guarantee that the name is unique.
+ try {
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create two fields with the same name.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create two fields with the same name.'));
+ }
+
+ // Check that field type is required.
+ try {
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ );
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create a field with no type.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create a field with no type.'));
+ }
+
+ // Check that field name is required.
+ try {
+ $field_definition = array(
+ 'type' => 'test_field'
+ );
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create an unnamed field.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create an unnamed field.'));
+ }
+
+ // Check that field name must start with a letter or _.
+ try {
+ $field_definition = array(
+ 'field_name' => '2field_2',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create a field with a name starting with a digit.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create a field with a name starting with a digit.'));
+ }
+
+ // Check that field name must only contain lowercase alphanumeric or _.
+ try {
+ $field_definition = array(
+ 'field_name' => 'field#_3',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create a field with a name containing an illegal character.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create a field with a name containing an illegal character.'));
+ }
+
+ // Check that field name cannot be longer than 32 characters long.
+ try {
+ $field_definition = array(
+ 'field_name' => '_12345678901234567890123456789012',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $this->fail(t('Cannot create a field with a name longer than 32 characters.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create a field with a name longer than 32 characters.'));
+ }
+
+ // Check that field name can not be an entity key.
+ // "ftvid" is known as an entity key from the "test_entity" type.
+ try {
+ $field_definition = array(
+ 'type' => 'test_field',
+ 'field_name' => 'ftvid',
+ );
+ $field = field_create_field($field_definition);
+ $this->fail(t('Cannot create a field bearing the name of an entity key.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create a field bearing the name of an entity key.'));
+ }
+ }
+
+ /**
+ * Test failure to create a field.
+ */
+ function testCreateFieldFail() {
+ $field_name = 'duplicate';
+ $field_definition = array('field_name' => $field_name, 'type' => 'test_field', 'storage' => array('type' => 'field_test_storage_failure'));
+ $query = db_select('field_config')->condition('field_name', $field_name)->countQuery();
+
+ // The field does not appear in field_config.
+ $count = $query->execute()->fetchField();
+ $this->assertEqual($count, 0, 'A field_config row for the field does not exist.');
+
+ // Try to create the field.
+ try {
+ $field = field_create_field($field_definition);
+ $this->assertTrue(FALSE, 'Field creation (correctly) fails.');
+ }
+ catch (Exception $e) {
+ $this->assertTrue(TRUE, 'Field creation (correctly) fails.');
+ }
+
+ // The field does not appear in field_config.
+ $count = $query->execute()->fetchField();
+ $this->assertEqual($count, 0, 'A field_config row for the field does not exist.');
+ }
+
+ /**
+ * Test reading back a field definition.
+ */
+ function testReadField() {
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+
+ // Read the field back.
+ $field = field_read_field($field_definition['field_name']);
+ $this->assertTrue($field_definition < $field, 'The field was properly read.');
+ }
+
+ /**
+ * Tests reading field definitions.
+ */
+ function testReadFields() {
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+
+ // Check that 'single column' criteria works.
+ $fields = field_read_fields(array('field_name' => $field_definition['field_name']));
+ $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
+
+ // Check that 'multi column' criteria works.
+ $fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => $field_definition['type']));
+ $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
+ $fields = field_read_fields(array('field_name' => $field_definition['field_name'], 'type' => 'foo'));
+ $this->assertTrue(empty($fields), 'No field was found.');
+
+ // Create an instance of the field.
+ $instance_definition = array(
+ 'field_name' => $field_definition['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance_definition);
+
+ // Check that criteria spanning over the field_config_instance table work.
+ $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'bundle' => $instance_definition['bundle']));
+ $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
+ $fields = field_read_fields(array('entity_type' => $instance_definition['entity_type'], 'field_name' => $instance_definition['field_name']));
+ $this->assertTrue(count($fields) == 1 && isset($fields[$field_definition['field_name']]), 'The field was properly read.');
+ }
+
+ /**
+ * Test creation of indexes on data column.
+ */
+ function testFieldIndexes() {
+ // Check that indexes specified by the field type are used by default.
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ );
+ field_create_field($field_definition);
+ $field = field_read_field($field_definition['field_name']);
+ $expected_indexes = array('value' => array('value'));
+ $this->assertEqual($field['indexes'], $expected_indexes, 'Field type indexes saved by default');
+
+ // Check that indexes specified by the field definition override the field
+ // type indexes.
+ $field_definition = array(
+ 'field_name' => 'field_2',
+ 'type' => 'test_field',
+ 'indexes' => array(
+ 'value' => array(),
+ ),
+ );
+ field_create_field($field_definition);
+ $field = field_read_field($field_definition['field_name']);
+ $expected_indexes = array('value' => array());
+ $this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes override field type indexes');
+
+ // Check that indexes specified by the field definition add to the field
+ // type indexes.
+ $field_definition = array(
+ 'field_name' => 'field_3',
+ 'type' => 'test_field',
+ 'indexes' => array(
+ 'value_2' => array('value'),
+ ),
+ );
+ field_create_field($field_definition);
+ $field = field_read_field($field_definition['field_name']);
+ $expected_indexes = array('value' => array('value'), 'value_2' => array('value'));
+ $this->assertEqual($field['indexes'], $expected_indexes, 'Field definition indexes are merged with field type indexes');
+ }
+
+ /**
+ * Test the deletion of a field.
+ */
+ function testDeleteField() {
+ // TODO: Also test deletion of the data stored in the field ?
+
+ // Create two fields (so we can test that only one is deleted).
+ $this->field = array('field_name' => 'field_1', 'type' => 'test_field');
+ field_create_field($this->field);
+ $this->another_field = array('field_name' => 'field_2', 'type' => 'test_field');
+ field_create_field($this->another_field);
+
+ // Create instances for each.
+ $this->instance_definition = array(
+ 'field_name' => $this->field['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ ),
+ );
+ field_create_instance($this->instance_definition);
+ $this->another_instance_definition = $this->instance_definition;
+ $this->another_instance_definition['field_name'] = $this->another_field['field_name'];
+ field_create_instance($this->another_instance_definition);
+
+ // Test that the first field is not deleted, and then delete it.
+ $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field is not marked for deletion.');
+ field_delete_field($this->field['field_name']);
+
+ // Make sure that the field is marked as deleted when it is specifically
+ // loaded.
+ $field = field_read_field($this->field['field_name'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion.');
+
+ // Make sure that this field's instance is marked as deleted when it is
+ // specifically loaded.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($instance['deleted']), 'An instance for a deleted field is marked for deletion.');
+
+ // Try to load the field normally and make sure it does not show up.
+ $field = field_read_field($this->field['field_name']);
+ $this->assertTrue(empty($field), 'A deleted field is not loaded by default.');
+
+ // Try to load the instance normally and make sure it does not show up.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertTrue(empty($instance), 'An instance for a deleted field is not loaded by default.');
+
+ // Make sure the other field (and its field instance) are not deleted.
+ $another_field = field_read_field($this->another_field['field_name']);
+ $this->assertTrue(!empty($another_field) && empty($another_field['deleted']), 'A non-deleted field is not marked for deletion.');
+ $another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']);
+ $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'An instance of a non-deleted field is not marked for deletion.');
+
+ // Try to create a new field the same name as a deleted field and
+ // write data into it.
+ field_create_field($this->field);
+ field_create_instance($this->instance_definition);
+ $field = field_read_field($this->field['field_name']);
+ $this->assertTrue(!empty($field) && empty($field['deleted']), 'A new field with a previously used name is created.');
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new instance for a previously used field name is created.');
+
+ // Save an entity with data for the field
+ $entity = field_test_create_stub_entity(0, 0, $instance['bundle']);
+ $langcode = LANGUAGE_NONE;
+ $values[0]['value'] = mt_rand(1, 127);
+ $entity->{$field['field_name']}[$langcode] = $values;
+ $entity_type = 'test_entity';
+ field_attach_insert('test_entity', $entity);
+
+ // Verify the field is present on load
+ $entity = field_test_create_stub_entity(0, 0, $this->instance_definition['bundle']);
+ field_attach_load($entity_type, array(0 => $entity));
+ $this->assertIdentical(count($entity->{$field['field_name']}[$langcode]), count($values), "Data in previously deleted field saves and loads correctly");
+ foreach ($values as $delta => $value) {
+ $this->assertEqual($entity->{$field['field_name']}[$langcode][$delta]['value'], $values[$delta]['value'], "Data in previously deleted field saves and loads correctly");
+ }
+ }
+
+ function testUpdateNonExistentField() {
+ $test_field = array('field_name' => 'does_not_exist', 'type' => 'number_decimal');
+ try {
+ field_update_field($test_field);
+ $this->fail(t('Cannot update a field that does not exist.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot update a field that does not exist.'));
+ }
+ }
+
+ function testUpdateFieldType() {
+ $field = array('field_name' => 'field_type', 'type' => 'number_decimal');
+ $field = field_create_field($field);
+
+ $test_field = array('field_name' => 'field_type', 'type' => 'number_integer');
+ try {
+ field_update_field($test_field);
+ $this->fail(t('Cannot update a field to a different type.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot update a field to a different type.'));
+ }
+ }
+
+ /**
+ * Test updating a field.
+ */
+ function testUpdateField() {
+ // Create a field with a defined cardinality, so that we can ensure it's
+ // respected. Since cardinality enforcement is consistent across database
+ // systems, it makes a good test case.
+ $cardinality = 4;
+ $field_definition = array(
+ 'field_name' => 'field_update',
+ 'type' => 'test_field',
+ 'cardinality' => $cardinality,
+ );
+ $field_definition = field_create_field($field_definition);
+ $instance = array(
+ 'field_name' => 'field_update',
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ $instance = field_create_instance($instance);
+
+ do {
+ // We need a unique ID for our entity. $cardinality will do.
+ $id = $cardinality;
+ $entity = field_test_create_stub_entity($id, $id, $instance['bundle']);
+ // Fill in the entity with more values than $cardinality.
+ for ($i = 0; $i < 20; $i++) {
+ $entity->field_update[LANGUAGE_NONE][$i]['value'] = $i;
+ }
+ // Save the entity.
+ field_attach_insert('test_entity', $entity);
+ // Load back and assert there are $cardinality number of values.
+ $entity = field_test_create_stub_entity($id, $id, $instance['bundle']);
+ field_attach_load('test_entity', array($id => $entity));
+ $this->assertEqual(count($entity->field_update[LANGUAGE_NONE]), $field_definition['cardinality'], 'Cardinality is kept');
+ // Now check the values themselves.
+ for ($delta = 0; $delta < $cardinality; $delta++) {
+ $this->assertEqual($entity->field_update[LANGUAGE_NONE][$delta]['value'], $delta, 'Value is kept');
+ }
+ // Increase $cardinality and set the field cardinality to the new value.
+ $field_definition['cardinality'] = ++$cardinality;
+ field_update_field($field_definition);
+ } while ($cardinality < 6);
+ }
+
+ /**
+ * Test field type modules forbidding an update.
+ */
+ function testUpdateFieldForbid() {
+ $field = array('field_name' => 'forbidden', 'type' => 'test_field', 'settings' => array('changeable' => 0, 'unchangeable' => 0));
+ $field = field_create_field($field);
+ $field['settings']['changeable']++;
+ try {
+ field_update_field($field);
+ $this->pass(t("A changeable setting can be updated."));
+ }
+ catch (FieldException $e) {
+ $this->fail(t("An unchangeable setting cannot be updated."));
+ }
+ $field['settings']['unchangeable']++;
+ try {
+ field_update_field($field);
+ $this->fail(t("An unchangeable setting can be updated."));
+ }
+ catch (FieldException $e) {
+ $this->pass(t("An unchangeable setting cannot be updated."));
+ }
+ }
+
+ /**
+ * Test that fields are properly marked active or inactive.
+ */
+ function testActive() {
+ $field_definition = array(
+ 'field_name' => 'field_1',
+ 'type' => 'test_field',
+ // For this test, we need a storage backend provided by a different
+ // module than field_test.module.
+ 'storage' => array(
+ 'type' => 'field_sql_storage',
+ ),
+ );
+ field_create_field($field_definition);
+
+ // Test disabling and enabling:
+ // - the field type module,
+ // - the storage module,
+ // - both.
+ $this->_testActiveHelper($field_definition, array('field_test'));
+ $this->_testActiveHelper($field_definition, array('field_sql_storage'));
+ $this->_testActiveHelper($field_definition, array('field_test', 'field_sql_storage'));
+ }
+
+ /**
+ * Helper function for testActive().
+ *
+ * Test dependency between a field and a set of modules.
+ *
+ * @param $field_definition
+ * A field definition.
+ * @param $modules
+ * An aray of module names. The field will be tested to be inactive as long
+ * as any of those modules is disabled.
+ */
+ function _testActiveHelper($field_definition, $modules) {
+ $field_name = $field_definition['field_name'];
+
+ // Read the field.
+ $field = field_read_field($field_name);
+ $this->assertTrue($field_definition <= $field, 'The field was properly read.');
+
+ module_disable($modules, FALSE);
+
+ $fields = field_read_fields(array('field_name' => $field_name), array('include_inactive' => TRUE));
+ $this->assertTrue(isset($fields[$field_name]) && $field_definition < $field, 'The field is properly read when explicitly fetching inactive fields.');
+
+ // Re-enable modules one by one, and check that the field is still inactive
+ // while some modules remain disabled.
+ while ($modules) {
+ $field = field_read_field($field_name);
+ $this->assertTrue(empty($field), format_string('%modules disabled. The field is marked inactive.', array('%modules' => implode(', ', $modules))));
+
+ $module = array_shift($modules);
+ module_enable(array($module), FALSE);
+ }
+
+ // Check that the field is active again after all modules have been
+ // enabled.
+ $field = field_read_field($field_name);
+ $this->assertTrue($field_definition <= $field, 'The field was was marked active.');
+ }
+}
+
+class FieldInstanceCrudTestCase extends FieldTestCase {
+ protected $field;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field instance CRUD tests',
+ 'description' => 'Create field entities by attaching fields to entities.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+
+ $this->field = array(
+ 'field_name' => drupal_strtolower($this->randomName()),
+ 'type' => 'test_field',
+ );
+ field_create_field($this->field);
+ $this->instance_definition = array(
+ 'field_name' => $this->field['field_name'],
+ 'entity_type' => 'test_entity',
+ 'bundle' => 'test_bundle',
+ );
+ }
+
+ // TODO : test creation with
+ // - a full fledged $instance structure, check that all the values are there
+ // - a minimal $instance structure, check all default values are set
+ // defer actual $instance comparison to a helper function, used for the two cases above,
+ // and for testUpdateFieldInstance
+
+ /**
+ * Test the creation of a field instance.
+ */
+ function testCreateFieldInstance() {
+ field_create_instance($this->instance_definition);
+
+ // Read the raw record from the {field_config_instance} table.
+ $result = db_query('SELECT * FROM {field_config_instance} WHERE field_name = :field_name AND bundle = :bundle', array(':field_name' => $this->instance_definition['field_name'], ':bundle' => $this->instance_definition['bundle']));
+ $record = $result->fetchAssoc();
+ $record['data'] = unserialize($record['data']);
+
+ $field_type = field_info_field_types($this->field['type']);
+ $widget_type = field_info_widget_types($field_type['default_widget']);
+ $formatter_type = field_info_formatter_types($field_type['default_formatter']);
+
+ // Check that default values are set.
+ $this->assertIdentical($record['data']['required'], FALSE, 'Required defaults to false.');
+ $this->assertIdentical($record['data']['label'], $this->instance_definition['field_name'], 'Label defaults to field name.');
+ $this->assertIdentical($record['data']['description'], '', 'Description defaults to empty string.');
+ $this->assertIdentical($record['data']['widget']['type'], $field_type['default_widget'], 'Default widget has been written.');
+ $this->assertTrue(isset($record['data']['display']['default']), 'Display for "full" view_mode has been written.');
+ $this->assertIdentical($record['data']['display']['default']['type'], $field_type['default_formatter'], 'Default formatter for "full" view_mode has been written.');
+
+ // Check that default settings are set.
+ $this->assertIdentical($record['data']['settings'], $field_type['instance_settings'] , 'Default instance settings have been written.');
+ $this->assertIdentical($record['data']['widget']['settings'], $widget_type['settings'] , 'Default widget settings have been written.');
+ $this->assertIdentical($record['data']['display']['default']['settings'], $formatter_type['settings'], 'Default formatter settings for "full" view_mode have been written.');
+
+ // Guarantee that the field/bundle combination is unique.
+ try {
+ field_create_instance($this->instance_definition);
+ $this->fail(t('Cannot create two instances with the same field / bundle combination.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create two instances with the same field / bundle combination.'));
+ }
+
+ // Check that the specified field exists.
+ try {
+ $this->instance_definition['field_name'] = $this->randomName();
+ field_create_instance($this->instance_definition);
+ $this->fail(t('Cannot create an instance of a non-existing field.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create an instance of a non-existing field.'));
+ }
+
+ // Create a field restricted to a specific entity type.
+ $field_restricted = array(
+ 'field_name' => drupal_strtolower($this->randomName()),
+ 'type' => 'test_field',
+ 'entity_types' => array('test_cacheable_entity'),
+ );
+ field_create_field($field_restricted);
+
+ // Check that an instance can be added to an entity type allowed
+ // by the field.
+ try {
+ $instance = $this->instance_definition;
+ $instance['field_name'] = $field_restricted['field_name'];
+ $instance['entity_type'] = 'test_cacheable_entity';
+ field_create_instance($instance);
+ $this->pass(t('Can create an instance on an entity type allowed by the field.'));
+ }
+ catch (FieldException $e) {
+ $this->fail(t('Can create an instance on an entity type allowed by the field.'));
+ }
+
+ // Check that an instance cannot be added to an entity type
+ // forbidden by the field.
+ try {
+ $instance = $this->instance_definition;
+ $instance['field_name'] = $field_restricted['field_name'];
+ field_create_instance($instance);
+ $this->fail(t('Cannot create an instance on an entity type forbidden by the field.'));
+ }
+ catch (FieldException $e) {
+ $this->pass(t('Cannot create an instance on an entity type forbidden by the field.'));
+ }
+
+ // TODO: test other failures.
+ }
+
+ /**
+ * Test reading back an instance definition.
+ */
+ function testReadFieldInstance() {
+ field_create_instance($this->instance_definition);
+
+ // Read the instance back.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertTrue($this->instance_definition < $instance, 'The field was properly read.');
+ }
+
+ /**
+ * Test the update of a field instance.
+ */
+ function testUpdateFieldInstance() {
+ field_create_instance($this->instance_definition);
+ $field_type = field_info_field_types($this->field['type']);
+
+ // Check that basic changes are saved.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $instance['required'] = !$instance['required'];
+ $instance['label'] = $this->randomName();
+ $instance['description'] = $this->randomName();
+ $instance['settings']['test_instance_setting'] = $this->randomName();
+ $instance['widget']['settings']['test_widget_setting'] =$this->randomName();
+ $instance['widget']['weight']++;
+ $instance['display']['default']['settings']['test_formatter_setting'] = $this->randomName();
+ $instance['display']['default']['weight']++;
+ field_update_instance($instance);
+
+ $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertEqual($instance['required'], $instance_new['required'], '"required" change is saved');
+ $this->assertEqual($instance['label'], $instance_new['label'], '"label" change is saved');
+ $this->assertEqual($instance['description'], $instance_new['description'], '"description" change is saved');
+ $this->assertEqual($instance['widget']['settings']['test_widget_setting'], $instance_new['widget']['settings']['test_widget_setting'], 'Widget setting change is saved');
+ $this->assertEqual($instance['widget']['weight'], $instance_new['widget']['weight'], 'Widget weight change is saved');
+ $this->assertEqual($instance['display']['default']['settings']['test_formatter_setting'], $instance_new['display']['default']['settings']['test_formatter_setting'], 'Formatter setting change is saved');
+ $this->assertEqual($instance['display']['default']['weight'], $instance_new['display']['default']['weight'], 'Widget weight change is saved');
+
+ // Check that changing widget and formatter types updates the default settings.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $instance['widget']['type'] = 'test_field_widget_multiple';
+ $instance['display']['default']['type'] = 'field_test_multiple';
+ field_update_instance($instance);
+
+ $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertEqual($instance['widget']['type'], $instance_new['widget']['type'] , 'Widget type change is saved.');
+ $settings = field_info_widget_settings($instance_new['widget']['type']);
+ $this->assertIdentical($settings, array_intersect_key($instance_new['widget']['settings'], $settings) , 'Widget type change updates default settings.');
+ $this->assertEqual($instance['display']['default']['type'], $instance_new['display']['default']['type'] , 'Formatter type change is saved.');
+ $info = field_info_formatter_types($instance_new['display']['default']['type']);
+ $settings = $info['settings'];
+ $this->assertIdentical($settings, array_intersect_key($instance_new['display']['default']['settings'], $settings) , 'Changing formatter type updates default settings.');
+
+ // Check that adding a new view mode is saved and gets default settings.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $instance['display']['teaser'] = array();
+ field_update_instance($instance);
+
+ $instance_new = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertTrue(isset($instance_new['display']['teaser']), 'Display for the new view_mode has been written.');
+ $this->assertIdentical($instance_new['display']['teaser']['type'], $field_type['default_formatter'], 'Default formatter for the new view_mode has been written.');
+ $info = field_info_formatter_types($instance_new['display']['teaser']['type']);
+ $settings = $info['settings'];
+ $this->assertIdentical($settings, $instance_new['display']['teaser']['settings'] , 'Default formatter settings for the new view_mode have been written.');
+
+ // TODO: test failures.
+ }
+
+ /**
+ * Test the deletion of a field instance.
+ */
+ function testDeleteFieldInstance() {
+ // TODO: Test deletion of the data stored in the field also.
+ // Need to check that data for a 'deleted' field / instance doesn't get loaded
+ // Need to check data marked deleted is cleaned on cron (not implemented yet...)
+
+ // Create two instances for the same field so we can test that only one
+ // is deleted.
+ field_create_instance($this->instance_definition);
+ $this->another_instance_definition = $this->instance_definition;
+ $this->another_instance_definition['bundle'] .= '_another_bundle';
+ $instance = field_create_instance($this->another_instance_definition);
+
+ // Test that the first instance is not deleted, and then delete it.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($instance) && empty($instance['deleted']), 'A new field instance is not marked for deletion.');
+ field_delete_instance($instance);
+
+ // Make sure the instance is marked as deleted when the instance is
+ // specifically loaded.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($instance['deleted']), 'A deleted field instance is marked for deletion.');
+
+ // Try to load the instance normally and make sure it does not show up.
+ $instance = field_read_instance('test_entity', $this->instance_definition['field_name'], $this->instance_definition['bundle']);
+ $this->assertTrue(empty($instance), 'A deleted field instance is not loaded by default.');
+
+ // Make sure the other field instance is not deleted.
+ $another_instance = field_read_instance('test_entity', $this->another_instance_definition['field_name'], $this->another_instance_definition['bundle']);
+ $this->assertTrue(!empty($another_instance) && empty($another_instance['deleted']), 'A non-deleted field instance is not marked for deletion.');
+
+ // Make sure the field is deleted when its last instance is deleted.
+ field_delete_instance($another_instance);
+ $field = field_read_field($another_instance['field_name'], array('include_deleted' => TRUE));
+ $this->assertTrue(!empty($field['deleted']), 'A deleted field is marked for deletion after all its instances have been marked for deletion.');
+ }
+}
+
+/**
+ * Unit test class for the multilanguage fields logic.
+ *
+ * The following tests will check the multilanguage logic of _field_invoke() and
+ * that only the correct values are returned by field_available_languages().
+ */
+class FieldTranslationsTestCase extends FieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field translations tests',
+ 'description' => 'Test multilanguage fields logic.',
+ 'group' => 'Field API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('locale', 'field_test');
+
+ $this->field_name = drupal_strtolower($this->randomName() . '_field_name');
+
+ $this->entity_type = 'test_entity';
+
+ $field = array(
+ 'field_name' => $this->field_name,
+ 'type' => 'test_field',
+ 'cardinality' => 4,
+ 'translatable' => TRUE,
+ );
+ field_create_field($field);
+ $this->field = field_read_field($this->field_name);
+
+ $instance = array(
+ 'field_name' => $this->field_name,
+ 'entity_type' => $this->entity_type,
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance);
+ $this->instance = field_read_instance('test_entity', $this->field_name, 'test_bundle');
+
+ require_once DRUPAL_ROOT . '/includes/locale.inc';
+ for ($i = 0; $i < 3; ++$i) {
+ locale_add_language('l' . $i, $this->randomString(), $this->randomString());
+ }
+ }
+
+ /**
+ * Ensures that only valid values are returned by field_available_languages().
+ */
+ function testFieldAvailableLanguages() {
+ // Test 'translatable' fieldable info.
+ field_test_entity_info_translatable('test_entity', FALSE);
+ $field = $this->field;
+ $field['field_name'] .= '_untranslatable';
+
+ // Enable field translations for the entity.
+ field_test_entity_info_translatable('test_entity', TRUE);
+
+ // Test hook_field_languages() invocation on a translatable field.
+ variable_set('field_test_field_available_languages_alter', TRUE);
+ $enabled_languages = field_content_languages();
+ $available_languages = field_available_languages($this->entity_type, $this->field);
+ foreach ($available_languages as $delta => $langcode) {
+ if ($langcode != 'xx' && $langcode != 'en') {
+ $this->assertTrue(in_array($langcode, $enabled_languages), format_string('%language is an enabled language.', array('%language' => $langcode)));
+ }
+ }
+ $this->assertTrue(in_array('xx', $available_languages), format_string('%language was made available.', array('%language' => 'xx')));
+ $this->assertFalse(in_array('en', $available_languages), format_string('%language was made unavailable.', array('%language' => 'en')));
+
+ // Test field_available_languages() behavior for untranslatable fields.
+ $this->field['translatable'] = FALSE;
+ field_update_field($this->field);
+ $available_languages = field_available_languages($this->entity_type, $this->field);
+ $this->assertTrue(count($available_languages) == 1 && $available_languages[0] === LANGUAGE_NONE, 'For untranslatable fields only LANGUAGE_NONE is available.');
+ }
+
+ /**
+ * Test the multilanguage logic of _field_invoke().
+ */
+ function testFieldInvoke() {
+ // Enable field translations for the entity.
+ field_test_entity_info_translatable('test_entity', TRUE);
+
+ $entity_type = 'test_entity';
+ $entity = field_test_create_stub_entity(0, 0, $this->instance['bundle']);
+
+ // Populate some extra languages to check if _field_invoke() correctly uses
+ // the result of field_available_languages().
+ $values = array();
+ $extra_languages = mt_rand(1, 4);
+ $languages = $available_languages = field_available_languages($this->entity_type, $this->field);
+ for ($i = 0; $i < $extra_languages; ++$i) {
+ $languages[] = $this->randomName(2);
+ }
+
+ // For each given language provide some random values.
+ foreach ($languages as $langcode) {
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ $values[$langcode][$delta]['value'] = mt_rand(1, 127);
+ }
+ }
+ $entity->{$this->field_name} = $values;
+
+ $results = _field_invoke('test_op', $entity_type, $entity);
+ foreach ($results as $langcode => $result) {
+ $hash = hash('sha256', serialize(array($entity_type, $entity, $this->field_name, $langcode, $values[$langcode])));
+ // Check whether the parameters passed to _field_invoke() were correctly
+ // forwarded to the callback function.
+ $this->assertEqual($hash, $result, format_string('The result for %language is correctly stored.', array('%language' => $langcode)));
+ }
+
+ $this->assertEqual(count($results), count($available_languages), 'No unavailable language has been processed.');
+ }
+
+ /**
+ * Test the multilanguage logic of _field_invoke_multiple().
+ */
+ function testFieldInvokeMultiple() {
+ // Enable field translations for the entity.
+ field_test_entity_info_translatable('test_entity', TRUE);
+
+ $values = array();
+ $options = array();
+ $entities = array();
+ $entity_type = 'test_entity';
+ $entity_count = 5;
+ $available_languages = field_available_languages($this->entity_type, $this->field);
+
+ for ($id = 1; $id <= $entity_count; ++$id) {
+ $entity = field_test_create_stub_entity($id, $id, $this->instance['bundle']);
+ $languages = $available_languages;
+
+ // Populate some extra languages to check whether _field_invoke()
+ // correctly uses the result of field_available_languages().
+ $extra_languages = mt_rand(1, 4);
+ for ($i = 0; $i < $extra_languages; ++$i) {
+ $languages[] = $this->randomName(2);
+ }
+
+ // For each given language provide some random values.
+ $language_count = count($languages);
+ for ($i = 0; $i < $language_count; ++$i) {
+ $langcode = $languages[$i];
+ // Avoid to populate at least one field translation to check that
+ // per-entity language suggestions work even when available field values
+ // are different for each language.
+ if ($i !== $id) {
+ for ($delta = 0; $delta < $this->field['cardinality']; $delta++) {
+ $values[$id][$langcode][$delta]['value'] = mt_rand(1, 127);
+ }
+ }
+ // Ensure that a language for which there is no field translation is
+ // used as display language to prepare per-entity language suggestions.
+ elseif (!isset($display_language)) {
+ $display_language = $langcode;
+ }
+ }
+
+ $entity->{$this->field_name} = $values[$id];
+ $entities[$id] = $entity;
+
+ // Store per-entity language suggestions.
+ $options['language'][$id] = field_language($entity_type, $entity, NULL, $display_language);
+ }
+
+ $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities);
+ foreach ($grouped_results as $id => $results) {
+ foreach ($results as $langcode => $result) {
+ if (isset($values[$id][$langcode])) {
+ $hash = hash('sha256', serialize(array($entity_type, $entities[$id], $this->field_name, $langcode, $values[$id][$langcode])));
+ // Check whether the parameters passed to _field_invoke_multiple()
+ // were correctly forwarded to the callback function.
+ $this->assertEqual($hash, $result, format_string('The result for entity %id/%language is correctly stored.', array('%id' => $id, '%language' => $langcode)));
+ }
+ }
+ $this->assertEqual(count($results), count($available_languages), format_string('No unavailable language has been processed for entity %id.', array('%id' => $id)));
+ }
+
+ $null = NULL;
+ $grouped_results = _field_invoke_multiple('test_op_multiple', $entity_type, $entities, $null, $null, $options);
+ foreach ($grouped_results as $id => $results) {
+ foreach ($results as $langcode => $result) {
+ $this->assertTrue(isset($options['language'][$id]), format_string('The result language %language for entity %id was correctly suggested (display language: %display_language).', array('%id' => $id, '%language' => $langcode, '%display_language' => $display_language)));
+ }
+ }
+ }
+
+ /**
+ * Test translatable fields storage/retrieval.
+ */
+ function testTranslatableFieldSaveLoad() {
+ // Enable field translations for nodes.
+ field_test_entity_info_translatable('node', TRUE);
+ $entity_info = entity_get_info('node');
+ $this->assertTrue(count($entity_info['translation']), 'Nodes are translatable.');
+
+ // Prepare the field translations.
+ field_test_entity_info_translatable('test_entity', TRUE);
+ $eid = $evid = 1;
+ $entity_type = 'test_entity';
+ $entity = field_test_create_stub_entity($eid, $evid, $this->instance['bundle']);
+ $field_translations = array();
+ $available_languages = field_available_languages($entity_type, $this->field);
+ $this->assertTrue(count($available_languages) > 1, 'Field is translatable.');
+ foreach ($available_languages as $langcode) {
+ $field_translations[$langcode] = $this->_generateTestFieldValues($this->field['cardinality']);
+ }
+
+ // Save and reload the field translations.
+ $entity->{$this->field_name} = $field_translations;
+ field_attach_insert($entity_type, $entity);
+ unset($entity->{$this->field_name});
+ field_attach_load($entity_type, array($eid => $entity));
+
+ // Check if the correct values were saved/loaded.
+ foreach ($field_translations as $langcode => $items) {
+ $result = TRUE;
+ foreach ($items as $delta => $item) {
+ $result = $result && $item['value'] == $entity->{$this->field_name}[$langcode][$delta]['value'];
+ }
+ $this->assertTrue($result, format_string('%language translation correctly handled.', array('%language' => $langcode)));
+ }
+ }
+
+ /**
+ * Tests display language logic for translatable fields.
+ */
+ function testFieldDisplayLanguage() {
+ $field_name = drupal_strtolower($this->randomName() . '_field_name');
+ $entity_type = 'test_entity';
+
+ // We need an additional field here to properly test display language
+ // suggestions.
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'test_field',
+ 'cardinality' => 2,
+ 'translatable' => TRUE,
+ );
+ field_create_field($field);
+
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => $entity_type,
+ 'bundle' => 'test_bundle',
+ );
+ field_create_instance($instance);
+
+ $entity = field_test_create_stub_entity(1, 1, $this->instance['bundle']);
+ $instances = field_info_instances($entity_type, $this->instance['bundle']);
+
+ $enabled_languages = field_content_languages();
+ $languages = array();
+
+ // Generate field translations for languages different from the first
+ // enabled.
+ foreach ($instances as $instance) {
+ $field_name = $instance['field_name'];
+ $field = field_info_field($field_name);
+ do {
+ // Index 0 is reserved for the requested language, this way we ensure
+ // that no field is actually populated with it.
+ $langcode = $enabled_languages[mt_rand(1, count($enabled_languages) - 1)];
+ }
+ while (isset($languages[$langcode]));
+ $languages[$langcode] = TRUE;
+ $entity->{$field_name}[$langcode] = $this->_generateTestFieldValues($field['cardinality']);
+ }
+
+ // Test multiple-fields display languages for untranslatable entities.
+ field_test_entity_info_translatable($entity_type, FALSE);
+ drupal_static_reset('field_language');
+ $requested_language = $enabled_languages[0];
+ $display_language = field_language($entity_type, $entity, NULL, $requested_language);
+ foreach ($instances as $instance) {
+ $field_name = $instance['field_name'];
+ $this->assertTrue($display_language[$field_name] == LANGUAGE_NONE, format_string('The display language for field %field_name is %language.', array('%field_name' => $field_name, '%language' => LANGUAGE_NONE)));
+ }
+
+ // Test multiple-fields display languages for translatable entities.
+ field_test_entity_info_translatable($entity_type, TRUE);
+ drupal_static_reset('field_language');
+ $display_language = field_language($entity_type, $entity, NULL, $requested_language);
+
+ foreach ($instances as $instance) {
+ $field_name = $instance['field_name'];
+ $langcode = $display_language[$field_name];
+ // As the requested language was not assinged to any field, if the
+ // returned language is defined for the current field, core fallback rules
+ // were successfully applied.
+ $this->assertTrue(isset($entity->{$field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+ }
+
+ // Test single-field display language.
+ drupal_static_reset('field_language');
+ $langcode = field_language($entity_type, $entity, $this->field_name, $requested_language);
+ $this->assertTrue(isset($entity->{$this->field_name}[$langcode]) && $langcode != $requested_language, format_string('The display language for the (single) field %field_name is %language.', array('%field_name' => $field_name, '%language' => $langcode)));
+
+ // Test field_language() basic behavior without language fallback.
+ variable_set('field_test_language_fallback', FALSE);
+ $entity->{$this->field_name}[$requested_language] = mt_rand(1, 127);
+ drupal_static_reset('field_language');
+ $display_language = field_language($entity_type, $entity, $this->field_name, $requested_language);
+ $this->assertEqual($display_language, $requested_language, 'Display language behave correctly when language fallback is disabled');
+ }
+
+ /**
+ * Tests field translations when creating a new revision.
+ */
+ function testFieldFormTranslationRevisions() {
+ $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
+ $this->drupalLogin($web_user);
+
+ // Prepare the field translations.
+ field_test_entity_info_translatable($this->entity_type, TRUE);
+ $eid = 1;
+ $entity = field_test_create_stub_entity($eid, $eid, $this->instance['bundle']);
+ $available_languages = array_flip(field_available_languages($this->entity_type, $this->field));
+ unset($available_languages[LANGUAGE_NONE]);
+ $field_name = $this->field['field_name'];
+
+ // Store the field translations.
+ $entity->is_new = TRUE;
+ foreach ($available_languages as $langcode => $value) {
+ $entity->{$field_name}[$langcode][0]['value'] = $value + 1;
+ }
+ field_test_entity_save($entity);
+
+ // Create a new revision.
+ $langcode = field_valid_language(NULL);
+ $edit = array("{$field_name}[$langcode][0][value]" => $entity->{$field_name}[$langcode][0]['value'], 'revision' => TRUE);
+ $this->drupalPost('test-entity/manage/' . $eid . '/edit', $edit, t('Save'));
+
+ // Check translation revisions.
+ $this->checkTranslationRevisions($eid, $eid, $available_languages);
+ $this->checkTranslationRevisions($eid, $eid + 1, $available_languages);
+ }
+
+ /**
+ * Check if the field translation attached to the entity revision identified
+ * by the passed arguments were correctly stored.
+ */
+ private function checkTranslationRevisions($eid, $evid, $available_languages) {
+ $field_name = $this->field['field_name'];
+ $entity = field_test_entity_test_load($eid, $evid);
+ foreach ($available_languages as $langcode => $value) {
+ $passed = isset($entity->{$field_name}[$langcode]) && $entity->{$field_name}[$langcode][0]['value'] == $value + 1;
+ $this->assertTrue($passed, format_string('The @language translation for revision @revision was correctly stored', array('@language' => $langcode, '@revision' => $entity->ftvid)));
+ }
+ }
+}
+
+/**
+ * Unit test class for field bulk delete and batch purge functionality.
+ */
+class FieldBulkDeleteTestCase extends FieldTestCase {
+ protected $field;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Field bulk delete tests',
+ 'description' => 'Bulk delete fields and instances, and clean up afterwards.',
+ 'group' => 'Field API',
+ );
+ }
+
+ /**
+ * Convenience function for Field API tests.
+ *
+ * Given an array of potentially fully-populated entities and an
+ * optional field name, generate an array of stub entities of the
+ * same fieldable type which contains the data for the field name
+ * (if given).
+ *
+ * @param $entity_type
+ * The entity type of $entities.
+ * @param $entities
+ * An array of entities of type $entity_type.
+ * @param $field_name
+ * Optional; a field name whose data should be copied from
+ * $entities into the returned stub entities.
+ * @return
+ * An array of stub entities corresponding to $entities.
+ */
+ function _generateStubEntities($entity_type, $entities, $field_name = NULL) {
+ $stubs = array();
+ foreach ($entities as $id => $entity) {
+ $stub = entity_create_stub_entity($entity_type, entity_extract_ids($entity_type, $entity));
+ if (isset($field_name)) {
+ $stub->{$field_name} = $entity->{$field_name};
+ }
+ $stubs[$id] = $stub;
+ }
+ return $stubs;
+ }
+
+ /**
+ * Tests that the expected hooks have been invoked on the expected entities.
+ *
+ * @param $expected_hooks
+ * An array keyed by hook name, with one entry per expected invocation.
+ * Each entry is the value of the "$entity" parameter the hook is expected
+ * to have been passed.
+ * @param $actual_hooks
+ * The array of actual hook invocations recorded by field_test_memorize().
+ */
+ function checkHooksInvocations($expected_hooks, $actual_hooks) {
+ foreach ($expected_hooks as $hook => $invocations) {
+ $actual_invocations = $actual_hooks[$hook];
+
+ // Check that the number of invocations is correct.
+ $this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times.");
+
+ // Check that the hook was called for each expected argument.
+ foreach ($invocations as $argument) {
+ $found = FALSE;
+ foreach ($actual_invocations as $actual_arguments) {
+ if ($actual_arguments[1] == $argument) {
+ $found = TRUE;
+ break;
+ }
+ }
+ $this->assertTrue($found, "$hook() was called on expected argument");
+ }
+ }
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+
+ $this->fields = array();
+ $this->instances = array();
+ $this->entities = array();
+ $this->entities_by_bundles = array();
+
+ // Create two bundles.
+ $this->bundles = array('bb_1' => 'bb_1', 'bb_2' => 'bb_2');
+ foreach ($this->bundles as $name => $desc) {
+ field_test_create_bundle($name, $desc);
+ }
+
+ // Create two fields.
+ $field = array('field_name' => 'bf_1', 'type' => 'test_field', 'cardinality' => 1);
+ $this->fields[] = field_create_field($field);
+ $field = array('field_name' => 'bf_2', 'type' => 'test_field', 'cardinality' => 4);
+ $this->fields[] = field_create_field($field);
+
+ // For each bundle, create an instance of each field, and 10
+ // entities with values for each field.
+ $id = 0;
+ $this->entity_type = 'test_entity';
+ foreach ($this->bundles as $bundle) {
+ foreach ($this->fields as $field) {
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => $this->entity_type,
+ 'bundle' => $bundle,
+ 'widget' => array(
+ 'type' => 'test_field_widget',
+ )
+ );
+ $this->instances[] = field_create_instance($instance);
+ }
+
+ for ($i = 0; $i < 10; $i++) {
+ $entity = field_test_create_stub_entity($id, $id, $bundle);
+ foreach ($this->fields as $field) {
+ $entity->{$field['field_name']}[LANGUAGE_NONE] = $this->_generateTestFieldValues($field['cardinality']);
+ }
+
+ $this->entities[$id] = $entity;
+ // Also keep track of the entities per bundle.
+ $this->entities_by_bundles[$bundle][$id] = $entity;
+ field_attach_insert($this->entity_type, $entity);
+ $id++;
+ }
+ }
+ }
+
+ /**
+ * Verify that deleting an instance leaves the field data items in
+ * the database and that the appropriate Field API functions can
+ * operate on the deleted data and instance.
+ *
+ * This tests how EntityFieldQuery interacts with
+ * field_delete_instance() and could be moved to FieldCrudTestCase,
+ * but depends on this class's setUp().
+ */
+ function testDeleteFieldInstance() {
+ $bundle = reset($this->bundles);
+ $field = reset($this->fields);
+
+ // There are 10 entities of this bundle.
+ $query = new EntityFieldQuery();
+ $found = $query
+ ->fieldCondition($field)
+ ->entityCondition('bundle', $bundle)
+ ->execute();
+ $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found before deleting');
+
+ // Delete the instance.
+ $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
+ field_delete_instance($instance);
+
+ // The instance still exists, deleted.
+ $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+ $this->assertEqual(count($instances), 1, 'There is one deleted instance');
+ $this->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle');
+
+ // There are 0 entities of this bundle with non-deleted data.
+ $query = new EntityFieldQuery();
+ $found = $query
+ ->fieldCondition($field)
+ ->entityCondition('bundle', $bundle)
+ ->execute();
+ $this->assertTrue(!isset($found['test_entity']), 'No entities found after deleting');
+
+ // There are 10 entities of this bundle when deleted fields are allowed, and
+ // their values are correct.
+ $query = new EntityFieldQuery();
+ $found = $query
+ ->fieldCondition($field)
+ ->entityCondition('bundle', $bundle)
+ ->deleted(TRUE)
+ ->execute();
+ field_attach_load($this->entity_type, $found[$this->entity_type], FIELD_LOAD_CURRENT, array('field_id' => $field['id'], 'deleted' => 1));
+ $this->assertEqual(count($found['test_entity']), 10, 'Correct number of entities found after deleting');
+ foreach ($found['test_entity'] as $id => $entity) {
+ $this->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity $id with deleted data loaded correctly");
+ }
+ }
+
+ /**
+ * Verify that field data items and instances are purged when an
+ * instance is deleted.
+ */
+ function testPurgeInstance() {
+ // Start recording hook invocations.
+ field_test_memorize();
+
+ $bundle = reset($this->bundles);
+ $field = reset($this->fields);
+
+ // Delete the instance.
+ $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
+ field_delete_instance($instance);
+
+ // No field hooks were called.
+ $mem = field_test_memorize();
+ $this->assertEqual(count($mem), 0, 'No field hooks were called');
+
+ $batch_size = 2;
+ for ($count = 8; $count >= 0; $count -= $batch_size) {
+ // Purge two entities.
+ field_purge_batch($batch_size);
+
+ // There are $count deleted entities left.
+ $query = new EntityFieldQuery();
+ $found = $query
+ ->fieldCondition($field)
+ ->entityCondition('bundle', $bundle)
+ ->deleted(TRUE)
+ ->execute();
+ $this->assertEqual($count ? count($found['test_entity']) : count($found), $count, 'Correct number of entities found after purging 2');
+ }
+
+ // Check hooks invocations.
+ // - hook_field_load() (multiple hook) should have been called on all
+ // entities by pairs of two.
+ // - hook_field_delete() should have been called once for each entity in the
+ // bundle.
+ $actual_hooks = field_test_memorize();
+ $hooks = array();
+ $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
+ foreach (array_chunk($stubs, $batch_size, TRUE) as $chunk) {
+ $hooks['field_test_field_load'][] = $chunk;
+ }
+ foreach ($stubs as $stub) {
+ $hooks['field_test_field_delete'][] = $stub;
+ }
+ $this->checkHooksInvocations($hooks, $actual_hooks);
+
+ // The instance still exists, deleted.
+ $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+ $this->assertEqual(count($instances), 1, 'There is one deleted instance');
+
+ // Purge the instance.
+ field_purge_batch($batch_size);
+
+ // The instance is gone.
+ $instances = field_read_instances(array('field_id' => $field['id'], 'deleted' => 1), array('include_deleted' => 1, 'include_inactive' => 1));
+ $this->assertEqual(count($instances), 0, 'The instance is gone');
+
+ // The field still exists, not deleted, because it has a second instance.
+ $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
+ $this->assertTrue(isset($fields[$field['id']]), 'The field exists and is not deleted');
+ }
+
+ /**
+ * Verify that fields are preserved and purged correctly as multiple
+ * instances are deleted and purged.
+ */
+ function testPurgeField() {
+ // Start recording hook invocations.
+ field_test_memorize();
+
+ $field = reset($this->fields);
+
+ // Delete the first instance.
+ $bundle = reset($this->bundles);
+ $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
+ field_delete_instance($instance);
+
+ // Assert that hook_field_delete() was not called yet.
+ $mem = field_test_memorize();
+ $this->assertEqual(count($mem), 0, 'No field hooks were called.');
+
+ // Purge the data.
+ field_purge_batch(10);
+
+ // Check hooks invocations.
+ // - hook_field_load() (multiple hook) should have been called once, for all
+ // entities in the bundle.
+ // - hook_field_delete() should have been called once for each entity in the
+ // bundle.
+ $actual_hooks = field_test_memorize();
+ $hooks = array();
+ $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
+ $hooks['field_test_field_load'][] = $stubs;
+ foreach ($stubs as $stub) {
+ $hooks['field_test_field_delete'][] = $stub;
+ }
+ $this->checkHooksInvocations($hooks, $actual_hooks);
+
+ // Purge again to purge the instance.
+ field_purge_batch(0);
+
+ // The field still exists, not deleted.
+ $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1));
+ $this->assertTrue(isset($fields[$field['id']]) && !$fields[$field['id']]['deleted'], 'The field exists and is not deleted');
+
+ // Delete the second instance.
+ $bundle = next($this->bundles);
+ $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
+ field_delete_instance($instance);
+
+ // Assert that hook_field_delete() was not called yet.
+ $mem = field_test_memorize();
+ $this->assertEqual(count($mem), 0, 'No field hooks were called.');
+
+ // Purge the data.
+ field_purge_batch(10);
+
+ // Check hooks invocations (same as above, for the 2nd bundle).
+ $actual_hooks = field_test_memorize();
+ $hooks = array();
+ $stubs = $this->_generateStubEntities($this->entity_type, $this->entities_by_bundles[$bundle], $field['field_name']);
+ $hooks['field_test_field_load'][] = $stubs;
+ foreach ($stubs as $stub) {
+ $hooks['field_test_field_delete'][] = $stub;
+ }
+ $this->checkHooksInvocations($hooks, $actual_hooks);
+
+ // The field still exists, deleted.
+ $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1));
+ $this->assertTrue(isset($fields[$field['id']]) && $fields[$field['id']]['deleted'], 'The field exists and is deleted');
+
+ // Purge again to purge the instance and the field.
+ field_purge_batch(0);
+
+ // The field is gone.
+ $fields = field_read_fields(array('id' => $field['id']), array('include_deleted' => 1, 'include_inactive' => 1));
+ $this->assertEqual(count($fields), 0, 'The field is purged.');
+ }
+}
+
+/**
+ * Tests entity properties.
+ */
+class EntityPropertiesTestCase extends FieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Entity properties',
+ 'description' => 'Tests entity properties.',
+ 'group' => 'Entity API',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('field_test');
+ }
+
+ /**
+ * Tests label key and label callback of an entity.
+ */
+ function testEntityLabel() {
+ $entity_types = array(
+ 'test_entity_no_label',
+ 'test_entity_label',
+ 'test_entity_label_callback',
+ );
+
+ $entity = field_test_create_stub_entity();
+
+ foreach ($entity_types as $entity_type) {
+ $label = entity_label($entity_type, $entity);
+
+ switch ($entity_type) {
+ case 'test_entity_no_label':
+ $this->assertFalse($label, 'Entity with no label property or callback returned FALSE.');
+ break;
+
+ case 'test_entity_label':
+ $this->assertEqual($label, $entity->ftlabel, 'Entity with label key returned correct label.');
+ break;
+
+ case 'test_entity_label_callback':
+ $this->assertEqual($label, 'label callback ' . $entity->ftlabel, 'Entity with label callback returned correct label.');
+ break;
+ }
+ }
+ }
+}
diff --git a/drupal-dev/modules/field/tests/field_test.entity.inc b/drupal-dev/modules/field/tests/field_test.entity.inc
new file mode 100644
index 0000000..c6686eb
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.entity.inc
@@ -0,0 +1,500 @@
+ array('label' => 'Test Bundle')));
+ $test_entity_modes = array(
+ 'full' => array(
+ 'label' => t('Full object'),
+ 'custom settings' => TRUE,
+ ),
+ 'teaser' => array(
+ 'label' => t('Teaser'),
+ 'custom settings' => TRUE,
+ ),
+ );
+
+ return array(
+ 'test_entity' => array(
+ 'label' => t('Test Entity'),
+ 'fieldable' => TRUE,
+ 'field cache' => FALSE,
+ 'base table' => 'test_entity',
+ 'revision table' => 'test_entity_revision',
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'revision' => 'ftvid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ // This entity type doesn't get form handling for now...
+ 'test_cacheable_entity' => array(
+ 'label' => t('Test Entity, cacheable'),
+ 'fieldable' => TRUE,
+ 'field cache' => TRUE,
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'revision' => 'ftvid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ 'test_entity_bundle_key' => array(
+ 'label' => t('Test Entity with a bundle key.'),
+ 'base table' => 'test_entity_bundle_key',
+ 'fieldable' => TRUE,
+ 'field cache' => FALSE,
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => array('bundle1' => array('label' => 'Bundle1'), 'bundle2' => array('label' => 'Bundle2')) + $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ // In this case, the bundle key is not stored in the database.
+ 'test_entity_bundle' => array(
+ 'label' => t('Test Entity with a specified bundle.'),
+ 'base table' => 'test_entity_bundle',
+ 'fieldable' => TRUE,
+ 'controller class' => 'TestEntityBundleController',
+ 'field cache' => FALSE,
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => array('test_entity_2' => array('label' => 'Test entity 2')) + $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ // @see EntityPropertiesTestCase::testEntityLabel()
+ 'test_entity_no_label' => array(
+ 'label' => t('Test entity without label'),
+ 'fieldable' => TRUE,
+ 'field cache' => FALSE,
+ 'base table' => 'test_entity',
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'revision' => 'ftvid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ 'test_entity_label' => array(
+ 'label' => t('Test entity label'),
+ 'fieldable' => TRUE,
+ 'field cache' => FALSE,
+ 'base table' => 'test_entity',
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'revision' => 'ftvid',
+ 'bundle' => 'fttype',
+ 'label' => 'ftlabel',
+ ),
+ 'bundles' => $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ 'test_entity_label_callback' => array(
+ 'label' => t('Test entity label callback'),
+ 'fieldable' => TRUE,
+ 'field cache' => FALSE,
+ 'base table' => 'test_entity',
+ 'label callback' => 'field_test_entity_label_callback',
+ 'entity keys' => array(
+ 'id' => 'ftid',
+ 'revision' => 'ftvid',
+ 'bundle' => 'fttype',
+ ),
+ 'bundles' => $bundles,
+ 'view modes' => $test_entity_modes,
+ ),
+ );
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function field_test_entity_info_alter(&$entity_info) {
+ // Enable/disable field_test as a translation handler.
+ foreach (field_test_entity_info_translatable() as $entity_type => $translatable) {
+ $entity_info[$entity_type]['translation']['field_test'] = $translatable;
+ }
+ // Disable locale as a translation handler.
+ foreach ($entity_info as $entity_type => $info) {
+ $entity_info[$entity_type]['translation']['locale'] = FALSE;
+ }
+}
+
+/**
+ * Helper function to enable entity translations.
+ */
+function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) {
+ drupal_static_reset('field_has_translation_handler');
+ $stored_value = &drupal_static(__FUNCTION__, array());
+ if (isset($entity_type)) {
+ $stored_value[$entity_type] = $translatable;
+ entity_info_cache_clear();
+ }
+ return $stored_value;
+}
+
+/**
+ * Creates a new bundle for test_entity entities.
+ *
+ * @param $bundle
+ * The machine-readable name of the bundle.
+ * @param $text
+ * The human-readable name of the bundle. If none is provided, the machine
+ * name will be used.
+ */
+function field_test_create_bundle($bundle, $text = NULL) {
+ $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
+ $bundles += array($bundle => array('label' => $text ? $text : $bundle));
+ variable_set('field_test_bundles', $bundles);
+
+ $info = field_test_entity_info();
+ foreach ($info as $type => $type_info) {
+ field_attach_create_bundle($type, $bundle);
+ }
+}
+
+/**
+ * Renames a bundle for test_entity entities.
+ *
+ * @param $bundle_old
+ * The machine-readable name of the bundle to rename.
+ * @param $bundle_new
+ * The new machine-readable name of the bundle.
+ */
+function field_test_rename_bundle($bundle_old, $bundle_new) {
+ $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
+ $bundles[$bundle_new] = $bundles[$bundle_old];
+ unset($bundles[$bundle_old]);
+ variable_set('field_test_bundles', $bundles);
+
+ $info = field_test_entity_info();
+ foreach ($info as $type => $type_info) {
+ field_attach_rename_bundle($type, $bundle_old, $bundle_new);
+ }
+}
+
+/**
+ * Deletes a bundle for test_entity objects.
+ *
+ * @param $bundle
+ * The machine-readable name of the bundle to delete.
+ */
+function field_test_delete_bundle($bundle) {
+ $bundles = variable_get('field_test_bundles', array('test_bundle' => array('label' => 'Test Bundle')));
+ unset($bundles[$bundle]);
+ variable_set('field_test_bundles', $bundles);
+
+ $info = field_test_entity_info();
+ foreach ($info as $type => $type_info) {
+ field_attach_delete_bundle($type, $bundle);
+ }
+}
+
+/**
+ * Creates a basic test_entity entity.
+ */
+function field_test_create_stub_entity($id = 1, $vid = 1, $bundle = 'test_bundle', $label = '') {
+ $entity = new stdClass();
+ // Only set id and vid properties if they don't come as NULL (creation form).
+ if (isset($id)) {
+ $entity->ftid = $id;
+ }
+ if (isset($vid)) {
+ $entity->ftvid = $vid;
+ }
+ $entity->fttype = $bundle;
+
+ $label = !empty($label) ? $label : $bundle . ' label';
+ $entity->ftlabel = $label;
+
+ return $entity;
+}
+
+/**
+ * Loads a test_entity.
+ *
+ * @param $ftid
+ * The id of the entity to load.
+ * @param $ftvid
+ * (Optional) The revision id of the entity to load. If not specified, the
+ * current revision will be used.
+ * @return
+ * The loaded entity.
+ */
+function field_test_entity_test_load($ftid, $ftvid = NULL) {
+ // Load basic strucure.
+ $query = db_select('test_entity', 'fte', array())
+ ->condition('fte.ftid', $ftid);
+
+ if ($ftvid) {
+ $query->join('test_entity_revision', 'fter', 'fte.ftid = fter.ftid');
+ $query->addField('fte', 'ftid');
+ $query->addField('fte', 'fttype');
+ $query->addField('fter', 'ftvid');
+ $query->condition('fter.ftvid', $ftvid);
+ }
+ else {
+ $query->fields('fte');
+ }
+
+ $entities = $query->execute()->fetchAllAssoc('ftid');
+
+ // Attach fields.
+ if ($ftvid) {
+ field_attach_load_revision('test_entity', $entities);
+ }
+ else {
+ field_attach_load('test_entity', $entities);
+ }
+
+ return $entities[$ftid];
+}
+
+/**
+ * Saves a test_entity.
+ *
+ * A new entity is created if $entity->ftid and $entity->is_new are both empty.
+ * A new revision is created if $entity->revision is not empty.
+ *
+ * @param $entity
+ * The entity to save.
+ */
+function field_test_entity_save(&$entity) {
+ field_attach_presave('test_entity', $entity);
+
+ if (!isset($entity->is_new)) {
+ $entity->is_new = empty($entity->ftid);
+ }
+
+ if (!$entity->is_new && !empty($entity->revision)) {
+ $entity->old_ftvid = $entity->ftvid;
+ unset($entity->ftvid);
+ }
+
+ $update_entity = TRUE;
+ if ($entity->is_new) {
+ drupal_write_record('test_entity', $entity);
+ drupal_write_record('test_entity_revision', $entity);
+ $op = 'insert';
+ }
+ else {
+ drupal_write_record('test_entity', $entity, 'ftid');
+ if (!empty($entity->revision)) {
+ drupal_write_record('test_entity_revision', $entity);
+ }
+ else {
+ drupal_write_record('test_entity_revision', $entity, 'ftvid');
+ $update_entity = FALSE;
+ }
+ $op = 'update';
+ }
+ if ($update_entity) {
+ db_update('test_entity')
+ ->fields(array('ftvid' => $entity->ftvid))
+ ->condition('ftid', $entity->ftid)
+ ->execute();
+ }
+
+ // Save fields.
+ $function = "field_attach_$op";
+ $function('test_entity', $entity);
+}
+
+/**
+ * Menu callback: displays the 'Add new test_entity' form.
+ */
+function field_test_entity_add($fttype) {
+ $fttype = str_replace('-', '_', $fttype);
+ $entity = (object)array('fttype' => $fttype);
+ drupal_set_title(t('Create test_entity @bundle', array('@bundle' => $fttype)), PASS_THROUGH);
+ return drupal_get_form('field_test_entity_form', $entity, TRUE);
+}
+
+/**
+ * Menu callback: displays the 'Edit exiisting test_entity' form.
+ */
+function field_test_entity_edit($entity) {
+ drupal_set_title(t('test_entity @ftid revision @ftvid', array('@ftid' => $entity->ftid, '@ftvid' => $entity->ftvid)), PASS_THROUGH);
+ return drupal_get_form('field_test_entity_form', $entity);
+}
+
+/**
+ * Test_entity form.
+ */
+function field_test_entity_form($form, &$form_state, $entity, $add = FALSE) {
+ // During initial form build, add the entity to the form state for use during
+ // form building and processing. During a rebuild, use what is in the form
+ // state.
+ if (!isset($form_state['test_entity'])) {
+ $form_state['test_entity'] = $entity;
+ }
+ else {
+ $entity = $form_state['test_entity'];
+ }
+
+ foreach (array('ftid', 'ftvid', 'fttype') as $key) {
+ $form[$key] = array(
+ '#type' => 'value',
+ '#value' => isset($entity->$key) ? $entity->$key : NULL,
+ );
+ }
+
+ // Add field widgets.
+ field_attach_form('test_entity', $entity, $form, $form_state);
+
+ if (!$add) {
+ $form['revision'] = array(
+ '#access' => user_access('administer field_test content'),
+ '#type' => 'checkbox',
+ '#title' => t('Create new revision'),
+ '#default_value' => FALSE,
+ '#weight' => 100,
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#weight' => 101,
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for field_test_entity_form().
+ */
+function field_test_entity_form_validate($form, &$form_state) {
+ entity_form_field_validate('test_entity', $form, $form_state);
+}
+
+/**
+ * Submit handler for field_test_entity_form().
+ */
+function field_test_entity_form_submit($form, &$form_state) {
+ $entity = field_test_entity_form_submit_build_test_entity($form, $form_state);
+ $insert = empty($entity->ftid);
+ field_test_entity_save($entity);
+
+ $message = $insert ? t('test_entity @id has been created.', array('@id' => $entity->ftid)) : t('test_entity @id has been updated.', array('@id' => $entity->ftid));
+ drupal_set_message($message);
+
+ if ($entity->ftid) {
+ $form_state['redirect'] = 'test-entity/manage/' . $entity->ftid . '/edit';
+ }
+ else {
+ // Error on save.
+ drupal_set_message(t('The entity could not be saved.'), 'error');
+ $form_state['rebuild'] = TRUE;
+ }
+}
+
+/**
+ * Updates the form state's entity by processing this submission's values.
+ */
+function field_test_entity_form_submit_build_test_entity($form, &$form_state) {
+ $entity = $form_state['test_entity'];
+ entity_form_submit_build_entity('test_entity', $entity, $form, $form_state);
+ return $entity;
+}
+
+/**
+ * Form combining two separate entities.
+ */
+function field_test_entity_nested_form($form, &$form_state, $entity_1, $entity_2) {
+ // First entity.
+ foreach (array('ftid', 'ftvid', 'fttype') as $key) {
+ $form[$key] = array(
+ '#type' => 'value',
+ '#value' => $entity_1->$key,
+ );
+ }
+ field_attach_form('test_entity', $entity_1, $form, $form_state);
+
+ // Second entity.
+ $form['entity_2'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Second entity'),
+ '#tree' => TRUE,
+ '#parents' => array('entity_2'),
+ '#weight' => 50,
+ );
+ foreach (array('ftid', 'ftvid', 'fttype') as $key) {
+ $form['entity_2'][$key] = array(
+ '#type' => 'value',
+ '#value' => $entity_2->$key,
+ );
+ }
+ field_attach_form('test_entity', $entity_2, $form['entity_2'], $form_state);
+
+ $form['save'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ '#weight' => 100,
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for field_test_entity_nested_form().
+ */
+function field_test_entity_nested_form_validate($form, &$form_state) {
+ $entity_1 = (object) $form_state['values'];
+ field_attach_form_validate('test_entity', $entity_1, $form, $form_state);
+
+ $entity_2 = (object) $form_state['values']['entity_2'];
+ field_attach_form_validate('test_entity', $entity_2, $form['entity_2'], $form_state);
+}
+
+/**
+ * Submit handler for field_test_entity_nested_form().
+ */
+function field_test_entity_nested_form_submit($form, &$form_state) {
+ $entity_1 = (object) $form_state['values'];
+ field_attach_submit('test_entity', $entity_1, $form, $form_state);
+ field_test_entity_save($entity_1);
+
+ $entity_2 = (object) $form_state['values']['entity_2'];
+ field_attach_submit('test_entity', $entity_2, $form['entity_2'], $form_state);
+ field_test_entity_save($entity_2);
+
+ drupal_set_message(t('test_entities @id_1 and @id_2 have been updated.', array('@id_1' => $entity_1->ftid, '@id_2' => $entity_2->ftid)));
+}
+
+/**
+ * Controller class for the test_entity_bundle entity type.
+ *
+ * This extends the DrupalDefaultEntityController class, adding required
+ * special handling for bundles (since they are not stored in the database).
+ */
+class TestEntityBundleController extends DrupalDefaultEntityController {
+
+ protected function attachLoad(&$entities, $revision_id = FALSE) {
+ // Add bundle information.
+ foreach ($entities as $key => $entity) {
+ $entity->fttype = 'test_entity_bundle';
+ $entities[$key] = $entity;
+ }
+ parent::attachLoad($entities, $revision_id);
+ }
+}
diff --git a/drupal-dev/modules/field/tests/field_test.field.inc b/drupal-dev/modules/field/tests/field_test.field.inc
new file mode 100644
index 0000000..1cab773
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.field.inc
@@ -0,0 +1,413 @@
+ array(
+ 'label' => t('Test field'),
+ 'description' => t('Dummy field type used for tests.'),
+ 'settings' => array(
+ 'test_field_setting' => 'dummy test string',
+ 'changeable' => 'a changeable field setting',
+ 'unchangeable' => 'an unchangeable field setting',
+ ),
+ 'instance_settings' => array(
+ 'test_instance_setting' => 'dummy test string',
+ 'test_hook_field_load' => FALSE,
+ ),
+ 'default_widget' => 'test_field_widget',
+ 'default_formatter' => 'field_test_default',
+ ),
+ 'shape' => array(
+ 'label' => t('Shape'),
+ 'description' => t('Another dummy field type.'),
+ 'settings' => array(
+ 'foreign_key_name' => 'shape',
+ ),
+ 'instance_settings' => array(),
+ 'default_widget' => 'test_field_widget',
+ 'default_formatter' => 'field_test_default',
+ ),
+ 'hidden_test_field' => array(
+ 'no_ui' => TRUE,
+ 'label' => t('Hidden from UI test field'),
+ 'description' => t('Dummy hidden field type used for tests.'),
+ 'settings' => array(),
+ 'instance_settings' => array(),
+ 'default_widget' => 'test_field_widget',
+ 'default_formatter' => 'field_test_default',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_update_forbid().
+ */
+function field_test_field_update_forbid($field, $prior_field, $has_data) {
+ if ($field['type'] == 'test_field' && $field['settings']['unchangeable'] != $prior_field['settings']['unchangeable']) {
+ throw new FieldException("field_test 'unchangeable' setting cannot be changed'");
+ }
+}
+
+/**
+ * Implements hook_field_load().
+ */
+function field_test_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+
+ foreach ($items as $id => $item) {
+ // To keep the test non-intrusive, only act for instances with the
+ // test_hook_field_load setting explicitly set to TRUE.
+ if ($instances[$id]['settings']['test_hook_field_load']) {
+ foreach ($item as $delta => $value) {
+ // Don't add anything on empty values.
+ if ($value) {
+ $items[$id][$delta]['additional_key'] = 'additional_value';
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_insert().
+ */
+function field_test_field_insert($entity_type, $entity, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Implements hook_field_update().
+ */
+function field_test_field_update($entity_type, $entity, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Implements hook_field_delete().
+ */
+function field_test_field_delete($entity_type, $entity, $field, $instance, $items) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Implements hook_field_validate().
+ *
+ * Possible error codes:
+ * - 'field_test_invalid': The value is invalid.
+ */
+function field_test_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+
+ foreach ($items as $delta => $item) {
+ if ($item['value'] == -1) {
+ $errors[$field['field_name']][$langcode][$delta][] = array(
+ 'error' => 'field_test_invalid',
+ 'message' => t('%name does not accept the value -1.', array('%name' => $instance['label'])),
+ );
+ }
+ }
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function field_test_field_is_empty($item, $field) {
+ return empty($item['value']);
+}
+
+/**
+ * Implements hook_field_settings_form().
+ */
+function field_test_field_settings_form($field, $instance, $has_data) {
+ $settings = $field['settings'];
+
+ $form['test_field_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Field test field setting'),
+ '#default_value' => $settings['test_field_setting'],
+ '#required' => FALSE,
+ '#description' => t('A dummy form element to simulate field setting.'),
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_instance_settings_form().
+ */
+function field_test_field_instance_settings_form($field, $instance) {
+ $settings = $instance['settings'];
+
+ $form['test_instance_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Field test field instance setting'),
+ '#default_value' => $settings['test_instance_setting'],
+ '#required' => FALSE,
+ '#description' => t('A dummy form element to simulate field instance setting.'),
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function field_test_field_widget_info() {
+ return array(
+ 'test_field_widget' => array(
+ 'label' => t('Test field'),
+ 'field types' => array('test_field', 'hidden_test_field'),
+ 'settings' => array('test_widget_setting' => 'dummy test string'),
+ ),
+ 'test_field_widget_multiple' => array(
+ 'label' => t('Test field 1'),
+ 'field types' => array('test_field'),
+ 'settings' => array('test_widget_setting_multiple' => 'dummy test string'),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function field_test_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+ switch ($instance['widget']['type']) {
+ case 'test_field_widget':
+ $element += array(
+ '#type' => 'textfield',
+ '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : '',
+ );
+ return array('value' => $element);
+
+ case 'test_field_widget_multiple':
+ $values = array();
+ foreach ($items as $delta => $value) {
+ $values[] = $value['value'];
+ }
+ $element += array(
+ '#type' => 'textfield',
+ '#default_value' => implode(', ', $values),
+ '#element_validate' => array('field_test_widget_multiple_validate'),
+ );
+ return $element;
+ }
+}
+
+/**
+ * Form element validation handler for 'test_field_widget_multiple' widget.
+ */
+function field_test_widget_multiple_validate($element, &$form_state) {
+ $values = array_map('trim', explode(',', $element['#value']));
+ $items = array();
+ foreach ($values as $value) {
+ $items[] = array('value' => $value);
+ }
+ form_set_value($element, $items, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_error().
+ */
+function field_test_field_widget_error($element, $error, $form, &$form_state) {
+ // @todo No easy way to differenciate widget types, we should receive it as a
+ // parameter.
+ if (isset($element['value'])) {
+ // Widget is test_field_widget.
+ $error_element = $element['value'];
+ }
+ else {
+ // Widget is test_field_widget_multiple.
+ $error_element = $element;
+ }
+
+ form_error($error_element, $error['message']);
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function field_test_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'];
+
+ $form['test_widget_setting'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Field test field widget setting'),
+ '#default_value' => $settings['test_widget_setting'],
+ '#required' => FALSE,
+ '#description' => t('A dummy form element to simulate field widget setting.'),
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function field_test_field_formatter_info() {
+ return array(
+ 'field_test_default' => array(
+ 'label' => t('Default'),
+ 'description' => t('Default formatter'),
+ 'field types' => array('test_field'),
+ 'settings' => array(
+ 'test_formatter_setting' => 'dummy test string',
+ ),
+ ),
+ 'field_test_multiple' => array(
+ 'label' => t('Multiple'),
+ 'description' => t('Multiple formatter'),
+ 'field types' => array('test_field'),
+ 'settings' => array(
+ 'test_formatter_setting_multiple' => 'dummy test string',
+ ),
+ ),
+ 'field_test_with_prepare_view' => array(
+ 'label' => t('Tests hook_field_formatter_prepare_view()'),
+ 'field types' => array('test_field'),
+ 'settings' => array(
+ 'test_formatter_setting_additional' => 'dummy test string',
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function field_test_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $element = array();
+
+ // The name of the setting depends on the formatter type.
+ $map = array(
+ 'field_test_default' => 'test_formatter_setting',
+ 'field_test_multiple' => 'test_formatter_setting_multiple',
+ 'field_test_with_prepare_view' => 'test_formatter_setting_additional',
+ );
+
+ if (isset($map[$display['type']])) {
+ $name = $map[$display['type']];
+
+ $element[$name] = array(
+ '#title' => t('Setting'),
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#default_value' => $settings[$name],
+ '#required' => TRUE,
+ );
+ }
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function field_test_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $summary = '';
+
+ // The name of the setting depends on the formatter type.
+ $map = array(
+ 'field_test_default' => 'test_formatter_setting',
+ 'field_test_multiple' => 'test_formatter_setting_multiple',
+ 'field_test_with_prepare_view' => 'test_formatter_setting_additional',
+ );
+
+ if (isset($map[$display['type']])) {
+ $name = $map[$display['type']];
+ $summary = t('@setting: @value', array('@setting' => $name, '@value' => $settings[$name]));
+ }
+
+ return $summary;
+}
+
+/**
+ * Implements hook_field_formatter_prepare_view().
+ */
+function field_test_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
+ foreach ($items as $id => $item) {
+ // To keep the test non-intrusive, only act on the
+ // 'field_test_with_prepare_view' formatter.
+ if ($displays[$id]['type'] == 'field_test_with_prepare_view') {
+ foreach ($item as $delta => $value) {
+ // Don't add anything on empty values.
+ if ($value) {
+ $items[$id][$delta]['additional_formatter_value'] = $value['value'] + 1;
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function field_test_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+ $settings = $display['settings'];
+
+ switch ($display['type']) {
+ case 'field_test_default':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array('#markup' => $settings['test_formatter_setting'] . '|' . $item['value']);
+ }
+ break;
+
+ case 'field_test_with_prepare_view':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array('#markup' => $settings['test_formatter_setting_additional'] . '|' . $item['value'] . '|' . $item['additional_formatter_value']);
+ }
+ break;
+
+ case 'field_test_multiple':
+ if (!empty($items)) {
+ $array = array();
+ foreach ($items as $delta => $item) {
+ $array[] = $delta . ':' . $item['value'];
+ }
+ $element[0] = array('#markup' => $settings['test_formatter_setting_multiple'] . '|' . implode('|', $array));
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Sample 'default value' callback.
+ */
+function field_test_default_value($entity_type, $entity, $field, $instance) {
+ return array(array('value' => 99));
+}
+
+/**
+ * Implements hook_field_access().
+ */
+function field_test_field_access($op, $field, $entity_type, $entity, $account) {
+ if ($field['field_name'] == "field_no_{$op}_access") {
+ return FALSE;
+ }
+ return TRUE;
+}
diff --git a/drupal-dev/modules/field/tests/field_test.info b/drupal-dev/modules/field/tests/field_test.info
new file mode 100644
index 0000000..ce4db19
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.info
@@ -0,0 +1,13 @@
+name = "Field API Test"
+description = "Support module for the Field API tests."
+core = 7.x
+package = Testing
+files[] = field_test.entity.inc
+version = VERSION
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/field/tests/field_test.install b/drupal-dev/modules/field/tests/field_test.install
new file mode 100644
index 0000000..eaf1390
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.install
@@ -0,0 +1,162 @@
+fields(array('weight' => 1))
+ ->condition('name', 'field_test')
+ ->execute();
+}
+
+/**
+ * Implements hook_schema().
+ */
+function field_test_schema() {
+ $schema['test_entity'] = array(
+ 'description' => 'The base table for test_entities.',
+ 'fields' => array(
+ 'ftid' => array(
+ 'description' => 'The primary identifier for a test_entity.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'ftvid' => array(
+ 'description' => 'The current {test_entity_revision}.ftvid version identifier.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'fttype' => array(
+ 'description' => 'The type of this test_entity.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'ftlabel' => array(
+ 'description' => 'The label of this test_entity.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ ),
+ 'unique keys' => array(
+ 'ftvid' => array('ftvid'),
+ ),
+ 'primary key' => array('ftid'),
+ );
+ $schema['test_entity_bundle_key'] = array(
+ 'description' => 'The base table for test entities with a bundle key.',
+ 'fields' => array(
+ 'ftid' => array(
+ 'description' => 'The primary identifier for a test_entity_bundle_key.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'fttype' => array(
+ 'description' => 'The type of this test_entity.',
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => FALSE,
+ 'default' => '',
+ ),
+ ),
+ );
+ $schema['test_entity_bundle'] = array(
+ 'description' => 'The base table for test entities with a bundle.',
+ 'fields' => array(
+ 'ftid' => array(
+ 'description' => 'The primary identifier for a test_entity_bundle.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ ),
+ );
+ $schema['test_entity_revision'] = array(
+ 'description' => 'Stores information about each saved version of a {test_entity}.',
+ 'fields' => array(
+ 'ftid' => array(
+ 'description' => 'The {test_entity} this version belongs to.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'ftvid' => array(
+ 'description' => 'The primary identifier for this version.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'indexes' => array(
+ 'nid' => array('ftid'),
+ ),
+ 'primary key' => array('ftvid'),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_field_schema().
+ */
+function field_test_field_schema($field) {
+ if ($field['type'] == 'test_field') {
+ return array(
+ 'columns' => array(
+ 'value' => array(
+ 'type' => 'int',
+ 'size' => 'medium',
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'value' => array('value'),
+ ),
+ );
+ }
+ else {
+ $foreign_keys = array();
+ // The 'foreign keys' key is not always used in tests.
+ if (!empty($field['settings']['foreign_key_name'])) {
+ $foreign_keys['foreign keys'] = array(
+ // This is a dummy foreign key definition, references a table that
+ // doesn't exist, but that's not a problem.
+ $field['settings']['foreign_key_name'] => array(
+ 'table' => $field['settings']['foreign_key_name'],
+ 'columns' => array($field['settings']['foreign_key_name'] => 'id'),
+ ),
+ );
+ }
+ return array(
+ 'columns' => array(
+ 'shape' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => FALSE,
+ ),
+ 'color' => array(
+ 'type' => 'varchar',
+ 'length' => 32,
+ 'not null' => FALSE,
+ ),
+ ),
+ ) + $foreign_keys;
+ }
+}
diff --git a/drupal-dev/modules/field/tests/field_test.module b/drupal-dev/modules/field/tests/field_test.module
new file mode 100644
index 0000000..dc2023a
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.module
@@ -0,0 +1,269 @@
+ array(
+ 'title' => t('Access field_test content'),
+ 'description' => t('View published field_test content.'),
+ ),
+ 'administer field_test content' => array(
+ 'title' => t('Administer field_test content'),
+ 'description' => t('Manage field_test content'),
+ ),
+ );
+ return $perms;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function field_test_menu() {
+ $items = array();
+ $bundles = field_info_bundles('test_entity');
+
+ foreach ($bundles as $bundle_name => $bundle_info) {
+ $bundle_url_str = str_replace('_', '-', $bundle_name);
+ $items['test-entity/add/' . $bundle_url_str] = array(
+ 'title' => t('Add %bundle test_entity', array('%bundle' => $bundle_info['label'])),
+ 'page callback' => 'field_test_entity_add',
+ 'page arguments' => array(2),
+ 'access arguments' => array('administer field_test content'),
+ 'type' => MENU_NORMAL_ITEM,
+ );
+ }
+ $items['test-entity/manage/%field_test_entity_test/edit'] = array(
+ 'title' => 'Edit test entity',
+ 'page callback' => 'field_test_entity_edit',
+ 'page arguments' => array(2),
+ 'access arguments' => array('administer field_test content'),
+ 'type' => MENU_NORMAL_ITEM,
+ );
+
+ $items['test-entity/nested/%field_test_entity_test/%field_test_entity_test'] = array(
+ 'title' => 'Nested entity form',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_test_entity_nested_form', 2, 3),
+ 'access arguments' => array('administer field_test content'),
+ 'type' => MENU_NORMAL_ITEM,
+ );
+
+ return $items;
+}
+
+/**
+ * Generic op to test _field_invoke behavior.
+ *
+ * This simulates a field operation callback to be invoked by _field_invoke().
+ */
+function field_test_field_test_op($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ return array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field['field_name'], $langcode, $items))));
+}
+
+/**
+ * Generic op to test _field_invoke_multiple behavior.
+ *
+ * This simulates a multiple field operation callback to be invoked by
+ * _field_invoke_multiple().
+ */
+function field_test_field_test_op_multiple($entity_type, $entities, $field, $instances, $langcode, &$items) {
+ $result = array();
+ foreach ($entities as $id => $entity) {
+ // Entities, instances and items are assumed to be consistently grouped by
+ // language. To verify this we try to access all the passed data structures
+ // by entity id. If they are grouped correctly, one entity, one instance and
+ // one array of items should be available for each entity id.
+ $field_name = $instances[$id]['field_name'];
+ $result[$id] = array($langcode => hash('sha256', serialize(array($entity_type, $entity, $field_name, $langcode, $items[$id]))));
+ }
+ return $result;
+}
+
+/**
+ * Implements hook_field_available_languages_alter().
+ */
+function field_test_field_available_languages_alter(&$languages, $context) {
+ if (variable_get('field_test_field_available_languages_alter', FALSE)) {
+ // Add an unavailable language.
+ $languages[] = 'xx';
+ // Remove an available language.
+ $index = array_search('en', $languages);
+ unset($languages[$index]);
+ }
+}
+
+/**
+ * Implements hook_field_language_alter().
+ */
+function field_test_field_language_alter(&$display_language, $context) {
+ if (variable_get('field_test_language_fallback', TRUE)) {
+ locale_field_language_fallback($display_language, $context['entity'], $context['language']);
+ }
+}
+
+/**
+ * Store and retrieve keyed data for later verification by unit tests.
+ *
+ * This function is a simple in-memory key-value store with the
+ * distinction that it stores all values for a given key instead of
+ * just the most recently set value. field_test module hooks call
+ * this function to record their arguments, keyed by hook name. The
+ * unit tests later call this function to verify that the correct
+ * hooks were called and were passed the correct arguments.
+ *
+ * This function ignores all calls until the first time it is called
+ * with $key of NULL. Each time it is called with $key of NULL, it
+ * erases all previously stored data from its internal cache, but also
+ * returns the previously stored data to the caller. A typical usage
+ * scenario is:
+ *
+ * @code
+ * // calls to field_test_memorize() here are ignored
+ *
+ * // turn on memorization
+ * field_test_memorize();
+ *
+ * // call some Field API functions that invoke field_test hooks
+ * $field = field_create_field(...);
+ *
+ * // retrieve and reset the memorized hook call data
+ * $mem = field_test_memorize();
+ *
+ * // make sure hook_field_create_field() is invoked correctly
+ * assertEqual(count($mem['field_test_field_create_field']), 1);
+ * assertEqual($mem['field_test_field_create_field'][0], array($field));
+ * @endcode
+ *
+ * @param $key
+ * The key under which to store to $value, or NULL as described above.
+ * @param $value
+ * A value to store for $key.
+ * @return
+ * An array mapping each $key to an array of each $value passed in
+ * for that key.
+ */
+function field_test_memorize($key = NULL, $value = NULL) {
+ $memorize = &drupal_static(__FUNCTION__, NULL);
+
+ if (!isset($key)) {
+ $return = $memorize;
+ $memorize = array();
+ return $return;
+ }
+ if (is_array($memorize)) {
+ $memorize[$key][] = $value;
+ }
+}
+
+/**
+ * Memorize calls to hook_field_create_field().
+ */
+function field_test_field_create_field($field) {
+ $args = func_get_args();
+ field_test_memorize(__FUNCTION__, $args);
+}
+
+/**
+ * Implements hook_entity_query_alter().
+ */
+function field_test_entity_query_alter(&$query) {
+ if (!empty($query->alterMyExecuteCallbackPlease)) {
+ $query->executeCallback = 'field_test_dummy_field_storage_query';
+ }
+}
+
+/**
+ * Pseudo-implements hook_field_storage_query().
+ */
+function field_test_dummy_field_storage_query(EntityFieldQuery $query) {
+ // Return dummy values that will be checked by the test.
+ return array(
+ 'user' => array(
+ 1 => entity_create_stub_entity('user', array(1, NULL, NULL)),
+ ),
+ );
+}
+
+/**
+ * Implements callback_entity_info_label().
+ *
+ * @return
+ * The label of the entity prefixed with "label callback".
+ */
+function field_test_entity_label_callback($entity) {
+ return 'label callback ' . $entity->ftlabel;
+}
+
+/**
+ * Implements hook_field_attach_view_alter().
+ */
+function field_test_field_attach_view_alter(&$output, $context) {
+ if (!empty($context['display']['settings']['alter'])) {
+ $output['test_field'][] = array('#markup' => 'field_test_field_attach_view_alter');
+ }
+}
+
+/**
+ * Implements hook_field_widget_properties_alter().
+ */
+function field_test_field_widget_properties_alter(&$widget, $context) {
+ // Make the alter_test_text field 42 characters for nodes and comments.
+ if (in_array($context['entity_type'], array('node', 'comment')) && ($context['field']['field_name'] == 'alter_test_text')) {
+ $widget['settings']['size'] = 42;
+ }
+}
+
+/**
+ * Implements hook_field_widget_properties_ENTITY_TYPE_alter().
+ */
+function field_test_field_widget_properties_user_alter(&$widget, $context) {
+ // Always use buttons for the alter_test_options field on user forms.
+ if ($context['field']['field_name'] == 'alter_test_options') {
+ $widget['type'] = 'options_buttons';
+ }
+}
+
+/**
+ * Implements hook_field_widget_form_alter().
+ */
+function field_test_field_widget_form_alter(&$element, &$form_state, $context) {
+ switch ($context['field']['field_name']) {
+ case 'alter_test_text':
+ drupal_set_message('Field size: ' . $context['instance']['widget']['settings']['size']);
+ break;
+
+ case 'alter_test_options':
+ drupal_set_message('Widget type: ' . $context['instance']['widget']['type']);
+ break;
+ }
+}
+
+/**
+ * Implements hook_query_TAG_alter() for tag 'efq_table_prefixing_test'.
+ *
+ * @see EntityFieldQueryTestCase::testTablePrefixing()
+ */
+function field_test_query_efq_table_prefixing_test_alter(&$query) {
+ // Add an additional join onto the entity base table. This will cause an
+ // exception if the EFQ does not properly prefix the base table.
+ $query->join('test_entity','te2','%alias.ftid = test_entity.ftid');
+}
diff --git a/drupal-dev/modules/field/tests/field_test.storage.inc b/drupal-dev/modules/field/tests/field_test.storage.inc
new file mode 100644
index 0000000..a26af17
--- /dev/null
+++ b/drupal-dev/modules/field/tests/field_test.storage.inc
@@ -0,0 +1,473 @@
+ array(
+ 'label' => t('Test storage'),
+ 'description' => t('Dummy test storage backend. Stores field values in the variable table.'),
+ ),
+ 'field_test_storage_failure' => array(
+ 'label' => t('Test storage failure'),
+ 'description' => t('Dummy test storage backend. Always fails to create fields.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_storage_details().
+ */
+function field_test_field_storage_details($field) {
+ $details = array();
+
+ // Add field columns.
+ $columns = array();
+ foreach ((array) $field['columns'] as $column_name => $attributes) {
+ $columns[$column_name] = $column_name;
+ }
+ return array(
+ 'drupal_variables' => array(
+ 'field_test_storage_data[FIELD_LOAD_CURRENT]' => $columns,
+ 'field_test_storage_data[FIELD_LOAD_REVISION]' => $columns,
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_storage_details_alter().
+ *
+ * @see FieldAttachStorageTestCase::testFieldStorageDetailsAlter()
+ */
+function field_test_field_storage_details_alter(&$details, $field) {
+
+ // For testing, storage details are changed only because of the field name.
+ if ($field['field_name'] == 'field_test_change_my_details') {
+ $columns = array();
+ foreach ((array) $field['columns'] as $column_name => $attributes) {
+ $columns[$column_name] = $column_name;
+ }
+ $details['drupal_variables'] = array(
+ FIELD_LOAD_CURRENT => array(
+ 'moon' => $columns,
+ ),
+ FIELD_LOAD_REVISION => array(
+ 'mars' => $columns,
+ ),
+ );
+ }
+}
+
+/**
+ * Helper function: stores or retrieves data from the 'storage backend'.
+ */
+function _field_test_storage_data($data = NULL) {
+ if (!isset($data)) {
+ return variable_get('field_test_storage_data', array());
+ }
+ else {
+ variable_set('field_test_storage_data', $data);
+ }
+}
+
+/**
+ * Implements hook_field_storage_load().
+ */
+function field_test_field_storage_load($entity_type, $entities, $age, $fields, $options) {
+ $data = _field_test_storage_data();
+
+ $load_current = $age == FIELD_LOAD_CURRENT;
+
+ foreach ($fields as $field_id => $ids) {
+ $field = field_info_field_by_id($field_id);
+ $field_name = $field['field_name'];
+ $field_data = $data[$field['id']];
+ $sub_table = $load_current ? 'current' : 'revisions';
+ $delta_count = array();
+ foreach ($field_data[$sub_table] as $row) {
+ if ($row->type == $entity_type && (!$row->deleted || $options['deleted'])) {
+ if (($load_current && in_array($row->entity_id, $ids)) || (!$load_current && in_array($row->revision_id, $ids))) {
+ if (in_array($row->language, field_available_languages($entity_type, $field))) {
+ if (!isset($delta_count[$row->entity_id][$row->language])) {
+ $delta_count[$row->entity_id][$row->language] = 0;
+ }
+ if ($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta_count[$row->entity_id][$row->language] < $field['cardinality']) {
+ $item = array();
+ foreach ($field['columns'] as $column => $attributes) {
+ $item[$column] = $row->{$column};
+ }
+ $entities[$row->entity_id]->{$field_name}[$row->language][] = $item;
+ $delta_count[$row->entity_id][$row->language]++;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_storage_write().
+ */
+function field_test_field_storage_write($entity_type, $entity, $op, $fields) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ foreach ($fields as $field_id) {
+ $field = field_info_field_by_id($field_id);
+ $field_name = $field['field_name'];
+ $field_data = &$data[$field_id];
+
+ $all_languages = field_available_languages($entity_type, $field);
+ $field_languages = array_intersect($all_languages, array_keys((array) $entity->$field_name));
+
+ // Delete and insert, rather than update, in case a value was added.
+ if ($op == FIELD_STORAGE_UPDATE) {
+ // Delete languages present in the incoming $entity->$field_name.
+ // Delete all languages if $entity->$field_name is empty.
+ $languages = !empty($entity->$field_name) ? $field_languages : $all_languages;
+ if ($languages) {
+ foreach ($field_data['current'] as $key => $row) {
+ if ($row->type == $entity_type && $row->entity_id == $id && in_array($row->language, $languages)) {
+ unset($field_data['current'][$key]);
+ }
+ }
+ if (isset($vid)) {
+ foreach ($field_data['revisions'] as $key => $row) {
+ if ($row->type == $entity_type && $row->revision_id == $vid) {
+ unset($field_data['revisions'][$key]);
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($field_languages as $langcode) {
+ $items = (array) $entity->{$field_name}[$langcode];
+ $delta_count = 0;
+ foreach ($items as $delta => $item) {
+ $row = (object) array(
+ 'field_id' => $field_id,
+ 'type' => $entity_type,
+ 'entity_id' => $id,
+ 'revision_id' => $vid,
+ 'bundle' => $bundle,
+ 'delta' => $delta,
+ 'deleted' => FALSE,
+ 'language' => $langcode,
+ );
+ foreach ($field['columns'] as $column => $attributes) {
+ $row->{$column} = isset($item[$column]) ? $item[$column] : NULL;
+ }
+
+ $field_data['current'][] = $row;
+ if (isset($vid)) {
+ $field_data['revisions'][] = $row;
+ }
+
+ if ($field['cardinality'] != FIELD_CARDINALITY_UNLIMITED && ++$delta_count == $field['cardinality']) {
+ break;
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_storage_delete().
+ */
+function field_test_field_storage_delete($entity_type, $entity, $fields) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // Note: reusing field_test_storage_purge(), like field_sql_storage.module
+ // does, is highly inefficient in our case...
+ foreach (field_info_instances($bundle) as $instance) {
+ if (isset($fields[$instance['field_id']])) {
+ $field = field_info_field_by_id($instance['field_id']);
+ field_test_field_storage_purge($entity_type, $entity, $field, $instance);
+ }
+ }
+}
+
+/**
+ * Implements hook_field_storage_purge().
+ */
+function field_test_field_storage_purge($entity_type, $entity, $field, $instance) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as $key => $row) {
+ if ($row->type == $entity_type && $row->entity_id == $id) {
+ unset($field_data[$sub_table][$key]);
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_storage_delete_revision().
+ */
+function field_test_field_storage_delete_revision($entity_type, $entity, $fields) {
+ $data = _field_test_storage_data();
+
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+ foreach ($fields as $field_id) {
+ $field_data = &$data[$field_id];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as $key => $row) {
+ if ($row->type == $entity_type && $row->entity_id == $id && $row->revision_id == $vid) {
+ unset($field_data[$sub_table][$key]);
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_storage_query().
+ */
+function field_test_field_storage_query($field_id, $conditions, $count, &$cursor = NULL, $age) {
+ $data = _field_test_storage_data();
+
+ $load_current = $age == FIELD_LOAD_CURRENT;
+
+ $field = field_info_field_by_id($field_id);
+ $field_columns = array_keys($field['columns']);
+
+ $field_data = $data[$field['id']];
+ $sub_table = $load_current ? 'current' : 'revisions';
+ // We need to sort records by entity type and entity id.
+ usort($field_data[$sub_table], '_field_test_field_storage_query_sort_helper');
+
+ // Initialize results array.
+ $return = array();
+ $entity_count = 0;
+ $rows_count = 0;
+ $rows_total = count($field_data[$sub_table]);
+ $skip = $cursor;
+ $skipped = 0;
+
+ foreach ($field_data[$sub_table] as $row) {
+ if ($count != FIELD_QUERY_NO_LIMIT && $entity_count >= $count) {
+ break;
+ }
+
+ if ($row->field_id == $field['id']) {
+ $match = TRUE;
+ $condition_deleted = FALSE;
+ // Add conditions.
+ foreach ($conditions as $condition) {
+ @list($column, $value, $operator) = $condition;
+ if (empty($operator)) {
+ $operator = is_array($value) ? 'IN' : '=';
+ }
+ switch ($operator) {
+ case '=':
+ $match = $match && $row->{$column} == $value;
+ break;
+ case '<>':
+ case '<':
+ case '<=':
+ case '>':
+ case '>=':
+ eval('$match = $match && ' . $row->{$column} . ' ' . $operator . ' '. $value);
+ break;
+ case 'IN':
+ $match = $match && in_array($row->{$column}, $value);
+ break;
+ case 'NOT IN':
+ $match = $match && !in_array($row->{$column}, $value);
+ break;
+ case 'BETWEEN':
+ $match = $match && $row->{$column} >= $value[0] && $row->{$column} <= $value[1];
+ break;
+ case 'STARTS_WITH':
+ case 'ENDS_WITH':
+ case 'CONTAINS':
+ // Not supported.
+ $match = FALSE;
+ break;
+ }
+ // Track condition on 'deleted'.
+ if ($column == 'deleted') {
+ $condition_deleted = TRUE;
+ }
+ }
+
+ // Exclude deleted data unless we have a condition on it.
+ if (!$condition_deleted && $row->deleted) {
+ $match = FALSE;
+ }
+
+ if ($match) {
+ if (!isset($skip) || $skipped >= $skip) {
+ $cursor++;
+ // If querying all revisions and the entity type has revisions, we need
+ // to key the results by revision_ids.
+ $entity_type = entity_get_info($row->type);
+ $id = ($load_current || empty($entity_type['entity keys']['revision'])) ? $row->entity_id : $row->revision_id;
+
+ if (!isset($return[$row->type][$id])) {
+ $return[$row->type][$id] = entity_create_stub_entity($row->type, array($row->entity_id, $row->revision_id, $row->bundle));
+ $entity_count++;
+ }
+ }
+ else {
+ $skipped++;
+ }
+ }
+ }
+ $rows_count++;
+
+ // The query is complete if we walked the whole array.
+ if ($count != FIELD_QUERY_NO_LIMIT && $rows_count >= $rows_total) {
+ $cursor = FIELD_QUERY_COMPLETE;
+ }
+ }
+
+ return $return;
+}
+
+/**
+ * Sort helper for field_test_field_storage_query().
+ *
+ * Sorts by entity type and entity id.
+ */
+function _field_test_field_storage_query_sort_helper($a, $b) {
+ if ($a->type == $b->type) {
+ if ($a->entity_id == $b->entity_id) {
+ return 0;
+ }
+ else {
+ return $a->entity_id < $b->entity_id ? -1 : 1;
+ }
+ }
+ else {
+ return $a->type < $b->type ? -1 : 1;
+ }
+}
+
+/**
+ * Implements hook_field_storage_create_field().
+ */
+function field_test_field_storage_create_field($field) {
+ if ($field['storage']['type'] == 'field_test_storage_failure') {
+ throw new Exception('field_test_storage_failure engine always fails to create fields');
+ }
+
+ $data = _field_test_storage_data();
+
+ $data[$field['id']] = array(
+ 'current' => array(),
+ 'revisions' => array(),
+ );
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_storage_delete_field().
+ */
+function field_test_field_storage_delete_field($field) {
+ $data = _field_test_storage_data();
+
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ $row->deleted = TRUE;
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_storage_delete_instance().
+ */
+function field_test_field_storage_delete_instance($instance) {
+ $data = _field_test_storage_data();
+
+ $field = field_info_field($instance['field_name']);
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $instance['bundle']) {
+ $row->deleted = TRUE;
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_attach_create_bundle().
+ */
+function field_test_field_attach_create_bundle($bundle) {
+ // We don't need to do anything here.
+}
+
+/**
+ * Implements hook_field_attach_rename_bundle().
+ */
+function field_test_field_attach_rename_bundle($bundle_old, $bundle_new) {
+ $data = _field_test_storage_data();
+
+ // We need to account for deleted or inactive fields and instances.
+ $instances = field_read_instances(array('bundle' => $bundle_new), array('include_deleted' => TRUE, 'include_inactive' => TRUE));
+ foreach ($instances as $field_name => $instance) {
+ $field = field_info_field_by_id($instance['field_id']);
+ if ($field['storage']['type'] == 'field_test_storage') {
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $bundle_old) {
+ $row->bundle = $bundle_new;
+ }
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
+
+/**
+ * Implements hook_field_attach_delete_bundle().
+ */
+function field_test_field_attach_delete_bundle($entity_type, $bundle, $instances) {
+ $data = _field_test_storage_data();
+
+ foreach ($instances as $field_name => $instance) {
+ $field = field_info_field($field_name);
+ if ($field['storage']['type'] == 'field_test_storage') {
+ $field_data = &$data[$field['id']];
+ foreach (array('current', 'revisions') as $sub_table) {
+ foreach ($field_data[$sub_table] as &$row) {
+ if ($row->bundle == $bundle_old) {
+ $row->deleted = TRUE;
+ }
+ }
+ }
+ }
+ }
+
+ _field_test_storage_data($data);
+}
diff --git a/drupal-dev/modules/field/theme/field-rtl.css b/drupal-dev/modules/field/theme/field-rtl.css
new file mode 100644
index 0000000..5d35a86
--- /dev/null
+++ b/drupal-dev/modules/field/theme/field-rtl.css
@@ -0,0 +1,14 @@
+
+form .field-multiple-table th.field-label {
+ padding-right: 0;
+}
+form .field-multiple-table td.field-multiple-drag {
+ padding-left: 0;
+}
+form .field-multiple-table td.field-multiple-drag a.tabledrag-handle{
+ padding-left: .5em;
+}
+.field-label-inline .field-label,
+.field-label-inline .field-items {
+ float: right;
+}
diff --git a/drupal-dev/modules/field/theme/field.css b/drupal-dev/modules/field/theme/field.css
new file mode 100644
index 0000000..9eba32f
--- /dev/null
+++ b/drupal-dev/modules/field/theme/field.css
@@ -0,0 +1,28 @@
+
+/* Field display */
+.field .field-label {
+ font-weight: bold;
+}
+.field-label-inline .field-label,
+.field-label-inline .field-items {
+ float:left; /*LTR*/
+}
+
+/* Form display */
+form .field-multiple-table {
+ margin: 0;
+}
+form .field-multiple-table th.field-label {
+ padding-left: 0; /*LTR*/
+}
+form .field-multiple-table td.field-multiple-drag {
+ width: 30px;
+ padding-right: 0; /*LTR*/
+}
+form .field-multiple-table td.field-multiple-drag a.tabledrag-handle {
+ padding-right: .5em; /*LTR*/
+}
+
+form .field-add-more-submit {
+ margin: .5em 0 0;
+}
diff --git a/drupal-dev/modules/field/theme/field.tpl.php b/drupal-dev/modules/field/theme/field.tpl.php
new file mode 100644
index 0000000..f0f9d58
--- /dev/null
+++ b/drupal-dev/modules/field/theme/field.tpl.php
@@ -0,0 +1,62 @@
+
+
+
>
+
+
>:
+
+
>
+ $item): ?>
+
>
+
+
+
diff --git a/drupal-dev/modules/field_ui/field_ui-rtl.css b/drupal-dev/modules/field_ui/field_ui-rtl.css
new file mode 100644
index 0000000..1066baa
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui-rtl.css
@@ -0,0 +1,9 @@
+/**
+ * @file
+ * Right-to-left specific stylesheet for the Field UI module.
+ */
+
+/* 'Manage fields' overview */
+table.field-ui-overview tr.add-new .label-input {
+ float: right;
+}
diff --git a/drupal-dev/modules/field_ui/field_ui.admin.inc b/drupal-dev/modules/field_ui/field_ui.admin.inc
new file mode 100644
index 0000000..5c6f529
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.admin.inc
@@ -0,0 +1,2112 @@
+ $type_bundles) {
+ foreach ($type_bundles as $bundle => $bundle_instances) {
+ foreach ($bundle_instances as $field_name => $instance) {
+ $field = field_info_field($field_name);
+
+ // Initialize the row if we encounter the field for the first time.
+ if (!isset($rows[$field_name])) {
+ $rows[$field_name]['class'] = $field['locked'] ? array('menu-disabled') : array('');
+ $rows[$field_name]['data'][0] = $field['locked'] ? t('@field_name (Locked)', array('@field_name' => $field_name)) : $field_name;
+ $module_name = $field_types[$field['type']]['module'];
+ $rows[$field_name]['data'][1] = $field_types[$field['type']]['label'] . ' ' . t('(module: !module)', array('!module' => $modules[$module_name]->info['name']));
+ }
+
+ // Add the current instance.
+ $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
+ $rows[$field_name]['data'][2][] = $admin_path ? l($bundles[$entity_type][$bundle]['label'], $admin_path . '/fields') : $bundles[$entity_type][$bundle]['label'];
+ }
+ }
+ }
+ foreach ($rows as $field_name => $cell) {
+ $rows[$field_name]['data'][2] = implode(', ', $cell['data'][2]);
+ }
+ if (empty($rows)) {
+ $output = t('No fields have been defined yet.');
+ }
+ else {
+ // Sort rows by field name.
+ ksort($rows);
+ $output = theme('table', array('header' => $header, 'rows' => $rows));
+ }
+ return $output;
+}
+
+/**
+ * Displays a message listing the inactive fields of a given bundle.
+ */
+function field_ui_inactive_message($entity_type, $bundle) {
+ $inactive_instances = field_ui_inactive_instances($entity_type, $bundle);
+ if (!empty($inactive_instances)) {
+ $field_types = field_info_field_types();
+ $widget_types = field_info_widget_types();
+
+ foreach ($inactive_instances as $field_name => $instance) {
+ $list[] = t('%field (@field_name) field requires the %widget_type widget provided by %widget_module module', array(
+ '%field' => $instance['label'],
+ '@field_name' => $instance['field_name'],
+ '%widget_type' => isset($widget_types[$instance['widget']['type']]) ? $widget_types[$instance['widget']['type']]['label'] : $instance['widget']['type'],
+ '%widget_module' => $instance['widget']['module'],
+ ));
+ }
+ drupal_set_message(t('Inactive fields are not shown unless their providing modules are enabled. The following fields are not enabled: !list', array('!list' => theme('item_list', array('items' => $list)))), 'error');
+ }
+}
+
+/**
+ * Determines the rendering order of an array representing a tree.
+ *
+ * Callback for array_reduce() within field_ui_table_pre_render().
+ */
+function _field_ui_reduce_order($array, $a) {
+ $array = !isset($array) ? array() : $array;
+ if ($a['name']) {
+ $array[] = $a['name'];
+ }
+ if (!empty($a['children'])) {
+ uasort($a['children'], 'drupal_sort_weight');
+ $array = array_merge($array, array_reduce($a['children'], '_field_ui_reduce_order'));
+ }
+ return $array;
+}
+
+/**
+ * Returns the region to which a row in the 'Manage fields' screen belongs.
+ *
+ * This function is used as a #region_callback in
+ * field_ui_field_overview_form(). It is called during
+ * field_ui_table_pre_render().
+ */
+function field_ui_field_overview_row_region($row) {
+ switch ($row['#row_type']) {
+ case 'field':
+ case 'extra_field':
+ return 'main';
+ case 'add_new_field':
+ // If no input in 'label', assume the row has not been dragged out of the
+ // 'add new' section.
+ return (!empty($row['label']['#value']) ? 'main' : 'add_new');
+ }
+}
+
+/**
+ * Returns the region to which a row in the 'Manage display' screen belongs.
+ *
+ * This function is used as a #region_callback in
+ * field_ui_field_overview_form(), and is called during
+ * field_ui_table_pre_render().
+ */
+function field_ui_display_overview_row_region($row) {
+ switch ($row['#row_type']) {
+ case 'field':
+ case 'extra_field':
+ return ($row['format']['type']['#value'] == 'hidden' ? 'hidden' : 'visible');
+ }
+}
+
+/**
+ * Pre-render callback for field_ui_table elements.
+ */
+function field_ui_table_pre_render($elements) {
+ $js_settings = array();
+
+ // For each region, build the tree structure from the weight and parenting
+ // data contained in the flat form structure, to determine row order and
+ // indentation.
+ $regions = $elements['#regions'];
+ $tree = array('' => array('name' => '', 'children' => array()));
+ $trees = array_fill_keys(array_keys($regions), $tree);
+
+ $parents = array();
+ $list = drupal_map_assoc(element_children($elements));
+
+ // Iterate on rows until we can build a known tree path for all of them.
+ while ($list) {
+ foreach ($list as $name) {
+ $row = &$elements[$name];
+ $parent = $row['parent_wrapper']['parent']['#value'];
+ // Proceed if parent is known.
+ if (empty($parent) || isset($parents[$parent])) {
+ // Grab parent, and remove the row from the next iteration.
+ $parents[$name] = $parent ? array_merge($parents[$parent], array($parent)) : array();
+ unset($list[$name]);
+
+ // Determine the region for the row.
+ $function = $row['#region_callback'];
+ $region_name = $function($row);
+
+ // Add the element in the tree.
+ $target = &$trees[$region_name][''];
+ foreach ($parents[$name] as $key) {
+ $target = &$target['children'][$key];
+ }
+ $target['children'][$name] = array('name' => $name, 'weight' => $row['weight']['#value']);
+
+ // Add tabledrag indentation to the first row cell.
+ if ($depth = count($parents[$name])) {
+ $children = element_children($row);
+ $cell = current($children);
+ $row[$cell]['#prefix'] = theme('indentation', array('size' => $depth)) . (isset($row[$cell]['#prefix']) ? $row[$cell]['#prefix'] : '');
+ }
+
+ // Add row id and associate JS settings.
+ $id = drupal_html_class($name);
+ $row['#attributes']['id'] = $id;
+ if (isset($row['#js_settings'])) {
+ $row['#js_settings'] += array(
+ 'rowHandler' => $row['#row_type'],
+ 'name' => $name,
+ 'region' => $region_name,
+ );
+ $js_settings[$id] = $row['#js_settings'];
+ }
+ }
+ }
+ }
+ // Determine rendering order from the tree structure.
+ foreach ($regions as $region_name => $region) {
+ $elements['#regions'][$region_name]['rows_order'] = array_reduce($trees[$region_name], '_field_ui_reduce_order');
+ }
+
+ $elements['#attached']['js'][] = array(
+ 'type' => 'setting',
+ 'data' => array('fieldUIRowsData' => $js_settings),
+ );
+
+ return $elements;
+}
+
+/**
+ * Returns HTML for Field UI overview tables.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - elements: An associative array containing a Form API structure to be
+ * rendered as a table.
+ *
+ * @ingroup themeable
+ */
+function theme_field_ui_table($variables) {
+ $elements = $variables['elements'];
+ $table = array();
+ $js_settings = array();
+
+ // Add table headers and attributes.
+ foreach (array('header', 'attributes') as $key) {
+ if (isset($elements["#$key"])) {
+ $table[$key] = $elements["#$key"];
+ }
+ }
+
+ // Determine the colspan to use for region rows, by checking the number of
+ // columns in the headers.
+ $columns_count = 0;
+ foreach ($table['header'] as $header) {
+ $columns_count += (is_array($header) && isset($header['colspan']) ? $header['colspan'] : 1);
+ }
+
+ // Render rows, region by region.
+ foreach ($elements['#regions'] as $region_name => $region) {
+ $region_name_class = drupal_html_class($region_name);
+
+ // Add region rows.
+ if (isset($region['title'])) {
+ $table['rows'][] = array(
+ 'class' => array('region-title', 'region-' . $region_name_class . '-title'),
+ 'no_striping' => TRUE,
+ 'data' => array(
+ array('data' => $region['title'], 'colspan' => $columns_count),
+ ),
+ );
+ }
+ if (isset($region['message'])) {
+ $class = (empty($region['rows_order']) ? 'region-empty' : 'region-populated');
+ $table['rows'][] = array(
+ 'class' => array('region-message', 'region-' . $region_name_class . '-message', $class),
+ 'no_striping' => TRUE,
+ 'data' => array(
+ array('data' => $region['message'], 'colspan' => $columns_count),
+ ),
+ );
+ }
+
+ // Add form rows, in the order determined at pre-render time.
+ foreach ($region['rows_order'] as $name) {
+ $element = $elements[$name];
+
+ $row = array('data' => array());
+ if (isset($element['#attributes'])) {
+ $row += $element['#attributes'];
+ }
+
+ // Render children as table cells.
+ foreach (element_children($element) as $cell_key) {
+ $child = &$element[$cell_key];
+ // Do not render a cell for children of #type 'value'.
+ if (!(isset($child['#type']) && $child['#type'] == 'value')) {
+ $cell = array('data' => drupal_render($child));
+ if (isset($child['#cell_attributes'])) {
+ $cell += $child['#cell_attributes'];
+ }
+ $row['data'][] = $cell;
+ }
+ }
+ $table['rows'][] = $row;
+ }
+ }
+
+ return theme('table', $table);
+}
+
+/**
+ * Form constructor for the 'Manage fields' form of a bundle.
+ *
+ * Allows fields and pseudo-fields to be re-ordered.
+ *
+ * @see field_ui_field_overview_form_validate()
+ * @see field_ui_field_overview_form_submit()
+ * @ingroup forms
+ */
+function field_ui_field_overview_form($form, &$form_state, $entity_type, $bundle) {
+ $bundle = field_extract_bundle($entity_type, $bundle);
+
+ field_ui_inactive_message($entity_type, $bundle);
+ $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
+
+ // When displaying the form, make sure the list of fields is up-to-date.
+ if (empty($form_state['post'])) {
+ field_info_cache_clear();
+ }
+
+ // Gather bundle information.
+ $instances = field_info_instances($entity_type, $bundle);
+ $field_types = field_info_field_types();
+ $widget_types = field_info_widget_types();
+
+ $extra_fields = field_info_extra_fields($entity_type, $bundle, 'form');
+
+ $form += array(
+ '#entity_type' => $entity_type,
+ '#bundle' => $bundle,
+ '#fields' => array_keys($instances),
+ '#extra' => array_keys($extra_fields),
+ );
+
+ $table = array(
+ '#type' => 'field_ui_table',
+ '#tree' => TRUE,
+ '#header' => array(
+ t('Label'),
+ t('Weight'),
+ t('Parent'),
+ t('Machine name'),
+ t('Field type'),
+ t('Widget'),
+ array('data' => t('Operations'), 'colspan' => 2),
+ ),
+ '#parent_options' => array(),
+ '#regions' => array(
+ 'main' => array('message' => t('No fields are present yet.')),
+ 'add_new' => array('title' => ' '),
+ ),
+ '#attributes' => array(
+ 'class' => array('field-ui-overview'),
+ 'id' => 'field-overview',
+ ),
+ );
+
+ // Fields.
+ foreach ($instances as $name => $instance) {
+ $field = field_info_field($instance['field_name']);
+ $admin_field_path = $admin_path . '/fields/' . $instance['field_name'];
+ $table[$name] = array(
+ '#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
+ '#row_type' => 'field',
+ '#region_callback' => 'field_ui_field_overview_row_region',
+ 'label' => array(
+ '#markup' => check_plain($instance['label']),
+ ),
+ 'weight' => array(
+ '#type' => 'textfield',
+ '#title' => t('Weight for @title', array('@title' => $instance['label'])),
+ '#title_display' => 'invisible',
+ '#default_value' => $instance['widget']['weight'],
+ '#size' => 3,
+ '#attributes' => array('class' => array('field-weight')),
+ ),
+ 'parent_wrapper' => array(
+ 'parent' => array(
+ '#type' => 'select',
+ '#title' => t('Parent for @title', array('@title' => $instance['label'])),
+ '#title_display' => 'invisible',
+ '#options' => $table['#parent_options'],
+ '#empty_value' => '',
+ '#attributes' => array('class' => array('field-parent')),
+ '#parents' => array('fields', $name, 'parent'),
+ ),
+ 'hidden_name' => array(
+ '#type' => 'hidden',
+ '#default_value' => $name,
+ '#attributes' => array('class' => array('field-name')),
+ ),
+ ),
+ 'field_name' => array(
+ '#markup' => $instance['field_name'],
+ ),
+ 'type' => array(
+ '#type' => 'link',
+ '#title' => t($field_types[$field['type']]['label']),
+ '#href' => $admin_field_path . '/field-settings',
+ '#options' => array('attributes' => array('title' => t('Edit field settings.'))),
+ ),
+ 'widget_type' => array(
+ '#type' => 'link',
+ '#title' => t($widget_types[$instance['widget']['type']]['label']),
+ '#href' => $admin_field_path . '/widget-type',
+ '#options' => array('attributes' => array('title' => t('Change widget type.'))),
+ ),
+ 'edit' => array(
+ '#type' => 'link',
+ '#title' => t('edit'),
+ '#href' => $admin_field_path,
+ '#options' => array('attributes' => array('title' => t('Edit instance settings.'))),
+ ),
+ 'delete' => array(
+ '#type' => 'link',
+ '#title' => t('delete'),
+ '#href' => $admin_field_path . '/delete',
+ '#options' => array('attributes' => array('title' => t('Delete instance.'))),
+ ),
+ );
+
+ if (!empty($instance['locked'])) {
+ $table[$name]['edit'] = array('#value' => t('Locked'));
+ $table[$name]['delete'] = array();
+ $table[$name]['#attributes']['class'][] = 'menu-disabled';
+ }
+ }
+
+ // Non-field elements.
+ foreach ($extra_fields as $name => $extra_field) {
+ $table[$name] = array(
+ '#attributes' => array('class' => array('draggable', 'tabledrag-leaf')),
+ '#row_type' => 'extra_field',
+ '#region_callback' => 'field_ui_field_overview_row_region',
+ 'label' => array(
+ '#markup' => check_plain($extra_field['label']),
+ ),
+ 'weight' => array(
+ '#type' => 'textfield',
+ '#default_value' => $extra_field['weight'],
+ '#size' => 3,
+ '#attributes' => array('class' => array('field-weight')),
+ '#title_display' => 'invisible',
+ '#title' => t('Weight for @title', array('@title' => $extra_field['label'])),
+ ),
+ 'parent_wrapper' => array(
+ 'parent' => array(
+ '#type' => 'select',
+ '#title' => t('Parent for @title', array('@title' => $extra_field['label'])),
+ '#title_display' => 'invisible',
+ '#options' => $table['#parent_options'],
+ '#empty_value' => '',
+ '#attributes' => array('class' => array('field-parent')),
+ '#parents' => array('fields', $name, 'parent'),
+ ),
+ 'hidden_name' => array(
+ '#type' => 'hidden',
+ '#default_value' => $name,
+ '#attributes' => array('class' => array('field-name')),
+ ),
+ ),
+ 'field_name' => array(
+ '#markup' => $name,
+ ),
+ 'type' => array(
+ '#markup' => isset($extra_field['description']) ? $extra_field['description'] : '',
+ '#cell_attributes' => array('colspan' => 2),
+ ),
+ 'edit' => array(
+ '#markup' => isset($extra_field['edit']) ? $extra_field['edit'] : '',
+ ),
+ 'delete' => array(
+ '#markup' => isset($extra_field['delete']) ? $extra_field['delete'] : '',
+ ),
+ );
+ }
+
+ // Additional row: add new field.
+ $max_weight = field_info_max_weight($entity_type, $bundle, 'form');
+ $field_type_options = field_ui_field_type_options();
+ $widget_type_options = field_ui_widget_type_options(NULL, TRUE);
+ if ($field_type_options && $widget_type_options) {
+ $name = '_add_new_field';
+ $table[$name] = array(
+ '#attributes' => array('class' => array('draggable', 'tabledrag-leaf', 'add-new')),
+ '#row_type' => 'add_new_field',
+ '#region_callback' => 'field_ui_field_overview_row_region',
+ 'label' => array(
+ '#type' => 'textfield',
+ '#title' => t('New field label'),
+ '#title_display' => 'invisible',
+ '#size' => 15,
+ '#description' => t('Label'),
+ '#prefix' => '
';
+ }
+ }
+
+ // Return the whole table.
+ return $form['fields'];
+}
+
+/**
+ * Form submission handler for field_ui_display_overview_form().
+ */
+function field_ui_display_overview_form_submit($form, &$form_state) {
+ $form_values = $form_state['values'];
+ $entity_type = $form['#entity_type'];
+ $bundle = $form['#bundle'];
+ $view_mode = $form['#view_mode'];
+
+ // Save data for 'regular' fields.
+ foreach ($form['#fields'] as $field_name) {
+ // Retrieve the stored instance settings to merge with the incoming values.
+ $instance = field_read_instance($entity_type, $field_name, $bundle);
+ $values = $form_values['fields'][$field_name];
+ // Get formatter settings. They lie either directly in submitted form
+ // values (if the whole form was submitted while some formatter
+ // settings were being edited), or have been persisted in
+ // $form_state.
+ $settings = array();
+ if (isset($values['settings_edit_form']['settings'])) {
+ $settings = $values['settings_edit_form']['settings'];
+ }
+ elseif (isset($form_state['formatter_settings'][$field_name])) {
+ $settings = $form_state['formatter_settings'][$field_name];
+ }
+ elseif (isset($instance['display'][$view_mode]['settings'])) {
+ $settings = $instance['display'][$view_mode]['settings'];
+ }
+
+ // Only save settings actually used by the selected formatter.
+ $default_settings = field_info_formatter_settings($values['type']);
+ $settings = array_intersect_key($settings, $default_settings);
+
+ $instance['display'][$view_mode] = array(
+ 'label' => $values['label'],
+ 'type' => $values['type'],
+ 'weight' => $values['weight'],
+ 'settings' => $settings,
+ );
+ field_update_instance($instance);
+ }
+
+ // Get current bundle settings.
+ $bundle_settings = field_bundle_settings($entity_type, $bundle);
+
+ // Save data for 'extra' fields.
+ foreach ($form['#extra'] as $name) {
+ $bundle_settings['extra_fields']['display'][$name][$view_mode] = array(
+ 'weight' => $form_values['fields'][$name]['weight'],
+ 'visible' => $form_values['fields'][$name]['type'] == 'visible',
+ );
+ }
+
+ // Save view modes data.
+ if ($view_mode == 'default') {
+ $entity_info = entity_get_info($entity_type);
+ foreach ($form_values['view_modes_custom'] as $view_mode_name => $value) {
+ // Display a message for each view mode newly configured to use custom
+ // settings.
+ $view_mode_settings = field_view_mode_settings($entity_type, $bundle);
+ if (!empty($value) && empty($view_mode_settings[$view_mode_name]['custom_settings'])) {
+ $view_mode_label = $entity_info['view modes'][$view_mode_name]['label'];
+ $path = _field_ui_bundle_admin_path($entity_type, $bundle) . "/display/$view_mode_name";
+ drupal_set_message(t('The %view_mode mode now uses custom display settings. You might want to configure them.', array('%view_mode' => $view_mode_label, '@url' => url($path))));
+ // Initialize the newly customized view mode with the display settings
+ // from the default view mode.
+ _field_ui_add_default_view_mode_settings($entity_type, $bundle, $view_mode_name, $bundle_settings);
+ }
+ $bundle_settings['view_modes'][$view_mode_name]['custom_settings'] = !empty($value);
+ }
+ }
+
+ // Save updated bundle settings.
+ field_bundle_settings($entity_type, $bundle, $bundle_settings);
+
+ drupal_set_message(t('Your settings have been saved.'));
+}
+
+/**
+ * Populates display settings for a new view mode from the default view mode.
+ *
+ * When an administrator decides to use custom display settings for a view mode,
+ * that view mode needs to be initialized with the display settings for the
+ * 'default' view mode, which it was previously using. This helper function
+ * adds the new custom display settings to this bundle's instances, and saves
+ * them. It also modifies the passed-in $settings array, which the caller can
+ * then save using field_bundle_settings().
+ *
+ * @param $entity_type
+ * The bundle's entity type.
+ * @param $bundle
+ * The bundle whose view mode is being customized.
+ * @param $view_mode
+ * The view mode that the administrator has set to use custom settings.
+ * @param $settings
+ * An associative array of bundle settings, as expected by
+ * field_bundle_settings().
+ *
+ * @see field_ui_display_overview_form_submit().
+ * @see field_bundle_settings()
+ */
+function _field_ui_add_default_view_mode_settings($entity_type, $bundle, $view_mode, &$settings) {
+ // Update display settings for field instances.
+ $instances = field_read_instances(array('entity_type' => $entity_type, 'bundle' => $bundle));
+ foreach ($instances as $instance) {
+ // If this field instance has display settings defined for this view mode,
+ // respect those settings.
+ if (!isset($instance['display'][$view_mode])) {
+ // The instance doesn't specify anything for this view mode, so use the
+ // default display settings.
+ $instance['display'][$view_mode] = $instance['display']['default'];
+ field_update_instance($instance);
+ }
+ }
+
+ // Update display settings for 'extra fields'.
+ foreach (array_keys($settings['extra_fields']['display']) as $name) {
+ if (!isset($settings['extra_fields']['display'][$name][$view_mode])) {
+ $settings['extra_fields']['display'][$name][$view_mode] = $settings['extra_fields']['display'][$name]['default'];
+ }
+ }
+}
+
+/**
+ * Returns an array of field_type options.
+ */
+function field_ui_field_type_options() {
+ $options = &drupal_static(__FUNCTION__);
+
+ if (!isset($options)) {
+ $options = array();
+ $field_types = field_info_field_types();
+ $field_type_options = array();
+ foreach ($field_types as $name => $field_type) {
+ // Skip field types which have no widget types, or should not be add via
+ // uesr interface.
+ if (field_ui_widget_type_options($name) && empty($field_type['no_ui'])) {
+ $options[$name] = $field_type['label'];
+ }
+ }
+ asort($options);
+ }
+ return $options;
+}
+
+/**
+ * Returns an array of widget type options for a field type.
+ *
+ * If no field type is provided, returns a nested array of all widget types,
+ * keyed by field type human name.
+ */
+function field_ui_widget_type_options($field_type = NULL, $by_label = FALSE) {
+ $options = &drupal_static(__FUNCTION__);
+
+ if (!isset($options)) {
+ $options = array();
+ $field_types = field_info_field_types();
+ foreach (field_info_widget_types() as $name => $widget_type) {
+ foreach ($widget_type['field types'] as $widget_field_type) {
+ // Check that the field type exists.
+ if (isset($field_types[$widget_field_type])) {
+ $options[$widget_field_type][$name] = $widget_type['label'];
+ }
+ }
+ }
+ }
+
+ if (isset($field_type)) {
+ return !empty($options[$field_type]) ? $options[$field_type] : array();
+ }
+ if ($by_label) {
+ $field_types = field_info_field_types();
+ $options_by_label = array();
+ foreach ($options as $field_type => $widgets) {
+ $options_by_label[$field_types[$field_type]['label']] = $widgets;
+ }
+ return $options_by_label;
+ }
+ return $options;
+}
+
+/**
+ * Returns an array of formatter options for a field type.
+ *
+ * If no field type is provided, returns a nested array of all formatters, keyed
+ * by field type.
+ */
+function field_ui_formatter_options($field_type = NULL) {
+ $options = &drupal_static(__FUNCTION__);
+
+ if (!isset($options)) {
+ $field_types = field_info_field_types();
+ $options = array();
+ foreach (field_info_formatter_types() as $name => $formatter) {
+ foreach ($formatter['field types'] as $formatter_field_type) {
+ // Check that the field type exists.
+ if (isset($field_types[$formatter_field_type])) {
+ $options[$formatter_field_type][$name] = $formatter['label'];
+ }
+ }
+ }
+ }
+
+ if ($field_type) {
+ return !empty($options[$field_type]) ? $options[$field_type] : array();
+ }
+ return $options;
+}
+
+/**
+ * Returns an array of existing fields to be added to a bundle.
+ */
+function field_ui_existing_field_options($entity_type, $bundle) {
+ $info = array();
+ $field_types = field_info_field_types();
+
+ foreach (field_info_instances() as $existing_entity_type => $bundles) {
+ foreach ($bundles as $existing_bundle => $instances) {
+ // No need to look in the current bundle.
+ if (!($existing_bundle == $bundle && $existing_entity_type == $entity_type)) {
+ foreach ($instances as $instance) {
+ $field = field_info_field($instance['field_name']);
+ // Don't show
+ // - locked fields,
+ // - fields already in the current bundle,
+ // - fields that cannot be added to the entity type,
+ // - fields that should not be added via user interface.
+
+ if (empty($field['locked'])
+ && !field_info_instance($entity_type, $field['field_name'], $bundle)
+ && (empty($field['entity_types']) || in_array($entity_type, $field['entity_types']))
+ && empty($field_types[$field['type']]['no_ui'])) {
+ $info[$instance['field_name']] = array(
+ 'type' => $field['type'],
+ 'type_label' => $field_types[$field['type']]['label'],
+ 'field' => $field['field_name'],
+ 'label' => $instance['label'],
+ 'widget_type' => $instance['widget']['type'],
+ );
+ }
+ }
+ }
+ }
+ }
+ return $info;
+}
+
+/**
+ * Form constructor for the field settings edit page.
+ *
+ * @see field_ui_field_settings_form_submit()
+ * @ingroup forms
+ */
+function field_ui_field_settings_form($form, &$form_state, $instance) {
+ $bundle = $instance['bundle'];
+ $entity_type = $instance['entity_type'];
+ $field = field_info_field($instance['field_name']);
+
+ drupal_set_title($instance['label']);
+
+ $description = '
' . t('These settings apply to the %field field everywhere it is used. These settings impact the way that data is stored in the database and cannot be changed once data has been created.', array('%field' => $instance['label'])) . '
';
+
+ // Create a form structure for the field values.
+ $form['field'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Field settings'),
+ '#description' => $description,
+ '#tree' => TRUE,
+ );
+
+ // See if data already exists for this field.
+ // If so, prevent changes to the field settings.
+ $has_data = field_has_data($field);
+ if ($has_data) {
+ $form['field']['#description'] = '
' . t('There is data for this field in the database. The field settings can no longer be changed.') . '
' . $form['field']['#description'];
+ }
+
+ // Build the non-configurable field values.
+ $form['field']['field_name'] = array('#type' => 'value', '#value' => $field['field_name']);
+ $form['field']['type'] = array('#type' => 'value', '#value' => $field['type']);
+ $form['field']['module'] = array('#type' => 'value', '#value' => $field['module']);
+ $form['field']['active'] = array('#type' => 'value', '#value' => $field['active']);
+
+ // Add settings provided by the field module. The field module is
+ // responsible for not returning settings that cannot be changed if
+ // the field already has data.
+ $form['field']['settings'] = array();
+ $additions = module_invoke($field['module'], 'field_settings_form', $field, $instance, $has_data);
+ if (is_array($additions)) {
+ $form['field']['settings'] = $additions;
+ }
+ if (empty($form['field']['settings'])) {
+ $form['field']['settings'] = array(
+ '#markup' => t('%field has no field settings.', array('%field' => $instance['label'])),
+ );
+ }
+ $form['#entity_type'] = $entity_type;
+ $form['#bundle'] = $bundle;
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save field settings'));
+ return $form;
+}
+
+/**
+ * Form submission handler for field_ui_field_settings_form().
+ */
+function field_ui_field_settings_form_submit($form, &$form_state) {
+ $form_values = $form_state['values'];
+ $field_values = $form_values['field'];
+
+ // Merge incoming form values into the existing field.
+ $field = field_info_field($field_values['field_name']);
+
+ $entity_type = $form['#entity_type'];
+ $bundle = $form['#bundle'];
+ $instance = field_info_instance($entity_type, $field['field_name'], $bundle);
+
+ // Update the field.
+ $field = array_merge($field, $field_values);
+
+ try {
+ field_update_field($field);
+ drupal_set_message(t('Updated field %label field settings.', array('%label' => $instance['label'])));
+ $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle);
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error');
+ }
+}
+
+/**
+ * Form constructor for the widget selection form.
+ *
+ * Path: BUNDLE_ADMIN_PATH/fields/%field/widget-type, where BUNDLE_ADMIN_PATH is
+ * the path stored in the ['admin']['info'] property in the return value of
+ * hook_entity_info().
+ *
+ * @see field_ui_menu()
+ * @see field_ui_widget_type_form_submit()
+ * @ingroup forms
+ */
+function field_ui_widget_type_form($form, &$form_state, $instance) {
+ drupal_set_title($instance['label']);
+
+ $bundle = $instance['bundle'];
+ $entity_type = $instance['entity_type'];
+ $field_name = $instance['field_name'];
+
+ $field = field_info_field($field_name);
+ $field_type = field_info_field_types($field['type']);
+ $widget_type = field_info_widget_types($instance['widget']['type']);
+ $bundles = field_info_bundles();
+ $bundle_label = $bundles[$entity_type][$bundle]['label'];
+
+ $form = array(
+ '#bundle' => $bundle,
+ '#entity_type' => $entity_type,
+ '#field_name' => $field_name,
+ );
+
+ $form['basic'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Change widget'),
+ );
+ $form['basic']['widget_type'] = array(
+ '#type' => 'select',
+ '#title' => t('Widget type'),
+ '#required' => TRUE,
+ '#options' => field_ui_widget_type_options($field['type']),
+ '#default_value' => $instance['widget']['type'],
+ '#description' => t('The type of form element you would like to present to the user when creating this field in the %type type.', array('%type' => $bundle_label)),
+ );
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Continue'));
+
+ $form['#validate'] = array();
+ $form['#submit'] = array('field_ui_widget_type_form_submit');
+
+ return $form;
+}
+
+/**
+ * Form submission handler for field_ui_widget_type_form().
+ */
+function field_ui_widget_type_form_submit($form, &$form_state) {
+ $form_values = $form_state['values'];
+ $bundle = $form['#bundle'];
+ $entity_type = $form['#entity_type'];
+ $field_name = $form['#field_name'];
+
+ // Retrieve the stored instance settings to merge with the incoming values.
+ $instance = field_read_instance($entity_type, $field_name, $bundle);
+
+ // Set the right module information.
+ $widget_type = field_info_widget_types($form_values['widget_type']);
+ $widget_module = $widget_type['module'];
+
+ $instance['widget']['type'] = $form_values['widget_type'];
+ $instance['widget']['module'] = $widget_module;
+
+ try {
+ field_update_instance($instance);
+ drupal_set_message(t('Changed the widget for field %label.', array('%label' => $instance['label'])));
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('There was a problem changing the widget for field %label.', array('%label' => $instance['label'])), 'error');
+ }
+
+ $form_state['redirect'] = field_ui_next_destination($entity_type, $bundle);
+}
+
+/**
+ * Form constructor for removing a field instance from a bundle.
+ *
+ * @see field_ui_field_delete_form_submit()
+ * @ingroup forms
+ */
+function field_ui_field_delete_form($form, &$form_state, $instance) {
+ $bundle = $instance['bundle'];
+ $entity_type = $instance['entity_type'];
+ $field = field_info_field($instance['field_name']);
+
+ $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
+
+ $form['entity_type'] = array('#type' => 'value', '#value' => $entity_type);
+ $form['bundle'] = array('#type' => 'value', '#value' => $bundle);
+ $form['field_name'] = array('#type' => 'value', '#value' => $field['field_name']);
+
+ $output = confirm_form($form,
+ t('Are you sure you want to delete the field %field?', array('%field' => $instance['label'])),
+ $admin_path . '/fields',
+ t('If you have any content left in this field, it will be lost. This action cannot be undone.'),
+ t('Delete'), t('Cancel'),
+ 'confirm'
+ );
+
+ if ($field['locked']) {
+ unset($output['actions']['submit']);
+ $output['description']['#markup'] = t('This field is locked and cannot be deleted.');
+ }
+
+ return $output;
+}
+
+/**
+ * Form submission handler for field_ui_field_delete_form().
+ *
+ * Removes a field instance from a bundle. If the field has no more instances,
+ * it will be marked as deleted too.
+ */
+function field_ui_field_delete_form_submit($form, &$form_state) {
+ $form_values = $form_state['values'];
+ $field_name = $form_values['field_name'];
+ $bundle = $form_values['bundle'];
+ $entity_type = $form_values['entity_type'];
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+ $bundles = field_info_bundles();
+ $bundle_label = $bundles[$entity_type][$bundle]['label'];
+
+ if (!empty($bundle) && $field && !$field['locked'] && $form_values['confirm']) {
+ field_delete_instance($instance);
+ drupal_set_message(t('The field %field has been deleted from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label)));
+ }
+ else {
+ drupal_set_message(t('There was a problem removing the %field from the %type content type.', array('%field' => $instance['label'], '%type' => $bundle_label)), 'error');
+ }
+
+ $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
+ $form_state['redirect'] = field_ui_get_destinations(array($admin_path . '/fields'));
+
+ // Fields are purged on cron. However field module prevents disabling modules
+ // when field types they provided are used in a field until it is fully
+ // purged. In the case that a field has minimal or no content, a single call
+ // to field_purge_batch() will remove it from the system. Call this with a
+ // low batch limit to avoid administrators having to wait for cron runs when
+ // removing instances that meet this criteria.
+ field_purge_batch(10);
+}
+
+/**
+ * Form constructor for the field instance settings form.
+ *
+ * @see field_ui_field_edit_form_validate()
+ * @see field_ui_field_edit_form_submit()
+ * @ingroup forms
+ */
+function field_ui_field_edit_form($form, &$form_state, $instance) {
+ $bundle = $instance['bundle'];
+ $entity_type = $instance['entity_type'];
+ $field = field_info_field($instance['field_name']);
+
+ drupal_set_title($instance['label']);
+
+ $form['#field'] = $field;
+ $form['#instance'] = $instance;
+
+ if (!empty($field['locked'])) {
+ $form['locked'] = array(
+ '#markup' => t('The field %field is locked and cannot be edited.', array('%field' => $instance['label'])),
+ );
+ return $form;
+ }
+
+ $field_type = field_info_field_types($field['type']);
+ $widget_type = field_info_widget_types($instance['widget']['type']);
+ $bundles = field_info_bundles();
+
+ // Create a form structure for the instance values.
+ $form['instance'] = array(
+ '#tree' => TRUE,
+ '#type' => 'fieldset',
+ '#title' => t('%type settings', array('%type' => $bundles[$entity_type][$bundle]['label'])),
+ '#description' => t('These settings apply only to the %field field when used in the %type type.', array(
+ '%field' => $instance['label'],
+ '%type' => $bundles[$entity_type][$bundle]['label'],
+ )),
+ // Ensure field_ui_field_edit_instance_pre_render() gets called in addition
+ // to, not instead of, the #pre_render function(s) needed by all fieldsets.
+ '#pre_render' => array_merge(array('field_ui_field_edit_instance_pre_render'), element_info_property('fieldset', '#pre_render', array())),
+ );
+
+ // Build the non-configurable instance values.
+ $form['instance']['field_name'] = array(
+ '#type' => 'value',
+ '#value' => $instance['field_name'],
+ );
+ $form['instance']['entity_type'] = array(
+ '#type' => 'value',
+ '#value' => $entity_type,
+ );
+ $form['instance']['bundle'] = array(
+ '#type' => 'value',
+ '#value' => $bundle,
+ );
+ $form['instance']['widget']['weight'] = array(
+ '#type' => 'value',
+ '#value' => !empty($instance['widget']['weight']) ? $instance['widget']['weight'] : 0,
+ );
+
+ // Build the configurable instance values.
+ $form['instance']['label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Label'),
+ '#default_value' => !empty($instance['label']) ? $instance['label'] : $field['field_name'],
+ '#required' => TRUE,
+ '#weight' => -20,
+ );
+ $form['instance']['required'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Required field'),
+ '#default_value' => !empty($instance['required']),
+ '#weight' => -10,
+ );
+
+ $form['instance']['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Help text'),
+ '#default_value' => !empty($instance['description']) ? $instance['description'] : '',
+ '#rows' => 5,
+ '#description' => t('Instructions to present to the user below this field on the editing form. Allowed HTML tags: @tags', array('@tags' => _field_filter_xss_display_allowed_tags())),
+ '#weight' => -5,
+ );
+
+ // Build the widget component of the instance.
+ $form['instance']['widget']['type'] = array(
+ '#type' => 'value',
+ '#value' => $instance['widget']['type'],
+ );
+ $form['instance']['widget']['module'] = array(
+ '#type' => 'value',
+ '#value' => $widget_type['module'],
+ );
+ $form['instance']['widget']['active'] = array(
+ '#type' => 'value',
+ '#value' => !empty($field['instance']['widget']['active']) ? 1 : 0,
+ );
+
+ // Add additional field instance settings from the field module.
+ $additions = module_invoke($field['module'], 'field_instance_settings_form', $field, $instance);
+ if (is_array($additions)) {
+ $form['instance']['settings'] = $additions;
+ }
+
+ // Add additional widget settings from the widget module.
+ $additions = module_invoke($widget_type['module'], 'field_widget_settings_form', $field, $instance);
+ if (is_array($additions)) {
+ $form['instance']['widget']['settings'] = $additions;
+ $form['instance']['widget']['active']['#value'] = 1;
+ }
+
+ // Add handling for default value if not provided by any other module.
+ if (field_behaviors_widget('default value', $instance) == FIELD_BEHAVIOR_DEFAULT && empty($instance['default_value_function'])) {
+ $form['instance']['default_value_widget'] = field_ui_default_value_widget($field, $instance, $form, $form_state);
+ }
+
+ $has_data = field_has_data($field);
+ if ($has_data) {
+ $description = '
' . t('These settings apply to the %field field everywhere it is used. Because the field already has data, some settings can no longer be changed.', array('%field' => $instance['label'])) . '
';
+ }
+ else {
+ $description = '
' . t('These settings apply to the %field field everywhere it is used.', array('%field' => $instance['label'])) . '
';
+ }
+
+ // Create a form structure for the field values.
+ $form['field'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('%field field settings', array('%field' => $instance['label'])),
+ '#description' => $description,
+ '#tree' => TRUE,
+ );
+
+ // Build the configurable field values.
+ $description = t('Maximum number of values users can enter for this field.');
+ if (field_behaviors_widget('multiple values', $instance) == FIELD_BEHAVIOR_DEFAULT) {
+ $description .= ' ' . t("'Unlimited' will provide an 'Add more' button so the users can add as many values as they like.");
+ }
+ $form['field']['cardinality'] = array(
+ '#type' => 'select',
+ '#title' => t('Number of values'),
+ '#options' => array(FIELD_CARDINALITY_UNLIMITED => t('Unlimited')) + drupal_map_assoc(range(1, 10)),
+ '#default_value' => $field['cardinality'],
+ '#description' => $description,
+ );
+
+ // Add additional field type settings. The field type module is
+ // responsible for not returning settings that cannot be changed if
+ // the field already has data.
+ $additions = module_invoke($field['module'], 'field_settings_form', $field, $instance, $has_data);
+ if (is_array($additions)) {
+ $form['field']['settings'] = $additions;
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
+ return $form;
+}
+
+/**
+ * Pre-render function for field instance settings.
+ *
+ * Combines the instance, widget, and other settings into a single fieldset so
+ * that elements within each group can be shown at different weights as if they
+ * all had the same parent.
+ */
+function field_ui_field_edit_instance_pre_render($element) {
+ // Merge the widget settings into the main form.
+ if (isset($element['widget']['settings'])) {
+ foreach (element_children($element['widget']['settings']) as $key) {
+ $element['widget_' . $key] = $element['widget']['settings'][$key];
+ }
+ unset($element['widget']['settings']);
+ }
+
+ // Merge the instance settings into the main form.
+ if (isset($element['settings'])) {
+ foreach (element_children($element['settings']) as $key) {
+ $element['instance_' . $key] = $element['settings'][$key];
+ }
+ unset($element['settings']);
+ }
+
+ return $element;
+}
+
+/**
+ * Builds the default value fieldset for a given field instance.
+ */
+function field_ui_default_value_widget($field, $instance, &$form, &$form_state) {
+ $field_name = $field['field_name'];
+
+ $element = array(
+ '#type' => 'fieldset',
+ '#title' => t('Default value'),
+ '#collapsible' => FALSE,
+ '#tree' => TRUE,
+ '#description' => t('The default value for this field, used when creating new content.'),
+ // Stick to an empty 'parents' on this form in order not to breaks widgets
+ // that do not use field_widget_[field|instance]() and still access
+ // $form_state['field'] directly.
+ '#parents' => array(),
+ );
+
+ // Insert the widget.
+ $items = $instance['default_value'];
+ $instance['required'] = FALSE;
+ $instance['description'] = '';
+
+ // @todo Allow multiple values (requires more work on 'add more' JS handler).
+ $element += field_default_form($instance['entity_type'], NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state, 0);
+
+ return $element;
+}
+
+/**
+ * Form validation handler for field_ui_field_edit_form().
+ *
+ * @see field_ui_field_edit_form_submit().
+ */
+function field_ui_field_edit_form_validate($form, &$form_state) {
+ // Take the incoming values as the $instance definition, so that the 'default
+ // value' gets validated using the instance settings being submitted.
+ $instance = $form_state['values']['instance'];
+ $field_name = $instance['field_name'];
+
+ if (isset($form['instance']['default_value_widget'])) {
+ $element = $form['instance']['default_value_widget'];
+
+ $field_state = field_form_get_state($element['#parents'], $field_name, LANGUAGE_NONE, $form_state);
+ $field = $field_state['field'];
+
+ // Extract the 'default value'.
+ $items = array();
+ field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
+
+ // Validate the value and report errors.
+ $errors = array();
+ $function = $field['module'] . '_field_validate';
+ if (function_exists($function)) {
+ $function(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $errors);
+ }
+ if (isset($errors[$field_name][LANGUAGE_NONE])) {
+ // Store reported errors in $form_state.
+ $field_state['errors'] = $errors[$field_name][LANGUAGE_NONE];
+ field_form_set_state($element['#parents'], $field_name, LANGUAGE_NONE, $form_state, $field_state);
+ // Assign reported errors to the correct form element.
+ field_default_form_errors(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
+ }
+ }
+}
+
+/**
+ * Form submission handler for field_ui_field_edit_form().
+ *
+ * @see field_ui_field_edit_form_validate().
+ */
+function field_ui_field_edit_form_submit($form, &$form_state) {
+ $instance = $form_state['values']['instance'];
+ $field = $form_state['values']['field'];
+
+ // Update any field settings that have changed.
+ $field_source = field_info_field($instance['field_name']);
+ $field = array_merge($field_source, $field);
+ try {
+ field_update_field($field);
+ }
+ catch (Exception $e) {
+ drupal_set_message(t('Attempt to update field %label failed: %message.', array('%label' => $instance['label'], '%message' => $e->getMessage())), 'error');
+ return;
+ }
+
+ // Handle the default value.
+ if (isset($form['instance']['default_value_widget'])) {
+ $element = $form['instance']['default_value_widget'];
+
+ // Extract field values.
+ $items = array();
+ field_default_extract_form_values(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
+ field_default_submit(NULL, NULL, $field, $instance, LANGUAGE_NONE, $items, $element, $form_state);
+
+ $instance['default_value'] = $items ? $items : NULL;
+ }
+
+ // Retrieve the stored instance settings to merge with the incoming values.
+ $instance_source = field_read_instance($instance['entity_type'], $instance['field_name'], $instance['bundle']);
+ $instance = array_merge($instance_source, $instance);
+ field_update_instance($instance);
+
+ drupal_set_message(t('Saved %label configuration.', array('%label' => $instance['label'])));
+
+ $form_state['redirect'] = field_ui_next_destination($instance['entity_type'], $instance['bundle']);
+}
+
+/**
+ * Extracts next redirect path from an array of multiple destinations.
+ *
+ * @see field_ui_next_destination()
+ */
+function field_ui_get_destinations($destinations) {
+ $path = array_shift($destinations);
+ $options = drupal_parse_url($path);
+ if ($destinations) {
+ $options['query']['destinations'] = $destinations;
+ }
+ return array($options['path'], $options);
+}
+
+/**
+ * Returns the next redirect path in a multipage sequence.
+ */
+function field_ui_next_destination($entity_type, $bundle) {
+ $destinations = !empty($_REQUEST['destinations']) ? $_REQUEST['destinations'] : array();
+ if (!empty($destinations)) {
+ unset($_REQUEST['destinations']);
+ return field_ui_get_destinations($destinations);
+ }
+ $admin_path = _field_ui_bundle_admin_path($entity_type, $bundle);
+ return $admin_path . '/fields';
+}
diff --git a/drupal-dev/modules/field_ui/field_ui.api.php b/drupal-dev/modules/field_ui/field_ui.api.php
new file mode 100644
index 0000000..05d9f05
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.api.php
@@ -0,0 +1,204 @@
+ 'textfield',
+ '#title' => t('Maximum length'),
+ '#default_value' => $settings['max_length'],
+ '#required' => FALSE,
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#description' => t('The maximum length of the field in characters. Leave blank for an unlimited size.'),
+ );
+ return $form;
+}
+
+/**
+ * Add settings to an instance field settings form.
+ *
+ * Invoked from field_ui_field_edit_form() to allow the module defining the
+ * field to add settings for a field instance.
+ *
+ * @param $field
+ * The field structure being configured.
+ * @param $instance
+ * The instance structure being configured.
+ *
+ * @return
+ * The form definition for the field instance settings.
+ */
+function hook_field_instance_settings_form($field, $instance) {
+ $settings = $instance['settings'];
+
+ $form['text_processing'] = array(
+ '#type' => 'radios',
+ '#title' => t('Text processing'),
+ '#default_value' => $settings['text_processing'],
+ '#options' => array(
+ t('Plain text'),
+ t('Filtered text (user selects text format)'),
+ ),
+ );
+ if ($field['type'] == 'text_with_summary') {
+ $form['display_summary'] = array(
+ '#type' => 'select',
+ '#title' => t('Display summary'),
+ '#options' => array(
+ t('No'),
+ t('Yes'),
+ ),
+ '#description' => t('Display the summary to allow the user to input a summary value. Hide the summary to automatically fill it with a trimmed portion from the main post.'),
+ '#default_value' => !empty($settings['display_summary']) ? $settings['display_summary'] : 0,
+ );
+ }
+
+ return $form;
+}
+
+/**
+ * Add settings to a widget settings form.
+ *
+ * Invoked from field_ui_field_edit_form() to allow the module defining the
+ * widget to add settings for a widget instance.
+ *
+ * @param $field
+ * The field structure being configured.
+ * @param $instance
+ * The instance structure being configured.
+ *
+ * @return
+ * The form definition for the widget settings.
+ */
+function hook_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'];
+
+ if ($widget['type'] == 'text_textfield') {
+ $form['size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Size of textfield'),
+ '#default_value' => $settings['size'],
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#required' => TRUE,
+ );
+ }
+ else {
+ $form['rows'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Rows'),
+ '#default_value' => $settings['rows'],
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#required' => TRUE,
+ );
+ }
+
+ return $form;
+}
+
+
+/**
+ * Specify the form elements for a formatter's settings.
+ *
+ * @param $field
+ * The field structure being configured.
+ * @param $instance
+ * The instance structure being configured.
+ * @param $view_mode
+ * The view mode being configured.
+ * @param $form
+ * The (entire) configuration form array, which will usually have no use here.
+ * @param $form_state
+ * The form state of the (entire) configuration form.
+ *
+ * @return
+ * The form elements for the formatter settings.
+ */
+function hook_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $element = array();
+
+ if ($display['type'] == 'text_trimmed' || $display['type'] == 'text_summary_or_trimmed') {
+ $element['trim_length'] = array(
+ '#title' => t('Length'),
+ '#type' => 'textfield',
+ '#size' => 20,
+ '#default_value' => $settings['trim_length'],
+ '#element_validate' => array('element_validate_integer_positive'),
+ '#required' => TRUE,
+ );
+ }
+
+ return $element;
+
+}
+
+/**
+ * Return a short summary for the current formatter settings of an instance.
+ *
+ * If an empty result is returned, the formatter is assumed to have no
+ * configurable settings, and no UI will be provided to display a settings
+ * form.
+ *
+ * @param $field
+ * The field structure.
+ * @param $instance
+ * The instance structure.
+ * @param $view_mode
+ * The view mode for which a settings summary is requested.
+ *
+ * @return
+ * A string containing a short summary of the formatter settings.
+ */
+function hook_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $summary = '';
+
+ if ($display['type'] == 'text_trimmed' || $display['type'] == 'text_summary_or_trimmed') {
+ $summary = t('Length: @chars chars', array('@chars' => $settings['trim_length']));
+ }
+
+ return $summary;
+}
+
+/**
+ * @} End of "addtogroup field_types".
+ */
diff --git a/drupal-dev/modules/field_ui/field_ui.css b/drupal-dev/modules/field_ui/field_ui.css
new file mode 100644
index 0000000..2184023
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.css
@@ -0,0 +1,71 @@
+/**
+ * @file
+ * Stylesheet for the Field UI module.
+ */
+
+/* 'Manage fields' and 'Manage display' overviews */
+table.field-ui-overview tr.add-new .label-input {
+ float: left; /* LTR */
+}
+table.field-ui-overview tr.add-new .tabledrag-changed {
+ display: none;
+}
+table.field-ui-overview tr.add-new .description {
+ margin-bottom: 0;
+ max-width: 250px;
+}
+table.field-ui-overview tr.add-new .form-type-machine-name .description {
+ white-space: normal;
+}
+table.field-ui-overview tr.add-new .add-new-placeholder {
+ font-weight: bold;
+ padding-bottom: .5em;
+}
+table.field-ui-overview tr.region-title td {
+ font-weight: bold;
+}
+table.field-ui-overview tr.region-message td {
+ font-style: italic;
+}
+table.field-ui-overview tr.region-populated {
+ display: none;
+}
+table.field-ui-overview tr.region-add-new-title {
+ display: none;
+}
+table.field-ui-overview tr.add-new td {
+ vertical-align: top;
+ white-space: nowrap;
+}
+
+/* 'Manage display' overview */
+#field-display-overview .field-formatter-summary-cell {
+ line-height: 1em;
+}
+#field-display-overview .field-formatter-summary {
+ float: left;
+ font-size: 0.9em;
+}
+#field-display-overview td.field-formatter-summary-cell span.warning {
+ display: block;
+ float: left;
+ margin-right: .5em;
+}
+#field-display-overview .field-formatter-settings-edit-wrapper {
+ float: right;
+}
+#field-display-overview .field-formatter-settings-edit {
+ float: right;
+}
+#field-display-overview tr.field-formatter-settings-editing td {
+ vertical-align: top;
+}
+#field-display-overview tr.field-formatter-settings-editing .field-formatter-type {
+ display: none;
+}
+#field-display-overview .field-formatter-settings-edit-form .formatter-name{
+ font-weight: bold;
+}
+#field-ui-display-overview-form #edit-refresh {
+ display:none;
+}
diff --git a/drupal-dev/modules/field_ui/field_ui.info b/drupal-dev/modules/field_ui/field_ui.info
new file mode 100644
index 0000000..e9dc3c7
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.info
@@ -0,0 +1,13 @@
+name = Field UI
+description = User interface for the Field API.
+package = Core
+version = VERSION
+core = 7.x
+dependencies[] = field
+files[] = field_ui.test
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/field_ui/field_ui.js b/drupal-dev/modules/field_ui/field_ui.js
new file mode 100644
index 0000000..65b28d0
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.js
@@ -0,0 +1,341 @@
+/**
+ * @file
+ * Attaches the behaviors for the Field UI module.
+ */
+
+(function($) {
+
+Drupal.behaviors.fieldUIFieldOverview = {
+ attach: function (context, settings) {
+ $('table#field-overview', context).once('field-overview', function () {
+ Drupal.fieldUIFieldOverview.attachUpdateSelects(this, settings);
+ });
+ }
+};
+
+Drupal.fieldUIFieldOverview = {
+ /**
+ * Implements dependent select dropdowns on the 'Manage fields' screen.
+ */
+ attachUpdateSelects: function(table, settings) {
+ var widgetTypes = settings.fieldWidgetTypes;
+ var fields = settings.fields;
+
+ // Store the default text of widget selects.
+ $('.widget-type-select', table).each(function () {
+ this.initialValue = this.options[0].text;
+ });
+
+ // 'Field type' select updates its 'Widget' select.
+ $('.field-type-select', table).each(function () {
+ this.targetSelect = $('.widget-type-select', $(this).closest('tr'));
+
+ $(this).bind('change keyup', function () {
+ var selectedFieldType = this.options[this.selectedIndex].value;
+ var options = (selectedFieldType in widgetTypes ? widgetTypes[selectedFieldType] : []);
+ this.targetSelect.fieldUIPopulateOptions(options);
+ });
+
+ // Trigger change on initial pageload to get the right widget options
+ // when field type comes pre-selected (on failed validation).
+ $(this).trigger('change', false);
+ });
+
+ // 'Existing field' select updates its 'Widget' select and 'Label' textfield.
+ $('.field-select', table).each(function () {
+ this.targetSelect = $('.widget-type-select', $(this).closest('tr'));
+ this.targetTextfield = $('.label-textfield', $(this).closest('tr'));
+ this.targetTextfield
+ .data('field_ui_edited', false)
+ .bind('keyup', function (e) {
+ $(this).data('field_ui_edited', $(this).val() != '');
+ });
+
+ $(this).bind('change keyup', function (e, updateText) {
+ var updateText = (typeof updateText == 'undefined' ? true : updateText);
+ var selectedField = this.options[this.selectedIndex].value;
+ var selectedFieldType = (selectedField in fields ? fields[selectedField].type : null);
+ var selectedFieldWidget = (selectedField in fields ? fields[selectedField].widget : null);
+ var options = (selectedFieldType && (selectedFieldType in widgetTypes) ? widgetTypes[selectedFieldType] : []);
+ this.targetSelect.fieldUIPopulateOptions(options, selectedFieldWidget);
+
+ // Only overwrite the "Label" input if it has not been manually
+ // changed, or if it is empty.
+ if (updateText && !this.targetTextfield.data('field_ui_edited')) {
+ this.targetTextfield.val(selectedField in fields ? fields[selectedField].label : '');
+ }
+ });
+
+ // Trigger change on initial pageload to get the right widget options
+ // and label when field type comes pre-selected (on failed validation).
+ $(this).trigger('change', false);
+ });
+ }
+};
+
+/**
+ * Populates options in a select input.
+ */
+jQuery.fn.fieldUIPopulateOptions = function (options, selected) {
+ return this.each(function () {
+ var disabled = false;
+ if (options.length == 0) {
+ options = [this.initialValue];
+ disabled = true;
+ }
+
+ // If possible, keep the same widget selected when changing field type.
+ // This is based on textual value, since the internal value might be
+ // different (options_buttons vs. node_reference_buttons).
+ var previousSelectedText = this.options[this.selectedIndex].text;
+
+ var html = '';
+ jQuery.each(options, function (value, text) {
+ // Figure out which value should be selected. The 'selected' param
+ // takes precedence.
+ var is_selected = ((typeof selected != 'undefined' && value == selected) || (typeof selected == 'undefined' && text == previousSelectedText));
+ html += '';
+ });
+
+ $(this).html(html).attr('disabled', disabled ? 'disabled' : false);
+ });
+};
+
+Drupal.behaviors.fieldUIDisplayOverview = {
+ attach: function (context, settings) {
+ $('table#field-display-overview', context).once('field-display-overview', function() {
+ Drupal.fieldUIOverview.attach(this, settings.fieldUIRowsData, Drupal.fieldUIDisplayOverview);
+ });
+ }
+};
+
+Drupal.fieldUIOverview = {
+ /**
+ * Attaches the fieldUIOverview behavior.
+ */
+ attach: function (table, rowsData, rowHandlers) {
+ var tableDrag = Drupal.tableDrag[table.id];
+
+ // Add custom tabledrag callbacks.
+ tableDrag.onDrop = this.onDrop;
+ tableDrag.row.prototype.onSwap = this.onSwap;
+
+ // Create row handlers.
+ $('tr.draggable', table).each(function () {
+ // Extract server-side data for the row.
+ var row = this;
+ if (row.id in rowsData) {
+ var data = rowsData[row.id];
+ data.tableDrag = tableDrag;
+
+ // Create the row handler, make it accessible from the DOM row element.
+ var rowHandler = new rowHandlers[data.rowHandler](row, data);
+ $(row).data('fieldUIRowHandler', rowHandler);
+ }
+ });
+ },
+
+ /**
+ * Event handler to be attached to form inputs triggering a region change.
+ */
+ onChange: function () {
+ var $trigger = $(this);
+ var row = $trigger.closest('tr').get(0);
+ var rowHandler = $(row).data('fieldUIRowHandler');
+
+ var refreshRows = {};
+ refreshRows[rowHandler.name] = $trigger.get(0);
+
+ // Handle region change.
+ var region = rowHandler.getRegion();
+ if (region != rowHandler.region) {
+ // Remove parenting.
+ $('select.field-parent', row).val('');
+ // Let the row handler deal with the region change.
+ $.extend(refreshRows, rowHandler.regionChange(region));
+ // Update the row region.
+ rowHandler.region = region;
+ }
+
+ // Ajax-update the rows.
+ Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
+ },
+
+ /**
+ * Lets row handlers react when a row is dropped into a new region.
+ */
+ onDrop: function () {
+ var dragObject = this;
+ var row = dragObject.rowObject.element;
+ var rowHandler = $(row).data('fieldUIRowHandler');
+ if (rowHandler !== undefined) {
+ var regionRow = $(row).prevAll('tr.region-message').get(0);
+ var region = regionRow.className.replace(/([^ ]+[ ]+)*region-([^ ]+)-message([ ]+[^ ]+)*/, '$2');
+
+ if (region != rowHandler.region) {
+ // Let the row handler deal with the region change.
+ refreshRows = rowHandler.regionChange(region);
+ // Update the row region.
+ rowHandler.region = region;
+ // Ajax-update the rows.
+ Drupal.fieldUIOverview.AJAXRefreshRows(refreshRows);
+ }
+ }
+ },
+
+ /**
+ * Refreshes placeholder rows in empty regions while a row is being dragged.
+ *
+ * Copied from block.js.
+ *
+ * @param table
+ * The table DOM element.
+ * @param rowObject
+ * The tableDrag rowObject for the row being dragged.
+ */
+ onSwap: function (draggedRow) {
+ var rowObject = this;
+ $('tr.region-message', rowObject.table).each(function () {
+ // If the dragged row is in this region, but above the message row, swap
+ // it down one space.
+ if ($(this).prev('tr').get(0) == rowObject.group[rowObject.group.length - 1]) {
+ // Prevent a recursion problem when using the keyboard to move rows up.
+ if ((rowObject.method != 'keyboard' || rowObject.direction == 'down')) {
+ rowObject.swap('after', this);
+ }
+ }
+ // This region has become empty.
+ if ($(this).next('tr').is(':not(.draggable)') || $(this).next('tr').length == 0) {
+ $(this).removeClass('region-populated').addClass('region-empty');
+ }
+ // This region has become populated.
+ else if ($(this).is('.region-empty')) {
+ $(this).removeClass('region-empty').addClass('region-populated');
+ }
+ });
+ },
+
+ /**
+ * Triggers Ajax refresh of selected rows.
+ *
+ * The 'format type' selects can trigger a series of changes in child rows.
+ * The #ajax behavior is therefore not attached directly to the selects, but
+ * triggered manually through a hidden #ajax 'Refresh' button.
+ *
+ * @param rows
+ * A hash object, whose keys are the names of the rows to refresh (they
+ * will receive the 'ajax-new-content' effect on the server side), and
+ * whose values are the DOM element in the row that should get an Ajax
+ * throbber.
+ */
+ AJAXRefreshRows: function (rows) {
+ // Separate keys and values.
+ var rowNames = [];
+ var ajaxElements = [];
+ $.each(rows, function (rowName, ajaxElement) {
+ rowNames.push(rowName);
+ ajaxElements.push(ajaxElement);
+ });
+
+ if (rowNames.length) {
+ // Add a throbber next each of the ajaxElements.
+ var $throbber = $('
');
+ $(ajaxElements)
+ .addClass('progress-disabled')
+ .after($throbber);
+
+ // Fire the Ajax update.
+ $('input[name=refresh_rows]').val(rowNames.join(' '));
+ $('input#edit-refresh').mousedown();
+
+ // Disabled elements do not appear in POST ajax data, so we mark the
+ // elements disabled only after firing the request.
+ $(ajaxElements).attr('disabled', true);
+ }
+ }
+};
+
+
+/**
+ * Row handlers for the 'Manage display' screen.
+ */
+Drupal.fieldUIDisplayOverview = {};
+
+/**
+ * Constructor for a 'field' row handler.
+ *
+ * This handler is used for both fields and 'extra fields' rows.
+ *
+ * @param row
+ * The row DOM element.
+ * @param data
+ * Additional data to be populated in the constructed object.
+ */
+Drupal.fieldUIDisplayOverview.field = function (row, data) {
+ this.row = row;
+ this.name = data.name;
+ this.region = data.region;
+ this.tableDrag = data.tableDrag;
+
+ // Attach change listener to the 'formatter type' select.
+ this.$formatSelect = $('select.field-formatter-type', row);
+ this.$formatSelect.change(Drupal.fieldUIOverview.onChange);
+
+ return this;
+};
+
+Drupal.fieldUIDisplayOverview.field.prototype = {
+ /**
+ * Returns the region corresponding to the current form values of the row.
+ */
+ getRegion: function () {
+ return (this.$formatSelect.val() == 'hidden') ? 'hidden' : 'visible';
+ },
+
+ /**
+ * Reacts to a row being changed regions.
+ *
+ * This function is called when the row is moved to a different region, as a
+ * result of either :
+ * - a drag-and-drop action (the row's form elements then probably need to be
+ * updated accordingly)
+ * - user input in one of the form elements watched by the
+ * Drupal.fieldUIOverview.onChange change listener.
+ *
+ * @param region
+ * The name of the new region for the row.
+ * @return
+ * A hash object indicating which rows should be Ajax-updated as a result
+ * of the change, in the format expected by
+ * Drupal.displayOverview.AJAXRefreshRows().
+ */
+ regionChange: function (region) {
+
+ // When triggered by a row drag, the 'format' select needs to be adjusted
+ // to the new region.
+ var currentValue = this.$formatSelect.val();
+ switch (region) {
+ case 'visible':
+ if (currentValue == 'hidden') {
+ // Restore the formatter back to the default formatter. Pseudo-fields do
+ // not have default formatters, we just return to 'visible' for those.
+ var value = (this.defaultFormatter != undefined) ? this.defaultFormatter : 'visible';
+ }
+ break;
+
+ default:
+ var value = 'hidden';
+ break;
+ }
+ if (value != undefined) {
+ this.$formatSelect.val(value);
+ }
+
+ var refreshRows = {};
+ refreshRows[this.name] = this.$formatSelect.get(0);
+
+ return refreshRows;
+ }
+};
+
+})(jQuery);
diff --git a/drupal-dev/modules/field_ui/field_ui.module b/drupal-dev/modules/field_ui/field_ui.module
new file mode 100644
index 0000000..ed833fe
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.module
@@ -0,0 +1,394 @@
+' . t('About') . '';
+ $output .= '
' . t('The Field UI module provides an administrative user interface (UI) for attaching and managing fields. Fields can be defined at the content-type level for content items and comments, at the vocabulary level for taxonomy terms, and at the site level for user accounts. Other modules may also enable fields to be defined for their data. Field types (text, image, number, etc.) are defined by modules, and collected and managed by the Field module. For more information, see the online handbook entry for Field UI module.', array('@field' => url('admin/help/field'), '@field_ui' => 'http://drupal.org/documentation/modules/field-ui')) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Planning fields') . '
';
+ $output .= '
' . t('There are several decisions you will need to make before defining a field for content, comments, etc.:') . '
';
+ $output .= '
' . t('What the field will be called') . '
';
+ $output .= '
' . t('A field has a label (the name displayed in the user interface) and a machine name (the name used internally). The label can be changed after you create the field, if needed, but the machine name cannot be changed after you have created the field.') . '';
+ $output .= '
' . t('What type of data the field will store') . '
';
+ $output .= '
' . t('Each field can store one type of data (text, number, file, etc.). When you define a field, you choose a particular field type, which corresponds to the type of data you want to store. The field type cannot be changed after you have created the field.') . '
';
+ $output .= '
' . t('How the data will be input and displayed') . '
';
+ $output .= '
' . t('Each field type has one or more available widgets associated with it; each widget provides a mechanism for data input when you are editing (text box, select list, file upload, etc.). Each field type also has one or more display options, which determine how the field is displayed to site visitors. The widget and display options can be changed after you have created the field.') . '
';
+ $output .= '
' . t('How many values the field will store') . '
';
+ $output .= '
' . t('You can store one value, a specific maximum number of values, or an unlimited number of values in each field. For example, an employee identification number field might store a single number, whereas a phone number field might store multiple phone numbers. This setting can be changed after you have created the field, but if you reduce the maximum number of values, you may lose information.') . '
';
+ $output .= '
';
+ $output .= '
' . t('Reusing fields') . '
';
+ $output .= '
' . t('Once you have defined a field, you can reuse it. For example, if you define a custom image field for one content type, and you need to have an image field with the same parameters on another content type, you can add the same field to the second content type, in the Add existing field area of the user interface. You could also add this field to a taxonomy vocabulary, comments, user accounts, etc.') . '
';
+ $output .= '
' . t('Some settings of a reused field are unique to each use of the field; others are shared across all places you use the field. For example, the label of a text field is unique to each use, while the setting for the number of values is shared.') . '
';
+ $output .= '
' . t('There are two main reasons for reusing fields. First, reusing fields can save you time over defining new fields. Second, reusing fields also allows you to display, filter, group, and sort content together by field across content types. For example, the contributed Views module allows you to create lists and tables of content. So if you use the same field on multiple content types, you can create a View containing all of those content types together displaying that field, sorted by that field, and/or filtered by that field.') . '
';
+ $output .= '
' . t('Fields on content items') . '
';
+ $output .= '
' . t('Fields on content items are defined at the content-type level, on the Manage fields tab of the content type edit page (which you can reach from the Content types page). When you define a field for a content type, each content item of that type will have that field added to it. Some fields, such as the Title and Body, are provided for you when you create a content type, or are provided on content types created by your installation profile.', array('@types' => url('admin/structure/types'))) . '
';
+ $output .= '
' . t('Fields on taxonomy terms') . '
';
+ $output .= '
' . t('Fields on taxonomy terms are defined at the taxonomy vocabulary level, on the Manage fields tab of the vocabulary edit page (which you can reach from the Taxonomy page). When you define a field for a vocabulary, each term in that vocabulary will have that field added to it. For example, you could define an image field for a vocabulary to store an icon with each term.', array('@taxonomy' => url('admin/structure/taxonomy'))) . '
';
+ $output .= '
' . t('Fields on user accounts') . '
';
+ $output .= '
' . t('Fields on user accounts are defined on a site-wide basis on the Manage fields tab of the Account settings page. When you define a field for user accounts, each user account will have that field added to it. For example, you could add a long text field to allow users to include a biography.', array('@fields' => url('admin/config/people/accounts/fields'), '@accounts' => url('admin/config/people/accounts'))) . '
';
+ $output .= '
' . t('Fields on comments') . '
';
+ $output .= '
' . t('Fields on comments are defined at the content-type level, on the Comment fields tab of the content type edit page (which you can reach from the Content types page). When you add a field for comments, each comment on a content item of that type will have that field added to it. For example, you could add a website field to the comments on forum posts, to allow forum commenters to add a link to their website.', array('@types' => url('admin/structure/types'))) . '
' . t('This list shows all fields currently in use for easy reference.') . '
';
+ }
+}
+
+/**
+ * Implements hook_field_attach_rename_bundle().
+ */
+function field_ui_field_attach_rename_bundle($entity_type, $bundle_old, $bundle_new) {
+ // The Field UI relies on entity_get_info() to build menu items for entity
+ // field administration pages. Clear the entity info cache and ensure that
+ // the menu is rebuilt.
+ entity_info_cache_clear();
+ menu_rebuild();
+}
+
+/**
+ * Implements hook_menu().
+ */
+function field_ui_menu() {
+ $items['admin/reports/fields'] = array(
+ 'title' => 'Field list',
+ 'description' => 'Overview of fields on all entity types.',
+ 'page callback' => 'field_ui_fields_list',
+ 'access arguments' => array('administer content types'),
+ 'type' => MENU_NORMAL_ITEM,
+ 'file' => 'field_ui.admin.inc',
+ );
+
+ // Ensure the following is not executed until field_bundles is working and
+ // tables are updated. Needed to avoid errors on initial installation.
+ if (defined('MAINTENANCE_MODE')) {
+ return $items;
+ }
+
+ // Create tabs for all possible bundles.
+ foreach (entity_get_info() as $entity_type => $entity_info) {
+ if ($entity_info['fieldable']) {
+ foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+ if (isset($bundle_info['admin'])) {
+ // Extract path information from the bundle.
+ $path = $bundle_info['admin']['path'];
+ // Different bundles can appear on the same path (e.g. %node_type and
+ // %comment_node_type). To allow field_ui_menu_load() to extract the
+ // actual bundle object from the translated menu router path
+ // arguments, we need to identify the argument position of the bundle
+ // name string ('bundle argument') and pass that position to the menu
+ // loader. The position needs to be casted into a string; otherwise it
+ // would be replaced with the bundle name string.
+ if (isset($bundle_info['admin']['bundle argument'])) {
+ $bundle_arg = $bundle_info['admin']['bundle argument'];
+ $bundle_pos = (string) $bundle_arg;
+ }
+ else {
+ $bundle_arg = $bundle_name;
+ $bundle_pos = '0';
+ }
+ // This is the position of the %field_ui_menu placeholder in the
+ // items below.
+ $field_position = count(explode('/', $path)) + 1;
+
+ // Extract access information, providing defaults.
+ $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
+ $access += array(
+ 'access callback' => 'user_access',
+ 'access arguments' => array('administer site configuration'),
+ );
+
+ $items["$path/fields"] = array(
+ 'title' => 'Manage fields',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_field_overview_form', $entity_type, $bundle_arg),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 1,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+ $items["$path/fields/%field_ui_menu"] = array(
+ 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+ 'title callback' => 'field_ui_menu_title',
+ 'title arguments' => array($field_position),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_field_edit_form', $field_position),
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+ $items["$path/fields/%field_ui_menu/edit"] = array(
+ 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+ 'title' => 'Edit',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_field_edit_form', $field_position),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+ $items["$path/fields/%field_ui_menu/field-settings"] = array(
+ 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+ 'title' => 'Field settings',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_field_settings_form', $field_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+ $items["$path/fields/%field_ui_menu/widget-type"] = array(
+ 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+ 'title' => 'Widget type',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_widget_type_form', $field_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+ $items["$path/fields/%field_ui_menu/delete"] = array(
+ 'load arguments' => array($entity_type, $bundle_arg, $bundle_pos, '%map'),
+ 'title' => 'Delete',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_field_delete_form', $field_position),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 10,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+
+ // 'Manage display' tab.
+ $items["$path/display"] = array(
+ 'title' => 'Manage display',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('field_ui_display_overview_form', $entity_type, $bundle_arg, 'default'),
+ 'type' => MENU_LOCAL_TASK,
+ 'weight' => 2,
+ 'file' => 'field_ui.admin.inc',
+ ) + $access;
+
+ // View modes secondary tabs.
+ // The same base $path for the menu item (with a placeholder) can be
+ // used for all bundles of a given entity type; but depending on
+ // administrator settings, each bundle has a different set of view
+ // modes available for customisation. So we define menu items for all
+ // view modes, and use an access callback to determine which ones are
+ // actually visible for a given bundle.
+ $weight = 0;
+ $view_modes = array('default' => array('label' => t('Default'))) + $entity_info['view modes'];
+ foreach ($view_modes as $view_mode => $view_mode_info) {
+ $items["$path/display/$view_mode"] = array(
+ 'title' => $view_mode_info['label'],
+ 'page arguments' => array('field_ui_display_overview_form', $entity_type, $bundle_arg, $view_mode),
+ // The access callback needs to check both the current 'custom
+ // display' setting for the view mode, and the overall access
+ // rules for the bundle admin pages.
+ 'access callback' => '_field_ui_view_mode_menu_access',
+ 'access arguments' => array_merge(array($entity_type, $bundle_arg, $view_mode, $access['access callback']), $access['access arguments']),
+ 'type' => ($view_mode == 'default' ? MENU_DEFAULT_LOCAL_TASK : MENU_LOCAL_TASK),
+ 'weight' => ($view_mode == 'default' ? -10 : $weight++),
+ 'file' => 'field_ui.admin.inc',
+ );
+ }
+ }
+ }
+ }
+ }
+ return $items;
+}
+
+/**
+ * Menu loader; Load a field instance based on field and bundle name.
+ *
+ * @param $field_name
+ * The name of the field, as contained in the path.
+ * @param $entity_type
+ * The name of the entity.
+ * @param $bundle_name
+ * The name of the bundle, as contained in the path.
+ * @param $bundle_pos
+ * The position of $bundle_name in $map.
+ * @param $map
+ * The translated menu router path argument map.
+ *
+ * @return
+ * The field instance array.
+ *
+ * @ingroup field
+ */
+function field_ui_menu_load($field_name, $entity_type, $bundle_name, $bundle_pos, $map) {
+ // Extract the actual bundle name from the translated argument map.
+ // The menu router path to manage fields of an entity can be shared among
+ // multiple bundles. For example:
+ // - admin/structure/types/manage/%node_type/fields/%field_ui_menu
+ // - admin/structure/types/manage/%comment_node_type/fields/%field_ui_menu
+ // The menu system will automatically load the correct bundle depending on the
+ // actual path arguments, but this menu loader function only receives the node
+ // type string as $bundle_name, which is not the bundle name for comments.
+ // We therefore leverage the dynamically translated $map provided by the menu
+ // system to retrieve the actual bundle and bundle name for the current path.
+ if ($bundle_pos > 0) {
+ $bundle = $map[$bundle_pos];
+ $bundle_name = field_extract_bundle($entity_type, $bundle);
+ }
+ // Check whether the field exists at all.
+ if ($field = field_info_field($field_name)) {
+ // Only return the field if a field instance exists for the given entity
+ // type and bundle.
+ if ($instance = field_info_instance($entity_type, $field_name, $bundle_name)) {
+ return $instance;
+ }
+ }
+ return FALSE;
+}
+
+/**
+ * Menu title callback.
+ */
+function field_ui_menu_title($instance) {
+ return $instance['label'];
+}
+
+/**
+ * Menu access callback for the 'view mode display settings' pages.
+ */
+function _field_ui_view_mode_menu_access($entity_type, $bundle, $view_mode, $access_callback) {
+ // First, determine visibility according to the 'use custom display'
+ // setting for the view mode.
+ $bundle = field_extract_bundle($entity_type, $bundle);
+ $view_mode_settings = field_view_mode_settings($entity_type, $bundle);
+ $visibility = ($view_mode == 'default') || !empty($view_mode_settings[$view_mode]['custom_settings']);
+
+ // Then, determine access according to the $access parameter. This duplicates
+ // part of _menu_check_access().
+ if ($visibility) {
+ // Grab the variable 'access arguments' part.
+ $all_args = func_get_args();
+ $args = array_slice($all_args, 4);
+ $callback = empty($access_callback) ? 0 : trim($access_callback);
+ if (is_numeric($callback)) {
+ return (bool) $callback;
+ }
+ else {
+ // As call_user_func_array() is quite slow and user_access is a very
+ // common callback, it is worth making a special case for it.
+ if ($access_callback == 'user_access') {
+ return (count($args) == 1) ? user_access($args[0]) : user_access($args[0], $args[1]);
+ }
+ elseif (function_exists($access_callback)) {
+ return call_user_func_array($access_callback, $args);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function field_ui_theme() {
+ return array(
+ 'field_ui_table' => array(
+ 'render element' => 'elements',
+ ),
+ );
+}
+
+/**
+ * Implements hook_element_info().
+ */
+function field_ui_element_info() {
+ return array(
+ 'field_ui_table' => array(
+ '#theme' => 'field_ui_table',
+ '#pre_render' => array('field_ui_table_pre_render'),
+ '#regions' => array('' => array()),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_attach_create_bundle().
+ */
+function field_ui_field_attach_create_bundle($entity_type, $bundle) {
+ // When a new bundle is created, the menu needs to be rebuilt to add our
+ // menu item tabs.
+ variable_set('menu_rebuild_needed', TRUE);
+}
+
+/**
+ * Determines the administration path for a bundle.
+ */
+function _field_ui_bundle_admin_path($entity_type, $bundle_name) {
+ $bundles = field_info_bundles($entity_type);
+ $bundle_info = $bundles[$bundle_name];
+ if (isset($bundle_info['admin'])) {
+ return isset($bundle_info['admin']['real path']) ? $bundle_info['admin']['real path'] : $bundle_info['admin']['path'];
+ }
+}
+
+/**
+ * Identifies inactive fields within a bundle.
+ */
+function field_ui_inactive_instances($entity_type, $bundle_name = NULL) {
+ $params = array('entity_type' => $entity_type);
+
+ if (empty($bundle_name)) {
+ $active = field_info_instances($entity_type);
+ $inactive = array();
+ }
+ else {
+ // Restrict to the specified bundle. For consistency with the case where
+ // $bundle_name is NULL, the $active and $inactive arrays are keyed by
+ // bundle name first.
+ $params['bundle'] = $bundle_name;
+ $active = array($bundle_name => field_info_instances($entity_type, $bundle_name));
+ $inactive = array($bundle_name => array());
+ }
+
+ // Iterate on existing definitions, and spot those that do not appear in the
+ // $active list collected earlier.
+ $all_instances = field_read_instances($params, array('include_inactive' => TRUE));
+ foreach ($all_instances as $instance) {
+ if (!isset($active[$instance['bundle']][$instance['field_name']])) {
+ $inactive[$instance['bundle']][$instance['field_name']] = $instance;
+ }
+ }
+
+ if (!empty($bundle_name)) {
+ return $inactive[$bundle_name];
+ }
+ return $inactive;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Adds a button 'Save and add fields' to the 'Create content type' form.
+ *
+ * @see node_type_form()
+ * @see field_ui_form_node_type_form_submit()
+ */
+function field_ui_form_node_type_form_alter(&$form, $form_state) {
+ // We want to display the button only on add page.
+ if (empty($form['#node_type']->type)) {
+ $form['actions']['save_continue'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save and add fields'),
+ '#weight' => 45,
+ );
+ $form['#submit'][] = 'field_ui_form_node_type_form_submit';
+ }
+}
+
+/**
+ * Form submission handler for the 'Save and add fields' button.
+ *
+ * @see field_ui_form_node_type_form_alter()
+ */
+function field_ui_form_node_type_form_submit($form, &$form_state) {
+ if ($form_state['triggering_element']['#parents'][0] === 'save_continue') {
+ $form_state['redirect'] = _field_ui_bundle_admin_path('node', $form_state['values']['type']) .'/fields';
+ }
+}
diff --git a/drupal-dev/modules/field_ui/field_ui.test b/drupal-dev/modules/field_ui/field_ui.test
new file mode 100644
index 0000000..21767d6
--- /dev/null
+++ b/drupal-dev/modules/field_ui/field_ui.test
@@ -0,0 +1,751 @@
+drupalCreateUser(array('access content', 'administer content types', 'administer taxonomy'));
+ $this->drupalLogin($admin_user);
+
+ // Create content type, with underscores.
+ $type_name = strtolower($this->randomName(8)) . '_test';
+ $type = $this->drupalCreateContentType(array('name' => $type_name, 'type' => $type_name));
+ $this->type = $type->type;
+ // Store a valid URL name, with hyphens instead of underscores.
+ $this->hyphen_type = str_replace('_', '-', $this->type);
+ }
+
+ /**
+ * Creates a new field through the Field UI.
+ *
+ * @param $bundle_path
+ * Admin path of the bundle that the new field is to be attached to.
+ * @param $initial_edit
+ * $edit parameter for drupalPost() on the first step ('Manage fields'
+ * screen).
+ * @param $field_edit
+ * $edit parameter for drupalPost() on the second step ('Field settings'
+ * form).
+ * @param $instance_edit
+ * $edit parameter for drupalPost() on the third step ('Instance settings'
+ * form).
+ */
+ function fieldUIAddNewField($bundle_path, $initial_edit, $field_edit = array(), $instance_edit = array()) {
+ // Use 'test_field' field type by default.
+ $initial_edit += array(
+ 'fields[_add_new_field][type]' => 'test_field',
+ 'fields[_add_new_field][widget_type]' => 'test_field_widget',
+ );
+ $label = $initial_edit['fields[_add_new_field][label]'];
+ $field_name = $initial_edit['fields[_add_new_field][field_name]'];
+
+ // First step : 'Add new field' on the 'Manage fields' page.
+ $this->drupalPost("$bundle_path/fields", $initial_edit, t('Save'));
+ $this->assertRaw(t('These settings apply to the %label field everywhere it is used.', array('%label' => $label)), 'Field settings page was displayed.');
+
+ // Second step : 'Field settings' form.
+ $this->drupalPost(NULL, $field_edit, t('Save field settings'));
+ $this->assertRaw(t('Updated field %label field settings.', array('%label' => $label)), 'Redirected to instance and widget settings page.');
+
+ // Third step : 'Instance settings' form.
+ $this->drupalPost(NULL, $instance_edit, t('Save settings'));
+ $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.');
+
+ // Check that the field appears in the overview form.
+ $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.');
+ }
+
+ /**
+ * Adds an existing field through the Field UI.
+ *
+ * @param $bundle_path
+ * Admin path of the bundle that the field is to be attached to.
+ * @param $initial_edit
+ * $edit parameter for drupalPost() on the first step ('Manage fields'
+ * screen).
+ * @param $instance_edit
+ * $edit parameter for drupalPost() on the second step ('Instance settings'
+ * form).
+ */
+ function fieldUIAddExistingField($bundle_path, $initial_edit, $instance_edit = array()) {
+ // Use 'test_field_widget' by default.
+ $initial_edit += array(
+ 'fields[_add_existing_field][widget_type]' => 'test_field_widget',
+ );
+ $label = $initial_edit['fields[_add_existing_field][label]'];
+ $field_name = $initial_edit['fields[_add_existing_field][field_name]'];
+
+ // First step : 'Add existing field' on the 'Manage fields' page.
+ $this->drupalPost("$bundle_path/fields", $initial_edit, t('Save'));
+
+ // Second step : 'Instance settings' form.
+ $this->drupalPost(NULL, $instance_edit, t('Save settings'));
+ $this->assertRaw(t('Saved %label configuration.', array('%label' => $label)), 'Redirected to "Manage fields" page.');
+
+ // Check that the field appears in the overview form.
+ $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $label, 'Field was created and appears in the overview page.');
+ }
+
+ /**
+ * Deletes a field instance through the Field UI.
+ *
+ * @param $bundle_path
+ * Admin path of the bundle that the field instance is to be deleted from.
+ * @param $field_name
+ * The name of the field.
+ * @param $label
+ * The label of the field.
+ * @param $bundle_label
+ * The label of the bundle.
+ */
+ function fieldUIDeleteField($bundle_path, $field_name, $label, $bundle_label) {
+ // Display confirmation form.
+ $this->drupalGet("$bundle_path/fields/$field_name/delete");
+ $this->assertRaw(t('Are you sure you want to delete the field %label', array('%label' => $label)), 'Delete confirmation was found.');
+
+ // Submit confirmation form.
+ $this->drupalPost(NULL, array(), t('Delete'));
+ $this->assertRaw(t('The field %label has been deleted from the %type content type.', array('%label' => $label, '%type' => $bundle_label)), 'Delete message was found.');
+
+ // Check that the field does not appear in the overview form.
+ $this->assertNoFieldByXPath('//table[@id="field-overview"]//span[@class="label-field"]', $label, 'Field does not appear in the overview page.');
+ }
+}
+
+/**
+ * Tests the functionality of the 'Manage fields' screen.
+ */
+class FieldUIManageFieldsTestCase extends FieldUITestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Manage fields',
+ 'description' => 'Test the Field UI "Manage fields" screen.',
+ 'group' => 'Field UI',
+ );
+ }
+
+ function setUp() {
+ parent::setUp();
+
+ // Create random field name.
+ $this->field_label = $this->randomName(8);
+ $this->field_name_input = strtolower($this->randomName(8));
+ $this->field_name = 'field_'. $this->field_name_input;
+ }
+
+ /**
+ * Runs the field CRUD tests.
+ *
+ * In order to act on the same fields, and not create the fields over and over
+ * again the following tests create, update and delete the same fields.
+ */
+ function testCRUDFields() {
+ $this->manageFieldsPage();
+ $this->createField();
+ $this->updateField();
+ $this->addExistingField();
+ }
+
+ /**
+ * Tests the manage fields page.
+ */
+ function manageFieldsPage() {
+ $this->drupalGet('admin/structure/types/manage/' . $this->hyphen_type . '/fields');
+ // Check all table columns.
+ $table_headers = array(
+ t('Label'),
+ t('Machine name'),
+ t('Field type'),
+ t('Widget'),
+ t('Operations'),
+ );
+ foreach ($table_headers as $table_header) {
+ // We check that the label appear in the table headings.
+ $this->assertRaw($table_header . '', format_string('%table_header table header was found.', array('%table_header' => $table_header)));
+ }
+
+ // "Add new field" and "Add existing field" aren't a table heading so just
+ // test the text.
+ foreach (array('Add new field', 'Add existing field') as $element) {
+ $this->assertText($element, format_string('"@element" was found.', array('@element' => $element)));
+ }
+ }
+
+ /**
+ * Tests adding a new field.
+ *
+ * @todo Assert properties can bet set in the form and read back in $field and
+ * $instances.
+ */
+ function createField() {
+ // Create a test field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => $this->field_label,
+ 'fields[_add_new_field][field_name]' => $this->field_name_input,
+ );
+ $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->hyphen_type, $edit);
+
+ // Assert the field appears in the "add existing field" section for
+ // different entity types; e.g. if a field was added in a node entity, it
+ // should also appear in the 'taxonomy term' entity.
+ $vocabulary = taxonomy_vocabulary_load(1);
+ $this->drupalGet('admin/structure/taxonomy/' . $vocabulary->machine_name . '/fields');
+ $this->assertTrue($this->xpath('//select[@name="fields[_add_existing_field][field_name]"]//option[@value="' . $this->field_name . '"]'), 'Existing field was found in account settings.');
+ }
+
+ /**
+ * Tests editing an existing field.
+ */
+ function updateField() {
+ // Go to the field edit page.
+ $this->drupalGet('admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $this->field_name);
+
+ // Populate the field settings with new settings.
+ $string = 'updated dummy test string';
+ $edit = array(
+ 'field[settings][test_field_setting]' => $string,
+ 'instance[settings][test_instance_setting]' => $string,
+ 'instance[widget][settings][test_widget_setting]' => $string,
+ );
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+
+ // Assert the field settings are correct.
+ $this->assertFieldSettings($this->type, $this->field_name, $string);
+
+ // Assert redirection back to the "manage fields" page.
+ $this->assertText(t('Saved @label configuration.', array('@label' => $this->field_label)), 'Redirected to "Manage fields" page.');
+ }
+
+ /**
+ * Tests adding an existing field in another content type.
+ */
+ function addExistingField() {
+ // Check "Add existing field" appears.
+ $this->drupalGet('admin/structure/types/manage/page/fields');
+ $this->assertRaw(t('Add existing field'), '"Add existing field" was found.');
+
+ // Check that the list of options respects entity type restrictions on
+ // fields. The 'comment' field is restricted to the 'comment' entity type
+ // and should not appear in the list.
+ $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value="comment"]'), 'The list of options respects entity type restrictions.');
+
+ // Add a new field based on an existing field.
+ $edit = array(
+ 'fields[_add_existing_field][label]' => $this->field_label . '_2',
+ 'fields[_add_existing_field][field_name]' => $this->field_name,
+ );
+ $this->fieldUIAddExistingField("admin/structure/types/manage/page", $edit);
+ }
+
+ /**
+ * Asserts field settings are as expected.
+ *
+ * @param $bundle
+ * The bundle name for the instance.
+ * @param $field_name
+ * The field name for the instance.
+ * @param $string
+ * The settings text.
+ * @param $entity_type
+ * The entity type for the instance.
+ */
+ function assertFieldSettings($bundle, $field_name, $string = 'dummy test string', $entity_type = 'node') {
+ // Reset the fields info.
+ field_info_cache_clear();
+ // Assert field settings.
+ $field = field_info_field($field_name);
+ $this->assertTrue($field['settings']['test_field_setting'] == $string, 'Field settings were found.');
+
+ // Assert instance and widget settings.
+ $instance = field_info_instance($entity_type, $field_name, $bundle);
+ $this->assertTrue($instance['settings']['test_instance_setting'] == $string, 'Field instance settings were found.');
+ $this->assertTrue($instance['widget']['settings']['test_widget_setting'] == $string, 'Field widget settings were found.');
+ }
+
+ /**
+ * Tests that default value is correctly validated and saved.
+ */
+ function testDefaultValue() {
+ // Create a test field and instance.
+ $field_name = 'test';
+ $field = array(
+ 'field_name' => $field_name,
+ 'type' => 'test_field'
+ );
+ field_create_field($field);
+ $instance = array(
+ 'field_name' => $field_name,
+ 'entity_type' => 'node',
+ 'bundle' => $this->type,
+ );
+ field_create_instance($instance);
+
+ $langcode = LANGUAGE_NONE;
+ $admin_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/' . $field_name;
+ $element_id = "edit-$field_name-$langcode-0-value";
+ $element_name = "{$field_name}[$langcode][0][value]";
+ $this->drupalGet($admin_path);
+ $this->assertFieldById($element_id, '', 'The default value widget was empty.');
+
+ // Check that invalid default values are rejected.
+ $edit = array($element_name => '-1');
+ $this->drupalPost($admin_path, $edit, t('Save settings'));
+ $this->assertText("$field_name does not accept the value -1", 'Form vaildation failed.');
+
+ // Check that the default value is saved.
+ $edit = array($element_name => '1');
+ $this->drupalPost($admin_path, $edit, t('Save settings'));
+ $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
+ $instance = field_info_instance('node', $field_name, $this->type);
+ $this->assertEqual($instance['default_value'], array(array('value' => 1)), 'The default value was correctly saved.');
+
+ // Check that the default value shows up in the form
+ $this->drupalGet($admin_path);
+ $this->assertFieldById($element_id, '1', 'The default value widget was displayed with the correct value.');
+
+ // Check that the default value can be emptied.
+ $edit = array($element_name => '');
+ $this->drupalPost(NULL, $edit, t('Save settings'));
+ $this->assertText("Saved $field_name configuration", 'The form was successfully submitted.');
+ field_info_cache_clear();
+ $instance = field_info_instance('node', $field_name, $this->type);
+ $this->assertEqual($instance['default_value'], NULL, 'The default value was correctly saved.');
+ }
+
+ /**
+ * Tests that deletion removes fields and instances as expected.
+ */
+ function testDeleteField() {
+ // Create a new field.
+ $bundle_path1 = 'admin/structure/types/manage/' . $this->hyphen_type;
+ $edit1 = array(
+ 'fields[_add_new_field][label]' => $this->field_label,
+ 'fields[_add_new_field][field_name]' => $this->field_name_input,
+ );
+ $this->fieldUIAddNewField($bundle_path1, $edit1);
+
+ // Create an additional node type.
+ $type_name2 = strtolower($this->randomName(8)) . '_test';
+ $type2 = $this->drupalCreateContentType(array('name' => $type_name2, 'type' => $type_name2));
+ $type_name2 = $type2->type;
+ $hyphen_type2 = str_replace('_', '-', $type_name2);
+
+ // Add an instance to the second node type.
+ $bundle_path2 = 'admin/structure/types/manage/' . $hyphen_type2;
+ $edit2 = array(
+ 'fields[_add_existing_field][label]' => $this->field_label,
+ 'fields[_add_existing_field][field_name]' => $this->field_name,
+ );
+ $this->fieldUIAddExistingField($bundle_path2, $edit2);
+
+ // Delete the first instance.
+ $this->fieldUIDeleteField($bundle_path1, $this->field_name, $this->field_label, $this->type);
+
+ // Reset the fields info.
+ field_info_cache_clear();
+ // Check that the field instance was deleted.
+ $this->assertNull(field_info_instance('node', $this->field_name, $this->type), 'Field instance was deleted.');
+ // Check that the field was not deleted
+ $this->assertNotNull(field_info_field($this->field_name), 'Field was not deleted.');
+
+ // Delete the second instance.
+ $this->fieldUIDeleteField($bundle_path2, $this->field_name, $this->field_label, $type_name2);
+
+ // Reset the fields info.
+ field_info_cache_clear();
+ // Check that the field instance was deleted.
+ $this->assertNull(field_info_instance('node', $this->field_name, $type_name2), 'Field instance was deleted.');
+ // Check that the field was deleted too.
+ $this->assertNull(field_info_field($this->field_name), 'Field was deleted.');
+ }
+
+ /**
+ * Tests that Field UI respects the 'no_ui' option in hook_field_info().
+ */
+ function testHiddenFields() {
+ $bundle_path = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields/';
+
+ // Check that the field type is not available in the 'add new field' row.
+ $this->drupalGet($bundle_path);
+ $this->assertFalse($this->xpath('//select[@id="edit-add-new-field-type"]//option[@value="hidden_test_field"]'), "The 'add new field' select respects field types 'no_ui' property.");
+
+ // Create a field and an instance programmatically.
+ $field_name = 'hidden_test_field';
+ field_create_field(array('field_name' => $field_name, 'type' => $field_name));
+ $instance = array(
+ 'field_name' => $field_name,
+ 'bundle' => $this->type,
+ 'entity_type' => 'node',
+ 'label' => t('Hidden field'),
+ 'widget' => array('type' => 'test_field_widget'),
+ );
+ field_create_instance($instance);
+ $this->assertTrue(field_read_instance('node', $field_name, $this->type), format_string('An instance of the field %field was created programmatically.', array('%field' => $field_name)));
+
+ // Check that the newly added instance appears on the 'Manage Fields'
+ // screen.
+ $this->drupalGet($bundle_path);
+ $this->assertFieldByXPath('//table[@id="field-overview"]//td[1]', $instance['label'], 'Field was created and appears in the overview page.');
+
+ // Check that the instance does not appear in the 'add existing field' row
+ // on other bundles.
+ $bundle_path = 'admin/structure/types/manage/article/fields/';
+ $this->drupalGet($bundle_path);
+ $this->assertFalse($this->xpath('//select[@id="edit-add-existing-field-field-name"]//option[@value=:field_name]', array(':field_name' => $field_name)), "The 'add existing field' select respects field types 'no_ui' property.");
+ }
+
+ /**
+ * Tests renaming a bundle.
+ */
+ function testRenameBundle() {
+ $type2 = strtolower($this->randomName(8)) . '_' .'test';
+ $hyphen_type2 = str_replace('_', '-', $type2);
+
+ $options = array(
+ 'type' => $type2,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type, $options, t('Save content type'));
+
+ $this->drupalGet('admin/structure/types/manage/' . $hyphen_type2 . '/fields');
+ }
+
+ /**
+ * Tests that a duplicate field name is caught by validation.
+ */
+ function testDuplicateFieldName() {
+ // field_tags already exists, so we're expecting an error when trying to
+ // create a new field with the same name.
+ $edit = array(
+ 'fields[_add_new_field][field_name]' => 'tags',
+ 'fields[_add_new_field][label]' => $this->randomName(),
+ 'fields[_add_new_field][type]' => 'taxonomy_term_reference',
+ 'fields[_add_new_field][widget_type]' => 'options_select',
+ );
+ $url = 'admin/structure/types/manage/' . $this->hyphen_type . '/fields';
+ $this->drupalPost($url, $edit, t('Save'));
+
+ $this->assertText(t('The machine-readable name is already in use. It must be unique.'));
+ $this->assertUrl($url, array(), 'Stayed on the same page.');
+ }
+}
+
+/**
+ * Tests the functionality of the 'Manage display' screens.
+ */
+class FieldUIManageDisplayTestCase extends FieldUITestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Manage display',
+ 'description' => 'Test the Field UI "Manage display" screens.',
+ 'group' => 'Field UI',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('search'));
+ }
+
+ /**
+ * Tests formatter settings.
+ */
+ function testFormatterUI() {
+ $manage_fields = 'admin/structure/types/manage/' . $this->hyphen_type;
+ $manage_display = $manage_fields . '/display';
+
+ // Create a field, and a node with some data for the field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => 'Test field',
+ 'fields[_add_new_field][field_name]' => 'test',
+ );
+ $this->fieldUIAddNewField($manage_fields, $edit);
+
+ // Clear the test-side cache and get the saved field instance.
+ field_info_cache_clear();
+ $instance = field_info_instance('node', 'field_test', $this->type);
+ $format = $instance['display']['default']['type'];
+ $default_settings = field_info_formatter_settings($format);
+ $setting_name = key($default_settings);
+ $setting_value = $instance['display']['default']['settings'][$setting_name];
+
+ // Display the "Manage display" screen and check that the expected formatter is
+ // selected.
+ $this->drupalGet($manage_display);
+ $this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.');
+ $this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.');
+
+ // Change the formatter and check that the summary is updated.
+ $edit = array('fields[field_test][type]' => 'field_test_multiple', 'refresh_rows' => 'field_test');
+ $this->drupalPostAJAX(NULL, $edit, array('op' => t('Refresh')));
+ $format = 'field_test_multiple';
+ $default_settings = field_info_formatter_settings($format);
+ $setting_name = key($default_settings);
+ $setting_value = $default_settings[$setting_name];
+ $this->assertFieldByName('fields[field_test][type]', $format, 'The expected formatter is selected.');
+ $this->assertText("$setting_name: $setting_value", 'The expected summary is displayed.');
+
+ // Submit the form and check that the instance is updated.
+ $this->drupalPost(NULL, array(), t('Save'));
+ field_info_cache_clear();
+ $instance = field_info_instance('node', 'field_test', $this->type);
+ $current_format = $instance['display']['default']['type'];
+ $current_setting_value = $instance['display']['default']['settings'][$setting_name];
+ $this->assertEqual($current_format, $format, 'The formatter was updated.');
+ $this->assertEqual($current_setting_value, $setting_value, 'The setting was updated.');
+ }
+
+ /**
+ * Tests switching view modes to use custom or 'default' settings'.
+ */
+ function testViewModeCustom() {
+ // Create a field, and a node with some data for the field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => 'Test field',
+ 'fields[_add_new_field][field_name]' => 'test',
+ );
+ $this->fieldUIAddNewField('admin/structure/types/manage/' . $this->hyphen_type, $edit);
+ // For this test, use a formatter setting value that is an integer unlikely
+ // to appear in a rendered node other than as part of the field being tested
+ // (for example, unlikely to be part of the "Submitted by ... on ..." line).
+ $value = 12345;
+ $settings = array(
+ 'type' => $this->type,
+ 'field_test' => array(LANGUAGE_NONE => array(array('value' => $value))),
+ );
+ $node = $this->drupalCreateNode($settings);
+
+ // Gather expected output values with the various formatters.
+ $formatters = field_info_formatter_types();
+ $output = array(
+ 'field_test_default' => $formatters['field_test_default']['settings']['test_formatter_setting'] . '|' . $value,
+ 'field_test_with_prepare_view' => $formatters['field_test_with_prepare_view']['settings']['test_formatter_setting_additional'] . '|' . $value. '|' . ($value + 1),
+ );
+
+ // Check that the field is displayed with the default formatter in 'rss'
+ // mode (uses 'default'), and hidden in 'teaser' mode (uses custom settings).
+ $this->assertNodeViewText($node, 'rss', $output['field_test_default'], "The field is displayed as expected in view modes that use 'default' settings.");
+ $this->assertNodeViewNoText($node, 'teaser', $value, "The field is hidden in view modes that use custom settings.");
+
+ // Change fomatter for 'default' mode, check that the field is displayed
+ // accordingly in 'rss' mode.
+ $edit = array(
+ 'fields[field_test][type]' => 'field_test_with_prepare_view',
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
+ $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in view modes that use 'default' settings.");
+
+ // Specialize the 'rss' mode, check that the field is displayed the same.
+ $edit = array(
+ "view_modes_custom[rss]" => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
+ $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected in newly specialized 'rss' mode.");
+
+ // Set the field to 'hidden' in the view mode, check that the field is
+ // hidden.
+ $edit = array(
+ 'fields[field_test][type]' => 'hidden',
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display/rss', $edit, t('Save'));
+ $this->assertNodeViewNoText($node, 'rss', $value, "The field is hidden in 'rss' mode.");
+
+ // Set the view mode back to 'default', check that the field is displayed
+ // accordingly.
+ $edit = array(
+ "view_modes_custom[rss]" => FALSE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
+ $this->assertNodeViewText($node, 'rss', $output['field_test_with_prepare_view'], "The field is displayed as expected when 'rss' mode is set back to 'default' settings.");
+
+ // Specialize the view mode again.
+ $edit = array(
+ "view_modes_custom[rss]" => TRUE,
+ );
+ $this->drupalPost('admin/structure/types/manage/' . $this->hyphen_type . '/display', $edit, t('Save'));
+ // Check that the previous settings for the view mode have been kept.
+ $this->assertNodeViewNoText($node, 'rss', $value, "The previous settings are kept when 'rss' mode is specialized again.");
+ }
+
+ /**
+ * Asserts that a string is found in the rendered node in a view mode.
+ *
+ * @param $node
+ * The node.
+ * @param $view_mode
+ * The view mode in which the node should be displayed.
+ * @param $text
+ * Plain text to look for.
+ * @param $message
+ * Message to display.
+ *
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertNodeViewText($node, $view_mode, $text, $message) {
+ return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, FALSE);
+ }
+
+ /**
+ * Asserts that a string is not found in the rendered node in a view mode.
+ *
+ * @param $node
+ * The node.
+ * @param $view_mode
+ * The view mode in which the node should be displayed.
+ * @param $text
+ * Plain text to look for.
+ * @param $message
+ * Message to display.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertNodeViewNoText($node, $view_mode, $text, $message) {
+ return $this->assertNodeViewTextHelper($node, $view_mode, $text, $message, TRUE);
+ }
+
+ /**
+ * Asserts that a string is (not) found in the rendered nodein a view mode.
+ *
+ * This helper function is used by assertNodeViewText() and
+ * assertNodeViewNoText().
+ *
+ * @param $node
+ * The node.
+ * @param $view_mode
+ * The view mode in which the node should be displayed.
+ * @param $text
+ * Plain text to look for.
+ * @param $message
+ * Message to display.
+ * @param $not_exists
+ * TRUE if this text should not exist, FALSE if it should.
+ *
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertNodeViewTextHelper($node, $view_mode, $text, $message, $not_exists) {
+ // Make sure caches on the tester side are refreshed after changes
+ // submitted on the tested side.
+ field_info_cache_clear();
+
+ // Save current content so that we can restore it when we're done.
+ $old_content = $this->drupalGetContent();
+
+ // Render a cloned node, so that we do not alter the original.
+ $clone = clone $node;
+ $element = node_view($clone, $view_mode);
+ $output = drupal_render($element);
+ $this->verbose(t('Rendered node - view mode: @view_mode', array('@view_mode' => $view_mode)) . ''. $output);
+
+ // Assign content so that DrupalWebTestCase functions can be used.
+ $this->drupalSetContent($output);
+ $method = ($not_exists ? 'assertNoText' : 'assertText');
+ $return = $this->{$method}((string) $text, $message);
+
+ // Restore previous content.
+ $this->drupalSetContent($old_content);
+
+ return $return;
+ }
+}
+
+/**
+ * Tests custom widget hooks and callbacks on the field administration pages.
+ */
+class FieldUIAlterTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Widget customization',
+ 'description' => 'Test custom field widget hooks and callbacks on field administration pages.',
+ 'group' => 'Field UI',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('field_test'));
+
+ // Create test user.
+ $admin_user = $this->drupalCreateUser(array('access content', 'administer content types', 'administer users'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Tests hook_field_widget_properties_alter() on the default field widget.
+ *
+ * @see field_test_field_widget_properties_alter()
+ * @see field_test_field_widget_properties_user_alter()
+ * @see field_test_field_widget_form_alter()
+ */
+ function testDefaultWidgetPropertiesAlter() {
+ // Create the alter_test_text field and an instance on article nodes.
+ field_create_field(array(
+ 'field_name' => 'alter_test_text',
+ 'type' => 'text',
+ ));
+ field_create_instance(array(
+ 'field_name' => 'alter_test_text',
+ 'entity_type' => 'node',
+ 'bundle' => 'article',
+ 'widget' => array(
+ 'type' => 'text_textfield',
+ 'size' => 60,
+ ),
+ ));
+
+ // Test that field_test_field_widget_properties_alter() sets the size to
+ // 42 and that field_test_field_widget_form_alter() reports the correct
+ // size when the form is displayed.
+ $this->drupalGet('admin/structure/types/manage/article/fields/alter_test_text');
+ $this->assertText('Field size: 42', 'Altered field size is found in hook_field_widget_form_alter().');
+
+ // Create the alter_test_options field.
+ field_create_field(array(
+ 'field_name' => 'alter_test_options',
+ 'type' => 'list_text'
+ ));
+ // Create instances on users and page nodes.
+ field_create_instance(array(
+ 'field_name' => 'alter_test_options',
+ 'entity_type' => 'user',
+ 'bundle' => 'user',
+ 'widget' => array(
+ 'type' => 'options_select',
+ )
+ ));
+ field_create_instance(array(
+ 'field_name' => 'alter_test_options',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'widget' => array(
+ 'type' => 'options_select',
+ )
+ ));
+
+ // Test that field_test_field_widget_properties_user_alter() replaces
+ // the widget and that field_test_field_widget_form_alter() reports the
+ // correct widget name when the form is displayed.
+ $this->drupalGet('admin/config/people/accounts/fields/alter_test_options');
+ $this->assertText('Widget type: options_buttons', 'Widget type is altered for users in hook_field_widget_form_alter().');
+
+ // Test that the widget is not altered on page nodes.
+ $this->drupalGet('admin/structure/types/manage/page/fields/alter_test_options');
+ $this->assertText('Widget type: options_select', 'Widget type is not altered for pages in hook_field_widget_form_alter().');
+ }
+}
diff --git a/drupal-dev/modules/file/file.api.php b/drupal-dev/modules/file/file.api.php
new file mode 100644
index 0000000..df178c6
--- /dev/null
+++ b/drupal-dev/modules/file/file.api.php
@@ -0,0 +1,60 @@
+ $grants['node']);
+ }
+}
diff --git a/drupal-dev/modules/file/file.css b/drupal-dev/modules/file/file.css
new file mode 100644
index 0000000..bd4a059
--- /dev/null
+++ b/drupal-dev/modules/file/file.css
@@ -0,0 +1,35 @@
+/**
+ * @file
+ * Admin stylesheet for file module.
+ */
+
+/**
+ * Managed file element styles.
+ */
+.form-managed-file .form-file,
+.form-managed-file .form-submit {
+ margin: 0;
+}
+
+.form-managed-file input.progress-disabled {
+ float: none;
+ display: inline;
+}
+
+.form-managed-file div.ajax-progress,
+.form-managed-file div.throbber {
+ display: inline;
+ float: none;
+ padding: 1px 5px 2px 5px;
+}
+
+.form-managed-file div.ajax-progress-bar {
+ display: none;
+ margin-top: 4px;
+ width: 28em;
+ padding: 0;
+}
+
+.form-managed-file div.ajax-progress-bar div.bar {
+ margin: 0;
+}
diff --git a/drupal-dev/modules/file/file.field.inc b/drupal-dev/modules/file/file.field.inc
new file mode 100644
index 0000000..d540c0a
--- /dev/null
+++ b/drupal-dev/modules/file/file.field.inc
@@ -0,0 +1,1024 @@
+ array(
+ 'label' => t('File'),
+ 'description' => t('This field stores the ID of a file as an integer value.'),
+ 'settings' => array(
+ 'display_field' => 0,
+ 'display_default' => 0,
+ 'uri_scheme' => variable_get('file_default_scheme', 'public'),
+ ),
+ 'instance_settings' => array(
+ 'file_extensions' => 'txt',
+ 'file_directory' => '',
+ 'max_filesize' => '',
+ 'description_field' => 0,
+ ),
+ 'default_widget' => 'file_generic',
+ 'default_formatter' => 'file_default',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_settings_form().
+ */
+function file_field_settings_form($field, $instance, $has_data) {
+ $defaults = field_info_field_settings($field['type']);
+ $settings = array_merge($defaults, $field['settings']);
+
+ $form['#attached']['js'][] = drupal_get_path('module', 'file') . '/file.js';
+
+ $form['display_field'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable Display field'),
+ '#default_value' => $settings['display_field'],
+ '#description' => t('The display option allows users to choose if a file should be shown when viewing the content.'),
+ );
+ $form['display_default'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Files displayed by default'),
+ '#default_value' => $settings['display_default'],
+ '#description' => t('This setting only has an effect if the display option is enabled.'),
+ );
+
+ $scheme_options = array();
+ foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) {
+ $scheme_options[$scheme] = $stream_wrapper['name'];
+ }
+ $form['uri_scheme'] = array(
+ '#type' => 'radios',
+ '#title' => t('Upload destination'),
+ '#options' => $scheme_options,
+ '#default_value' => $settings['uri_scheme'],
+ '#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
+ '#disabled' => $has_data,
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_instance_settings_form().
+ */
+function file_field_instance_settings_form($field, $instance) {
+ $settings = $instance['settings'];
+
+ $form['file_directory'] = array(
+ '#type' => 'textfield',
+ '#title' => t('File directory'),
+ '#default_value' => $settings['file_directory'],
+ '#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'),
+ '#element_validate' => array('_file_generic_settings_file_directory_validate'),
+ '#weight' => 3,
+ );
+
+ // Make the extension list a little more human-friendly by comma-separation.
+ $extensions = str_replace(' ', ', ', $settings['file_extensions']);
+ $form['file_extensions'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Allowed file extensions'),
+ '#default_value' => $extensions,
+ '#description' => t('Separate extensions with a space or comma and do not include the leading dot.'),
+ '#element_validate' => array('_file_generic_settings_extensions'),
+ '#weight' => 1,
+ // By making this field required, we prevent a potential security issue
+ // that would allow files of any type to be uploaded.
+ '#required' => TRUE,
+ );
+
+ $form['max_filesize'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum upload size'),
+ '#default_value' => $settings['max_filesize'],
+ '#description' => t('Enter a value like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes) in order to restrict the allowed file size. If left empty the file sizes will be limited only by PHP\'s maximum post and file upload sizes (current limit %limit).', array('%limit' => format_size(file_upload_max_size()))),
+ '#size' => 10,
+ '#element_validate' => array('_file_generic_settings_max_filesize'),
+ '#weight' => 5,
+ );
+
+ $form['description_field'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable Description field'),
+ '#default_value' => isset($settings['description_field']) ? $settings['description_field'] : '',
+ '#description' => t('The description field allows users to enter a description about the uploaded file.'),
+ '#parents' => array('instance', 'settings', 'description_field'),
+ '#weight' => 11,
+ );
+
+ return $form;
+}
+
+/**
+ * Element validate callback for the maximum upload size field.
+ *
+ * Ensure a size that can be parsed by parse_size() has been entered.
+ */
+function _file_generic_settings_max_filesize($element, &$form_state) {
+ if (!empty($element['#value']) && !is_numeric(parse_size($element['#value']))) {
+ form_error($element, t('The "!name" option must contain a valid value. You may either leave the text field empty or enter a string like "512" (bytes), "80 KB" (kilobytes) or "50 MB" (megabytes).', array('!name' => t($element['title']))));
+ }
+}
+
+/**
+ * Element validate callback for the allowed file extensions field.
+ *
+ * This doubles as a convenience clean-up function and a validation routine.
+ * Commas are allowed by the end-user, but ultimately the value will be stored
+ * as a space-separated list for compatibility with file_validate_extensions().
+ */
+function _file_generic_settings_extensions($element, &$form_state) {
+ if (!empty($element['#value'])) {
+ $extensions = preg_replace('/([, ]+\.?)/', ' ', trim(strtolower($element['#value'])));
+ $extensions = array_filter(explode(' ', $extensions));
+ $extensions = implode(' ', array_unique($extensions));
+ if (!preg_match('/^([a-z0-9]+([.][a-z0-9])* ?)+$/', $extensions)) {
+ form_error($element, t('The list of allowed extensions is not valid, be sure to exclude leading dots and to separate extensions with a comma or space.'));
+ }
+ else {
+ form_set_value($element, $extensions, $form_state);
+ }
+ }
+}
+
+/**
+ * Element validate callback for the file destination field.
+ *
+ * Remove slashes from the beginning and end of the destination value and ensure
+ * that the file directory path is not included at the beginning of the value.
+ */
+function _file_generic_settings_file_directory_validate($element, &$form_state) {
+ // Strip slashes from the beginning and end of $widget['file_directory'].
+ $value = trim($element['#value'], '\\/');
+ form_set_value($element, $value, $form_state);
+}
+
+/**
+ * Implements hook_field_load().
+ */
+function file_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
+
+ $fids = array();
+ foreach ($entities as $id => $entity) {
+ // Load the files from the files table.
+ foreach ($items[$id] as $delta => $item) {
+ if (!empty($item['fid'])) {
+ $fids[] = $item['fid'];
+ }
+ }
+ }
+ $files = file_load_multiple($fids);
+
+ foreach ($entities as $id => $entity) {
+ foreach ($items[$id] as $delta => $item) {
+ // If the file does not exist, mark the entire item as empty.
+ if (empty($item['fid']) || !isset($files[$item['fid']])) {
+ $items[$id][$delta] = NULL;
+ }
+ else {
+ $items[$id][$delta] = array_merge((array) $files[$item['fid']], $item);
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_prepare_view().
+ */
+function file_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
+ // Remove files specified to not be displayed.
+ foreach ($entities as $id => $entity) {
+ foreach ($items[$id] as $delta => $item) {
+ if (!file_field_displayed($item, $field)) {
+ unset($items[$id][$delta]);
+ }
+ }
+ // Ensure consecutive deltas.
+ $items[$id] = array_values($items[$id]);
+ }
+}
+
+/**
+ * Implements hook_field_presave().
+ */
+function file_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ // Make sure that each file which will be saved with this object has a
+ // permanent status, so that it will not be removed when temporary files are
+ // cleaned up.
+ foreach ($items as $delta => $item) {
+ if (empty($item['fid'])) {
+ unset($items[$delta]);
+ continue;
+ }
+ $file = file_load($item['fid']);
+ if (empty($file)) {
+ unset($items[$delta]);
+ continue;
+ }
+ if (!$file->status) {
+ $file->status = FILE_STATUS_PERMANENT;
+ file_save($file);
+ }
+ }
+}
+
+/**
+ * Implements hook_field_insert().
+ */
+function file_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // Add a new usage of each uploaded file.
+ foreach ($items as $item) {
+ $file = (object) $item;
+ file_usage_add($file, 'file', $entity_type, $id);
+ }
+}
+
+/**
+ * Implements hook_field_update().
+ *
+ * Checks for files that have been removed from the object.
+ */
+function file_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // On new revisions, all files are considered to be a new usage and no
+ // deletion of previous file usages are necessary.
+ if (!empty($entity->revision)) {
+ foreach ($items as $item) {
+ $file = (object) $item;
+ file_usage_add($file, 'file', $entity_type, $id);
+ }
+ return;
+ }
+
+ // Build a display of the current FIDs.
+ $current_fids = array();
+ foreach ($items as $item) {
+ $current_fids[] = $item['fid'];
+ }
+
+ // Compare the original field values with the ones that are being saved. Use
+ // $entity->original to check this when possible, but if it isn't available,
+ // create a bare-bones entity and load its previous values instead.
+ if (isset($entity->original)) {
+ $original = $entity->original;
+ }
+ else {
+ $original = entity_create_stub_entity($entity_type, array($id, $vid, $bundle));
+ field_attach_load($entity_type, array($id => $original), FIELD_LOAD_CURRENT, array('field_id' => $field['id']));
+ }
+ $original_fids = array();
+ if (!empty($original->{$field['field_name']}[$langcode])) {
+ foreach ($original->{$field['field_name']}[$langcode] as $original_item) {
+ $original_fids[] = $original_item['fid'];
+ if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) {
+ // Decrement the file usage count by 1 and delete the file if possible.
+ file_field_delete_file($original_item, $field, $entity_type, $id);
+ }
+ }
+ }
+
+ // Add new usage entries for newly added files.
+ foreach ($items as $item) {
+ if (!in_array($item['fid'], $original_fids)) {
+ $file = (object) $item;
+ file_usage_add($file, 'file', $entity_type, $id);
+ }
+ }
+}
+
+/**
+ * Implements hook_field_delete().
+ */
+function file_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+
+ // Delete all file usages within this entity.
+ foreach ($items as $delta => $item) {
+ file_field_delete_file($item, $field, $entity_type, $id, 0);
+ }
+}
+
+/**
+ * Implements hook_field_delete_revision().
+ */
+function file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity);
+ foreach ($items as $delta => $item) {
+ // Decrement the file usage count by 1 and delete the file if possible.
+ if (file_field_delete_file($item, $field, $entity_type, $id)) {
+ $items[$delta] = NULL;
+ }
+ }
+}
+
+/**
+ * Decrements the usage count for a file and attempts to delete it.
+ *
+ * This function only has an effect if the file being deleted is used only by
+ * File module.
+ *
+ * @param $item
+ * The field item that contains a file array.
+ * @param $field
+ * The field structure for the operation.
+ * @param $entity_type
+ * The type of $entity.
+ * @param $id
+ * The entity ID which contains the file being deleted.
+ * @param $count
+ * (optional) The number of references to decrement from the object
+ * containing the file. Defaults to 1.
+ *
+ * @return
+ * Boolean TRUE if the file was deleted, or an array of remaining references
+ * if the file is still in use by other modules. Boolean FALSE if an error
+ * was encountered.
+ */
+function file_field_delete_file($item, $field, $entity_type, $id, $count = 1) {
+ // To prevent the file field from deleting files it doesn't know about, check
+ // the file reference count. Temporary files can be deleted because they
+ // are not yet associated with any content at all.
+ $file = (object) $item;
+ $file_usage = file_usage_list($file);
+ if ($file->status == 0 || !empty($file_usage['file'])) {
+ file_usage_delete($file, 'file', $entity_type, $id, $count);
+ return file_delete($file);
+ }
+
+ // Even if the file is not deleted, return TRUE to indicate the file field
+ // record can be removed from the field database tables.
+ return TRUE;
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function file_field_is_empty($item, $field) {
+ return empty($item['fid']);
+}
+
+/**
+ * Determines whether a file should be displayed when outputting field content.
+ *
+ * @param $item
+ * A field item array.
+ * @param $field
+ * A field array.
+ *
+ * @return
+ * Boolean TRUE if the file will be displayed, FALSE if the file is hidden.
+ */
+function file_field_displayed($item, $field) {
+ if (!empty($field['settings']['display_field'])) {
+ return (bool) $item['display'];
+ }
+ return TRUE;
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function file_field_formatter_info() {
+ return array(
+ 'file_default' => array(
+ 'label' => t('Generic file'),
+ 'field types' => array('file'),
+ ),
+ 'file_table' => array(
+ 'label' => t('Table of files'),
+ 'field types' => array('file'),
+ ),
+ 'file_url_plain' => array(
+ 'label' => t('URL to file'),
+ 'field types' => array('file'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function file_field_widget_info() {
+ return array(
+ 'file_generic' => array(
+ 'label' => t('File'),
+ 'field types' => array('file'),
+ 'settings' => array(
+ 'progress_indicator' => 'throbber',
+ ),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ 'default value' => FIELD_BEHAVIOR_NONE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function file_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'];
+
+ $form['progress_indicator'] = array(
+ '#type' => 'radios',
+ '#title' => t('Progress indicator'),
+ '#options' => array(
+ 'throbber' => t('Throbber'),
+ 'bar' => t('Bar with progress meter'),
+ ),
+ '#default_value' => $settings['progress_indicator'],
+ '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
+ '#weight' => 16,
+ '#access' => file_progress_implementation(),
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function file_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+
+ $defaults = array(
+ 'fid' => 0,
+ 'display' => !empty($field['settings']['display_default']),
+ 'description' => '',
+ );
+
+ // Load the items for form rebuilds from the field state as they might not be
+ // in $form_state['values'] because of validation limitations. Also, they are
+ // only passed in as $items when editing existing entities.
+ $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
+ if (isset($field_state['items'])) {
+ $items = $field_state['items'];
+ }
+
+ // Essentially we use the managed_file type, extended with some enhancements.
+ $element_info = element_info('managed_file');
+ $element += array(
+ '#type' => 'managed_file',
+ '#upload_location' => file_field_widget_uri($field, $instance),
+ '#upload_validators' => file_field_widget_upload_validators($field, $instance),
+ '#value_callback' => 'file_field_widget_value',
+ '#process' => array_merge($element_info['#process'], array('file_field_widget_process')),
+ '#progress_indicator' => $instance['widget']['settings']['progress_indicator'],
+ // Allows this field to return an array instead of a single value.
+ '#extended' => TRUE,
+ );
+
+ if ($field['cardinality'] == 1) {
+ // Set the default value.
+ $element['#default_value'] = !empty($items) ? $items[0] : $defaults;
+ // If there's only one field, return it as delta 0.
+ if (empty($element['#default_value']['fid'])) {
+ $element['#description'] = theme('file_upload_help', array('description' => $element['#description'], 'upload_validators' => $element['#upload_validators']));
+ }
+ $elements = array($element);
+ }
+ else {
+ // If there are multiple values, add an element for each existing one.
+ foreach ($items as $item) {
+ $elements[$delta] = $element;
+ $elements[$delta]['#default_value'] = $item;
+ $elements[$delta]['#weight'] = $delta;
+ $delta++;
+ }
+ // And then add one more empty row for new uploads except when this is a
+ // programmed form as it is not necessary.
+ if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
+ $elements[$delta] = $element;
+ $elements[$delta]['#default_value'] = $defaults;
+ $elements[$delta]['#weight'] = $delta;
+ $elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
+ }
+ // The group of elements all-together need some extra functionality
+ // after building up the full list (like draggable table rows).
+ $elements['#file_upload_delta'] = $delta;
+ $elements['#theme'] = 'file_widget_multiple';
+ $elements['#theme_wrappers'] = array('fieldset');
+ $elements['#process'] = array('file_field_widget_process_multiple');
+ $elements['#title'] = $element['#title'];
+ $elements['#description'] = $element['#description'];
+ $elements['#field_name'] = $element['#field_name'];
+ $elements['#language'] = $element['#language'];
+ $elements['#display_field'] = $field['settings']['display_field'];
+
+ // Add some properties that will eventually be added to the file upload
+ // field. These are added here so that they may be referenced easily through
+ // a hook_form_alter().
+ $elements['#file_upload_title'] = t('Add a new file');
+ $elements['#file_upload_description'] = theme('file_upload_help', array('description' => '', 'upload_validators' => $elements[0]['#upload_validators']));
+ }
+
+ return $elements;
+}
+
+/**
+ * Retrieves the upload validators for a file field.
+ *
+ * @param $field
+ * A field array.
+ *
+ * @return
+ * An array suitable for passing to file_save_upload() or the file field
+ * element's '#upload_validators' property.
+ */
+function file_field_widget_upload_validators($field, $instance) {
+ // Cap the upload size according to the PHP limit.
+ $max_filesize = parse_size(file_upload_max_size());
+ if (!empty($instance['settings']['max_filesize']) && parse_size($instance['settings']['max_filesize']) < $max_filesize) {
+ $max_filesize = parse_size($instance['settings']['max_filesize']);
+ }
+
+ $validators = array();
+
+ // There is always a file size limit due to the PHP server limit.
+ $validators['file_validate_size'] = array($max_filesize);
+
+ // Add the extension check if necessary.
+ if (!empty($instance['settings']['file_extensions'])) {
+ $validators['file_validate_extensions'] = array($instance['settings']['file_extensions']);
+ }
+
+ return $validators;
+}
+
+/**
+ * Determines the URI for a file field instance.
+ *
+ * @param $field
+ * A field array.
+ * @param $instance
+ * A field instance array.
+ * @param $data
+ * An array of token objects to pass to token_replace().
+ *
+ * @return
+ * A file directory URI with tokens replaced.
+ *
+ * @see token_replace()
+ */
+function file_field_widget_uri($field, $instance, $data = array()) {
+ $destination = trim($instance['settings']['file_directory'], '/');
+
+ // Replace tokens.
+ $destination = token_replace($destination, $data);
+
+ return $field['settings']['uri_scheme'] . '://' . $destination;
+}
+
+/**
+ * The #value_callback for the file_generic field element.
+ */
+function file_field_widget_value($element, $input = FALSE, $form_state) {
+ if ($input) {
+ // Checkboxes lose their value when empty.
+ // If the display field is present make sure its unchecked value is saved.
+ $field = field_widget_field($element, $form_state);
+ if (empty($input['display'])) {
+ $input['display'] = $field['settings']['display_field'] ? 0 : 1;
+ }
+ }
+
+ // We depend on the managed file element to handle uploads.
+ $return = file_managed_file_value($element, $input, $form_state);
+
+ // Ensure that all the required properties are returned even if empty.
+ $return += array(
+ 'fid' => 0,
+ 'display' => 1,
+ 'description' => '',
+ );
+
+ return $return;
+}
+
+/**
+ * An element #process callback for the file_generic field type.
+ *
+ * Expands the file_generic type to include the description and display fields.
+ */
+function file_field_widget_process($element, &$form_state, $form) {
+ $item = $element['#value'];
+ $item['fid'] = $element['fid']['#value'];
+
+ $field = field_widget_field($element, $form_state);
+ $instance = field_widget_instance($element, $form_state);
+ $settings = $instance['widget']['settings'];
+
+ $element['#theme'] = 'file_widget';
+
+ // Add the display field if enabled.
+ if (!empty($field['settings']['display_field']) && $item['fid']) {
+ $element['display'] = array(
+ '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
+ '#title' => t('Include file in display'),
+ '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
+ '#attributes' => array('class' => array('file-display')),
+ );
+ }
+ else {
+ $element['display'] = array(
+ '#type' => 'hidden',
+ '#value' => '1',
+ );
+ }
+
+ // Add the description field if enabled.
+ if (!empty($instance['settings']['description_field']) && $item['fid']) {
+ $element['description'] = array(
+ '#type' => variable_get('file_description_type', 'textfield'),
+ '#title' => t('Description'),
+ '#value' => isset($item['description']) ? $item['description'] : '',
+ '#maxlength' => variable_get('file_description_length', 128),
+ '#description' => t('The description may be used as the label of the link to the file.'),
+ );
+ }
+
+ // Adjust the Ajax settings so that on upload and remove of any individual
+ // file, the entire group of file fields is updated together.
+ if ($field['cardinality'] != 1) {
+ $parents = array_slice($element['#array_parents'], 0, -1);
+ $new_path = 'file/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
+ $field_element = drupal_array_get_nested_value($form, $parents);
+ $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
+ foreach (element_children($element) as $key) {
+ if (isset($element[$key]['#ajax'])) {
+ $element[$key]['#ajax']['path'] = $new_path;
+ $element[$key]['#ajax']['wrapper'] = $new_wrapper;
+ }
+ }
+ unset($element['#prefix'], $element['#suffix']);
+ }
+
+ // Add another submit handler to the upload and remove buttons, to implement
+ // functionality needed by the field widget. This submit handler, along with
+ // the rebuild logic in file_field_widget_form() requires the entire field,
+ // not just the individual item, to be valid.
+ foreach (array('upload_button', 'remove_button') as $key) {
+ $element[$key]['#submit'][] = 'file_field_widget_submit';
+ $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
+ }
+
+ return $element;
+}
+
+/**
+ * An element #process callback for a group of file_generic fields.
+ *
+ * Adds the weight field to each row so it can be ordered and adds a new Ajax
+ * wrapper around the entire group so it can be replaced all at once.
+ */
+function file_field_widget_process_multiple($element, &$form_state, $form) {
+ $element_children = element_children($element, TRUE);
+ $count = count($element_children);
+
+ foreach ($element_children as $delta => $key) {
+ if ($key != $element['#file_upload_delta']) {
+ $description = _file_field_get_description_from_element($element[$key]);
+ $element[$key]['_weight'] = array(
+ '#type' => 'weight',
+ '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
+ '#title_display' => 'invisible',
+ '#delta' => $count,
+ '#default_value' => $delta,
+ );
+ }
+ else {
+ // The title needs to be assigned to the upload field so that validation
+ // errors include the correct widget label.
+ $element[$key]['#title'] = $element['#title'];
+ $element[$key]['_weight'] = array(
+ '#type' => 'hidden',
+ '#default_value' => $delta,
+ );
+ }
+ }
+
+ // Add a new wrapper around all the elements for Ajax replacement.
+ $element['#prefix'] = '
';
+ $element['#suffix'] = '
';
+
+ return $element;
+}
+
+/**
+ * Retrieves the file description from a field field element.
+ *
+ * This helper function is used by file_field_widget_process_multiple().
+ *
+ * @param $element
+ * The element being processed.
+ *
+ * @return
+ * A description of the file suitable for use in the administrative interface.
+ */
+function _file_field_get_description_from_element($element) {
+ // Use the actual file description, if it's available.
+ if (!empty($element['#default_value']['description'])) {
+ return $element['#default_value']['description'];
+ }
+ // Otherwise, fall back to the filename.
+ if (!empty($element['#default_value']['filename'])) {
+ return $element['#default_value']['filename'];
+ }
+ // This is probably a newly uploaded file; no description is available.
+ return FALSE;
+}
+
+/**
+ * Form submission handler for upload/remove button of file_field_widget_form().
+ *
+ * This runs in addition to and after file_managed_file_submit().
+ *
+ * @see file_managed_file_submit()
+ * @see file_field_widget_form()
+ * @see file_field_widget_process()
+ */
+function file_field_widget_submit($form, &$form_state) {
+ // During the form rebuild, file_field_widget_form() will create field item
+ // widget elements using re-indexed deltas, so clear out $form_state['input']
+ // to avoid a mismatch between old and new deltas. The rebuilt elements will
+ // have #default_value set appropriately for the current state of the field,
+ // so nothing is lost in doing this.
+ $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
+ drupal_array_set_nested_value($form_state['input'], $parents, NULL);
+
+ $button = $form_state['triggering_element'];
+
+ // Go one level up in the form, to the widgets container.
+ $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
+ $field_name = $element['#field_name'];
+ $langcode = $element['#language'];
+ $parents = $element['#field_parents'];
+
+ $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2));
+ foreach ($submitted_values as $delta => $submitted_value) {
+ if (!$submitted_value['fid']) {
+ unset($submitted_values[$delta]);
+ }
+ }
+
+ // Re-index deltas after removing empty items.
+ $submitted_values = array_values($submitted_values);
+
+ // Update form_state values.
+ drupal_array_set_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2), $submitted_values);
+
+ // Update items.
+ $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
+ $field_state['items'] = $submitted_values;
+ field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
+}
+
+/**
+ * Returns HTML for an individual file upload widget.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the widget.
+ *
+ * @ingroup themeable
+ */
+function theme_file_widget($variables) {
+ $element = $variables['element'];
+ $output = '';
+
+ // The "form-managed-file" class is required for proper Ajax functionality.
+ $output .= '
';
+ if ($element['fid']['#value'] != 0) {
+ // Add the file size after the file name.
+ $element['filename']['#markup'] .= ' (' . format_size($element['#file']->filesize) . ') ';
+ }
+ $output .= drupal_render_children($element);
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Returns HTML for a group of file upload widgets.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the widgets.
+ *
+ * @ingroup themeable
+ */
+function theme_file_widget_multiple($variables) {
+ $element = $variables['element'];
+
+ // Special ID and classes for draggable tables.
+ $weight_class = $element['#id'] . '-weight';
+ $table_id = $element['#id'] . '-table';
+
+ // Build up a table of applicable fields.
+ $headers = array();
+ $headers[] = t('File information');
+ if ($element['#display_field']) {
+ $headers[] = array(
+ 'data' => t('Display'),
+ 'class' => array('checkbox'),
+ );
+ }
+ $headers[] = t('Weight');
+ $headers[] = t('Operations');
+
+ // Get our list of widgets in order (needed when the form comes back after
+ // preview or failed validation).
+ $widgets = array();
+ foreach (element_children($element) as $key) {
+ $widgets[] = &$element[$key];
+ }
+ usort($widgets, '_field_sort_items_value_helper');
+
+ $rows = array();
+ foreach ($widgets as $key => &$widget) {
+ // Save the uploading row for last.
+ if ($widget['#file'] == FALSE) {
+ $widget['#title'] = $element['#file_upload_title'];
+ $widget['#description'] = $element['#file_upload_description'];
+ continue;
+ }
+
+ // Delay rendering of the buttons, so that they can be rendered later in the
+ // "operations" column.
+ $operations_elements = array();
+ foreach (element_children($widget) as $sub_key) {
+ if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
+ hide($widget[$sub_key]);
+ $operations_elements[] = &$widget[$sub_key];
+ }
+ }
+
+ // Delay rendering of the "Display" option and the weight selector, so that
+ // each can be rendered later in its own column.
+ if ($element['#display_field']) {
+ hide($widget['display']);
+ }
+ hide($widget['_weight']);
+
+ // Render everything else together in a column, without the normal wrappers.
+ $widget['#theme_wrappers'] = array();
+ $information = drupal_render($widget);
+
+ // Render the previously hidden elements, using render() instead of
+ // drupal_render(), to undo the earlier hide().
+ $operations = '';
+ foreach ($operations_elements as $operation_element) {
+ $operations .= render($operation_element);
+ }
+ $display = '';
+ if ($element['#display_field']) {
+ unset($widget['display']['#title']);
+ $display = array(
+ 'data' => render($widget['display']),
+ 'class' => array('checkbox'),
+ );
+ }
+ $widget['_weight']['#attributes']['class'] = array($weight_class);
+ $weight = render($widget['_weight']);
+
+ // Arrange the row with all of the rendered columns.
+ $row = array();
+ $row[] = $information;
+ if ($element['#display_field']) {
+ $row[] = $display;
+ }
+ $row[] = $weight;
+ $row[] = $operations;
+ $rows[] = array(
+ 'data' => $row,
+ 'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
+ );
+ }
+
+ drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
+
+ $output = '';
+ $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
+ $output .= drupal_render_children($element);
+ return $output;
+}
+
+
+/**
+ * Returns HTML for help text based on file upload validators.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - description: The normal description for this field, specified by the
+ * user.
+ * - upload_validators: An array of upload validators as used in
+ * $element['#upload_validators'].
+ *
+ * @ingroup themeable
+ */
+function theme_file_upload_help($variables) {
+ $description = $variables['description'];
+ $upload_validators = $variables['upload_validators'];
+
+ $descriptions = array();
+
+ if (strlen($description)) {
+ $descriptions[] = $description;
+ }
+ if (isset($upload_validators['file_validate_size'])) {
+ $descriptions[] = t('Files must be less than !size.', array('!size' => '' . format_size($upload_validators['file_validate_size'][0]) . ''));
+ }
+ if (isset($upload_validators['file_validate_extensions'])) {
+ $descriptions[] = t('Allowed file types: !extensions.', array('!extensions' => '' . check_plain($upload_validators['file_validate_extensions'][0]) . ''));
+ }
+ if (isset($upload_validators['file_validate_image_resolution'])) {
+ $max = $upload_validators['file_validate_image_resolution'][0];
+ $min = $upload_validators['file_validate_image_resolution'][1];
+ if ($min && $max && $min == $max) {
+ $descriptions[] = t('Images must be exactly !size pixels.', array('!size' => '' . $max . ''));
+ }
+ elseif ($min && $max) {
+ $descriptions[] = t('Images must be between !min and !max pixels.', array('!min' => '' . $min . '', '!max' => '' . $max . ''));
+ }
+ elseif ($min) {
+ $descriptions[] = t('Images must be larger than !min pixels.', array('!min' => '' . $min . ''));
+ }
+ elseif ($max) {
+ $descriptions[] = t('Images must be smaller than !max pixels.', array('!max' => '' . $max . ''));
+ }
+ }
+
+ return implode(' ', $descriptions);
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function file_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ switch ($display['type']) {
+ case 'file_default':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array(
+ '#theme' => 'file_link',
+ '#file' => (object) $item,
+ );
+ }
+ break;
+
+ case 'file_url_plain':
+ foreach ($items as $delta => $item) {
+ $element[$delta] = array('#markup' => empty($item['uri']) ? '' : file_create_url($item['uri']));
+ }
+ break;
+
+ case 'file_table':
+ if (!empty($items)) {
+ // Display all values in a single element..
+ $element[0] = array(
+ '#theme' => 'file_formatter_table',
+ '#items' => $items,
+ );
+ }
+ break;
+ }
+
+ return $element;
+}
+
+/**
+ * Returns HTML for a file attachments table.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - items: An array of file attachments.
+ *
+ * @ingroup themeable
+ */
+function theme_file_formatter_table($variables) {
+ $header = array(t('Attachment'), t('Size'));
+ $rows = array();
+ foreach ($variables['items'] as $delta => $item) {
+ $rows[] = array(
+ theme('file_link', array('file' => (object) $item)),
+ format_size($item['filesize']),
+ );
+ }
+
+ return empty($rows) ? '' : theme('table', array('header' => $header, 'rows' => $rows));
+}
diff --git a/drupal-dev/modules/file/file.info b/drupal-dev/modules/file/file.info
new file mode 100644
index 0000000..b96e435
--- /dev/null
+++ b/drupal-dev/modules/file/file.info
@@ -0,0 +1,13 @@
+name = File
+description = Defines a file field type.
+package = Core
+version = VERSION
+core = 7.x
+dependencies[] = field
+files[] = tests/file.test
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/file/file.install b/drupal-dev/modules/file/file.install
new file mode 100644
index 0000000..47ee4fd
--- /dev/null
+++ b/drupal-dev/modules/file/file.install
@@ -0,0 +1,98 @@
+ array(
+ 'fid' => array(
+ 'description' => 'The {file_managed}.fid being referenced in this field.',
+ 'type' => 'int',
+ 'not null' => FALSE,
+ 'unsigned' => TRUE,
+ ),
+ 'display' => array(
+ 'description' => 'Flag to control whether this file should be displayed when viewing content.',
+ 'type' => 'int',
+ 'size' => 'tiny',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 1,
+ ),
+ 'description' => array(
+ 'description' => 'A description of the file.',
+ 'type' => 'text',
+ 'not null' => FALSE,
+ ),
+ ),
+ 'indexes' => array(
+ 'fid' => array('fid'),
+ ),
+ 'foreign keys' => array(
+ 'fid' => array(
+ 'table' => 'file_managed',
+ 'columns' => array('fid' => 'fid'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_requirements().
+ *
+ * Display information about getting upload progress bars working.
+ */
+function file_requirements($phase) {
+ $requirements = array();
+
+ // Check the server's ability to indicate upload progress.
+ if ($phase == 'runtime') {
+ $implementation = file_progress_implementation();
+ $apache = strpos($_SERVER['SERVER_SOFTWARE'], 'Apache') !== FALSE;
+ $fastcgi = strpos($_SERVER['SERVER_SOFTWARE'], 'mod_fastcgi') !== FALSE || strpos($_SERVER["SERVER_SOFTWARE"], 'mod_fcgi') !== FALSE;
+ $description = NULL;
+ if (!$apache) {
+ $value = t('Not enabled');
+ $description = t('Your server is not capable of displaying file upload progress. File upload progress requires an Apache server running PHP with mod_php.');
+ $severity = REQUIREMENT_INFO;
+ }
+ elseif ($fastcgi) {
+ $value = t('Not enabled');
+ $description = t('Your server is not capable of displaying file upload progress. File upload progress requires PHP be run with mod_php and not as FastCGI.');
+ $severity = REQUIREMENT_INFO;
+ }
+ elseif (!$implementation && extension_loaded('apc')) {
+ $value = t('Not enabled');
+ $description = t('Your server is capable of displaying file upload progress through APC, but it is not enabled. Add apc.rfc1867 = 1 to your php.ini configuration. Alternatively, it is recommended to use PECL uploadprogress, which supports more than one simultaneous upload.');
+ $severity = REQUIREMENT_INFO;
+ }
+ elseif (!$implementation) {
+ $value = t('Not enabled');
+ $description = t('Your server is capable of displaying file upload progress, but does not have the required libraries. It is recommended to install the PECL uploadprogress library (preferred) or to install APC.');
+ $severity = REQUIREMENT_INFO;
+ }
+ elseif ($implementation == 'apc') {
+ $value = t('Enabled (APC RFC1867)');
+ $description = t('Your server is capable of displaying file upload progress using APC RFC1867. Note that only one upload at a time is supported. It is recommended to use the PECL uploadprogress library if possible.');
+ $severity = REQUIREMENT_OK;
+ }
+ elseif ($implementation == 'uploadprogress') {
+ $value = t('Enabled (PECL uploadprogress)');
+ $severity = REQUIREMENT_OK;
+ }
+ $requirements['file_progress'] = array(
+ 'title' => t('Upload progress'),
+ 'value' => $value,
+ 'severity' => $severity,
+ 'description' => $description,
+ );
+ }
+
+ return $requirements;
+}
diff --git a/drupal-dev/modules/file/file.js b/drupal-dev/modules/file/file.js
new file mode 100644
index 0000000..0135a3b
--- /dev/null
+++ b/drupal-dev/modules/file/file.js
@@ -0,0 +1,155 @@
+/**
+ * @file
+ * Provides JavaScript additions to the managed file field type.
+ *
+ * This file provides progress bar support (if available), popup windows for
+ * file previews, and disabling of other file fields during Ajax uploads (which
+ * prevents separate file fields from accidentally uploading files).
+ */
+
+(function ($) {
+
+/**
+ * Attach behaviors to managed file element upload fields.
+ */
+Drupal.behaviors.fileValidateAutoAttach = {
+ attach: function (context, settings) {
+ if (settings.file && settings.file.elements) {
+ $.each(settings.file.elements, function(selector) {
+ var extensions = settings.file.elements[selector];
+ $(selector, context).bind('change', {extensions: extensions}, Drupal.file.validateExtension);
+ });
+ }
+ },
+ detach: function (context, settings) {
+ if (settings.file && settings.file.elements) {
+ $.each(settings.file.elements, function(selector) {
+ $(selector, context).unbind('change', Drupal.file.validateExtension);
+ });
+ }
+ }
+};
+
+/**
+ * Attach behaviors to the file upload and remove buttons.
+ */
+Drupal.behaviors.fileButtons = {
+ attach: function (context) {
+ $('input.form-submit', context).bind('mousedown', Drupal.file.disableFields);
+ $('div.form-managed-file input.form-submit', context).bind('mousedown', Drupal.file.progressBar);
+ },
+ detach: function (context) {
+ $('input.form-submit', context).unbind('mousedown', Drupal.file.disableFields);
+ $('div.form-managed-file input.form-submit', context).unbind('mousedown', Drupal.file.progressBar);
+ }
+};
+
+/**
+ * Attach behaviors to links within managed file elements.
+ */
+Drupal.behaviors.filePreviewLinks = {
+ attach: function (context) {
+ $('div.form-managed-file .file a, .file-widget .file a', context).bind('click',Drupal.file.openInNewWindow);
+ },
+ detach: function (context){
+ $('div.form-managed-file .file a, .file-widget .file a', context).unbind('click', Drupal.file.openInNewWindow);
+ }
+};
+
+/**
+ * File upload utility functions.
+ */
+Drupal.file = Drupal.file || {
+ /**
+ * Client-side file input validation of file extensions.
+ */
+ validateExtension: function (event) {
+ // Remove any previous errors.
+ $('.file-upload-js-error').remove();
+
+ // Add client side validation for the input[type=file].
+ var extensionPattern = event.data.extensions.replace(/,\s*/g, '|');
+ if (extensionPattern.length > 1 && this.value.length > 0) {
+ var acceptableMatch = new RegExp('\\.(' + extensionPattern + ')$', 'gi');
+ if (!acceptableMatch.test(this.value)) {
+ var error = Drupal.t("The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.", {
+ // According to the specifications of HTML5, a file upload control
+ // should not reveal the real local path to the file that a user
+ // has selected. Some web browsers implement this restriction by
+ // replacing the local path with "C:\fakepath\", which can cause
+ // confusion by leaving the user thinking perhaps Drupal could not
+ // find the file because it messed up the file path. To avoid this
+ // confusion, therefore, we strip out the bogus fakepath string.
+ '%filename': this.value.replace('C:\\fakepath\\', ''),
+ '%extensions': extensionPattern.replace(/\|/g, ', ')
+ });
+ $(this).closest('div.form-managed-file').prepend('
' + error + '
');
+ this.value = '';
+ return false;
+ }
+ }
+ },
+ /**
+ * Prevent file uploads when using buttons not intended to upload.
+ */
+ disableFields: function (event){
+ var clickedButton = this;
+
+ // Only disable upload fields for Ajax buttons.
+ if (!$(clickedButton).hasClass('ajax-processed')) {
+ return;
+ }
+
+ // Check if we're working with an "Upload" button.
+ var $enabledFields = [];
+ if ($(this).closest('div.form-managed-file').length > 0) {
+ $enabledFields = $(this).closest('div.form-managed-file').find('input.form-file');
+ }
+
+ // Temporarily disable upload fields other than the one we're currently
+ // working with. Filter out fields that are already disabled so that they
+ // do not get enabled when we re-enable these fields at the end of behavior
+ // processing. Re-enable in a setTimeout set to a relatively short amount
+ // of time (1 second). All the other mousedown handlers (like Drupal's Ajax
+ // behaviors) are excuted before any timeout functions are called, so we
+ // don't have to worry about the fields being re-enabled too soon.
+ // @todo If the previous sentence is true, why not set the timeout to 0?
+ var $fieldsToTemporarilyDisable = $('div.form-managed-file input.form-file').not($enabledFields).not(':disabled');
+ $fieldsToTemporarilyDisable.attr('disabled', 'disabled');
+ setTimeout(function (){
+ $fieldsToTemporarilyDisable.attr('disabled', false);
+ }, 1000);
+ },
+ /**
+ * Add progress bar support if possible.
+ */
+ progressBar: function (event) {
+ var clickedButton = this;
+ var $progressId = $(clickedButton).closest('div.form-managed-file').find('input.file-progress');
+ if ($progressId.length) {
+ var originalName = $progressId.attr('name');
+
+ // Replace the name with the required identifier.
+ $progressId.attr('name', originalName.match(/APC_UPLOAD_PROGRESS|UPLOAD_IDENTIFIER/)[0]);
+
+ // Restore the original name after the upload begins.
+ setTimeout(function () {
+ $progressId.attr('name', originalName);
+ }, 1000);
+ }
+ // Show the progress bar if the upload takes longer than half a second.
+ setTimeout(function () {
+ $(clickedButton).closest('div.form-managed-file').find('div.ajax-progress-bar').slideDown();
+ }, 500);
+ },
+ /**
+ * Open links to files within forms in a new window.
+ */
+ openInNewWindow: function (event) {
+ $(this).attr('target', '_blank');
+ window.open(this.href, 'filePreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550');
+ return false;
+ }
+};
+
+})(jQuery);
diff --git a/drupal-dev/modules/file/file.module b/drupal-dev/modules/file/file.module
new file mode 100644
index 0000000..3d351fa
--- /dev/null
+++ b/drupal-dev/modules/file/file.module
@@ -0,0 +1,1013 @@
+' . t('About') . '';
+ $output .= '
' . t('The File module defines a File field type for the Field module, which lets you manage and validate uploaded files attached to content on your site (see the Field module help page for more information about fields). For more information, see the online handbook entry for File module.', array('@field-help' => url('admin/help/field'), '@file' => 'http://drupal.org/documentation/modules/file')) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Attaching files to content') . '
';
+ $output .= '
' . t('The File module allows users to attach files to content (e.g., PDF files, spreadsheets, etc.), when a File field is added to a given content type using the Field UI module. You can add validation options to your File field, such as specifying a maximum file size and allowed file extensions.', array('@fieldui-help' => url('admin/help/field_ui'))) . '
';
+ $output .= '
' . t('Managing attachment display') . '
';
+ $output .= '
' . t('When you attach a file to content, you can specify whether it is listed or not. Listed files are displayed automatically in a section at the bottom of your content; non-listed files are available for embedding in your content, but are not included in the list at the bottom.') . '
';
+ $output .= '
' . t('Managing file locations') . '
';
+ $output .= '
' . t("When you create a File field, you can specify a directory where the files will be stored, which can be within either the public or private files directory. Files in the public directory can be accessed directly through the web server; when public files are listed, direct links to the files are used, and anyone who knows a file's URL can download the file. Files in the private directory are not accessible directly through the web server; when private files are listed, the links are Drupal path requests. This adds to server load and download time, since Drupal must start up and resolve the path for each file download request, but allows for access restrictions.") . '
';
+ $output .= '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function file_menu() {
+ $items = array();
+
+ $items['file/ajax'] = array(
+ 'page callback' => 'file_ajax_upload',
+ 'delivery callback' => 'ajax_deliver',
+ 'access arguments' => array('access content'),
+ 'theme callback' => 'ajax_base_page_theme',
+ 'type' => MENU_CALLBACK,
+ );
+ $items['file/progress'] = array(
+ 'page callback' => 'file_ajax_progress',
+ 'access arguments' => array('access content'),
+ 'theme callback' => 'ajax_base_page_theme',
+ 'type' => MENU_CALLBACK,
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_element_info().
+ *
+ * The managed file element may be used anywhere in Drupal.
+ */
+function file_element_info() {
+ $file_path = drupal_get_path('module', 'file');
+ $types['managed_file'] = array(
+ '#input' => TRUE,
+ '#process' => array('file_managed_file_process'),
+ '#value_callback' => 'file_managed_file_value',
+ '#element_validate' => array('file_managed_file_validate'),
+ '#pre_render' => array('file_managed_file_pre_render'),
+ '#theme' => 'file_managed_file',
+ '#theme_wrappers' => array('form_element'),
+ '#progress_indicator' => 'throbber',
+ '#progress_message' => NULL,
+ '#upload_validators' => array(),
+ '#upload_location' => NULL,
+ '#size' => 22,
+ '#extended' => FALSE,
+ '#attached' => array(
+ 'css' => array($file_path . '/file.css'),
+ 'js' => array($file_path . '/file.js'),
+ ),
+ );
+ return $types;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function file_theme() {
+ return array(
+ // file.module.
+ 'file_link' => array(
+ 'variables' => array('file' => NULL, 'icon_directory' => NULL),
+ ),
+ 'file_icon' => array(
+ 'variables' => array('file' => NULL, 'icon_directory' => NULL),
+ ),
+ 'file_managed_file' => array(
+ 'render element' => 'element',
+ ),
+
+ // file.field.inc.
+ 'file_widget' => array(
+ 'render element' => 'element',
+ ),
+ 'file_widget_multiple' => array(
+ 'render element' => 'element',
+ ),
+ 'file_formatter_table' => array(
+ 'variables' => array('items' => NULL),
+ ),
+ 'file_upload_help' => array(
+ 'variables' => array('description' => NULL, 'upload_validators' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_file_download().
+ *
+ * This function takes an extra parameter $field_type so that it may
+ * be re-used by other File-like modules, such as Image.
+ */
+function file_file_download($uri, $field_type = 'file') {
+ global $user;
+
+ // Get the file record based on the URI. If not in the database just return.
+ $files = file_load_multiple(array(), array('uri' => $uri));
+ if (count($files)) {
+ foreach ($files as $item) {
+ // Since some database servers sometimes use a case-insensitive comparison
+ // by default, double check that the filename is an exact match.
+ if ($item->uri === $uri) {
+ $file = $item;
+ break;
+ }
+ }
+ }
+ if (!isset($file)) {
+ return;
+ }
+
+ // Find out which (if any) fields of this type contain the file.
+ $references = file_get_file_references($file, NULL, FIELD_LOAD_CURRENT, $field_type);
+
+ // Stop processing if there are no references in order to avoid returning
+ // headers for files controlled by other modules. Make an exception for
+ // temporary files where the host entity has not yet been saved (for example,
+ // an image preview on a node/add form) in which case, allow download by the
+ // file's owner.
+ if (empty($references) && ($file->status == FILE_STATUS_PERMANENT || $file->uid != $user->uid)) {
+ return;
+ }
+
+ // Default to allow access.
+ $denied = FALSE;
+ // Loop through all references of this file. If a reference explicitly allows
+ // access to the field to which this file belongs, no further checks are done
+ // and download access is granted. If a reference denies access, eventually
+ // existing additional references are checked. If all references were checked
+ // and no reference denied access, access is granted as well. If at least one
+ // reference denied access, access is denied.
+ foreach ($references as $field_name => $field_references) {
+ foreach ($field_references as $entity_type => $type_references) {
+ foreach ($type_references as $id => $reference) {
+ // Try to load $entity and $field.
+ $entity = entity_load($entity_type, array($id));
+ $entity = reset($entity);
+ $field = field_info_field($field_name);
+
+ // Load the field item that references the file.
+ $field_item = NULL;
+ if ($entity) {
+ // Load all field items for that entity.
+ $field_items = field_get_items($entity_type, $entity, $field_name);
+
+ // Find the field item with the matching URI.
+ foreach ($field_items as $item) {
+ if ($item['uri'] == $uri) {
+ $field_item = $item;
+ break;
+ }
+ }
+ }
+
+ // Check that $entity, $field and $field_item were loaded successfully
+ // and check if access to that field is not disallowed. If any of these
+ // checks fail, stop checking access for this reference.
+ if (empty($entity) || empty($field) || empty($field_item) || !field_access('view', $field, $entity_type, $entity)) {
+ $denied = TRUE;
+ break;
+ }
+
+ // Invoke hook and collect grants/denies for download access.
+ // Default to FALSE and let entities overrule this ruling.
+ $grants = array('system' => FALSE);
+ foreach (module_implements('file_download_access') as $module) {
+ $grants = array_merge($grants, array($module => module_invoke($module, 'file_download_access', $field_item, $entity_type, $entity)));
+ }
+ // Allow other modules to alter the returned grants/denies.
+ drupal_alter('file_download_access', $grants, $field_item, $entity_type, $entity);
+
+ if (in_array(TRUE, $grants)) {
+ // If TRUE is returned, access is granted and no further checks are
+ // necessary.
+ $denied = FALSE;
+ break 3;
+ }
+
+ if (in_array(FALSE, $grants)) {
+ // If an implementation returns FALSE, access to this entity is denied
+ // but the file could belong to another entity to which the user might
+ // have access. Continue with these.
+ $denied = TRUE;
+ }
+ }
+ }
+ }
+
+ // Access specifically denied.
+ if ($denied) {
+ return -1;
+ }
+
+ // Access is granted.
+ $headers = file_get_content_headers($file);
+ return $headers;
+}
+
+/**
+ * Menu callback; Shared Ajax callback for file uploads and deletions.
+ *
+ * This rebuilds the form element for a particular field item. As long as the
+ * form processing is properly encapsulated in the widget element the form
+ * should rebuild correctly using FAPI without the need for additional callbacks
+ * or processing.
+ */
+function file_ajax_upload() {
+ $form_parents = func_get_args();
+ $form_build_id = (string) array_pop($form_parents);
+
+ if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) {
+ // Invalid request.
+ drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+ return array('#type' => 'ajax', '#commands' => $commands);
+ }
+
+ list($form, $form_state) = ajax_get_form();
+
+ if (!$form) {
+ // Invalid form_build_id.
+ drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+ return array('#type' => 'ajax', '#commands' => $commands);
+ }
+
+ // Get the current element and count the number of files.
+ $current_element = $form;
+ foreach ($form_parents as $parent) {
+ $current_element = $current_element[$parent];
+ }
+ $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
+
+ // Process user input. $form and $form_state are modified in the process.
+ drupal_process_form($form['#form_id'], $form, $form_state);
+
+ // Retrieve the element to be rendered.
+ foreach ($form_parents as $parent) {
+ $form = $form[$parent];
+ }
+
+ // Add the special Ajax class if a new file was added.
+ if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
+ $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+ }
+ // Otherwise just add the new content class on a placeholder.
+ else {
+ $form['#suffix'] .= '';
+ }
+
+ $output = theme('status_messages') . drupal_render($form);
+ $js = drupal_add_js();
+ $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
+
+ $commands = array();
+ $commands[] = ajax_command_replace(NULL, $output, $settings);
+ return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
+ * Menu callback for upload progress.
+ *
+ * @param $key
+ * The unique key for this upload process.
+ */
+function file_ajax_progress($key) {
+ $progress = array(
+ 'message' => t('Starting upload...'),
+ 'percentage' => -1,
+ );
+
+ $implementation = file_progress_implementation();
+ if ($implementation == 'uploadprogress') {
+ $status = uploadprogress_get_info($key);
+ if (isset($status['bytes_uploaded']) && !empty($status['bytes_total'])) {
+ $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['bytes_uploaded']), '@total' => format_size($status['bytes_total'])));
+ $progress['percentage'] = round(100 * $status['bytes_uploaded'] / $status['bytes_total']);
+ }
+ }
+ elseif ($implementation == 'apc') {
+ $status = apc_fetch('upload_' . $key);
+ if (isset($status['current']) && !empty($status['total'])) {
+ $progress['message'] = t('Uploading... (@current of @total)', array('@current' => format_size($status['current']), '@total' => format_size($status['total'])));
+ $progress['percentage'] = round(100 * $status['current'] / $status['total']);
+ }
+ }
+
+ drupal_json_output($progress);
+}
+
+/**
+ * Determines the preferred upload progress implementation.
+ *
+ * @return
+ * A string indicating which upload progress system is available. Either "apc"
+ * or "uploadprogress". If neither are available, returns FALSE.
+ */
+function file_progress_implementation() {
+ static $implementation;
+ if (!isset($implementation)) {
+ $implementation = FALSE;
+
+ // We prefer the PECL extension uploadprogress because it supports multiple
+ // simultaneous uploads. APC only supports one at a time.
+ if (extension_loaded('uploadprogress')) {
+ $implementation = 'uploadprogress';
+ }
+ elseif (extension_loaded('apc') && ini_get('apc.rfc1867')) {
+ $implementation = 'apc';
+ }
+ }
+ return $implementation;
+}
+
+/**
+ * Implements hook_file_delete().
+ */
+function file_file_delete($file) {
+ // TODO: Remove references to a file that is in-use.
+}
+
+/**
+ * Process function to expand the managed_file element type.
+ *
+ * Expands the file type to include Upload and Remove buttons, as well as
+ * support for a default value.
+ */
+function file_managed_file_process($element, &$form_state, $form) {
+ $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
+
+ // Set some default element properties.
+ $element['#progress_indicator'] = empty($element['#progress_indicator']) ? 'none' : $element['#progress_indicator'];
+ $element['#file'] = $fid ? file_load($fid) : FALSE;
+ $element['#tree'] = TRUE;
+
+ $ajax_settings = array(
+ 'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
+ 'wrapper' => $element['#id'] . '-ajax-wrapper',
+ 'effect' => 'fade',
+ 'progress' => array(
+ 'type' => $element['#progress_indicator'],
+ 'message' => $element['#progress_message'],
+ ),
+ );
+
+ // Set up the buttons first since we need to check if they were clicked.
+ $element['upload_button'] = array(
+ '#name' => implode('_', $element['#parents']) . '_upload_button',
+ '#type' => 'submit',
+ '#value' => t('Upload'),
+ '#validate' => array(),
+ '#submit' => array('file_managed_file_submit'),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#ajax' => $ajax_settings,
+ '#weight' => -5,
+ );
+
+ // Force the progress indicator for the remove button to be either 'none' or
+ // 'throbber', even if the upload button is using something else.
+ $ajax_settings['progress']['type'] = ($element['#progress_indicator'] == 'none') ? 'none' : 'throbber';
+ $ajax_settings['progress']['message'] = NULL;
+ $ajax_settings['effect'] = 'none';
+ $element['remove_button'] = array(
+ '#name' => implode('_', $element['#parents']) . '_remove_button',
+ '#type' => 'submit',
+ '#value' => t('Remove'),
+ '#validate' => array(),
+ '#submit' => array('file_managed_file_submit'),
+ '#limit_validation_errors' => array($element['#parents']),
+ '#ajax' => $ajax_settings,
+ '#weight' => -5,
+ );
+
+ $element['fid'] = array(
+ '#type' => 'hidden',
+ '#value' => $fid,
+ );
+
+ // Add progress bar support to the upload if possible.
+ if ($element['#progress_indicator'] == 'bar' && $implementation = file_progress_implementation()) {
+ $upload_progress_key = mt_rand();
+
+ if ($implementation == 'uploadprogress') {
+ $element['UPLOAD_IDENTIFIER'] = array(
+ '#type' => 'hidden',
+ '#value' => $upload_progress_key,
+ '#attributes' => array('class' => array('file-progress')),
+ // Uploadprogress extension requires this field to be at the top of the
+ // form.
+ '#weight' => -20,
+ );
+ }
+ elseif ($implementation == 'apc') {
+ $element['APC_UPLOAD_PROGRESS'] = array(
+ '#type' => 'hidden',
+ '#value' => $upload_progress_key,
+ '#attributes' => array('class' => array('file-progress')),
+ // Uploadprogress extension requires this field to be at the top of the
+ // form.
+ '#weight' => -20,
+ );
+ }
+
+ // Add the upload progress callback.
+ $element['upload_button']['#ajax']['progress']['path'] = 'file/progress/' . $upload_progress_key;
+ }
+
+ // The file upload field itself.
+ $element['upload'] = array(
+ '#name' => 'files[' . implode('_', $element['#parents']) . ']',
+ '#type' => 'file',
+ '#title' => t('Choose a file'),
+ '#title_display' => 'invisible',
+ '#size' => $element['#size'],
+ '#theme_wrappers' => array(),
+ '#weight' => -10,
+ );
+
+ if ($fid && $element['#file']) {
+ $element['filename'] = array(
+ '#type' => 'markup',
+ '#markup' => theme('file_link', array('file' => $element['#file'])) . ' ',
+ '#weight' => -10,
+ );
+ }
+
+ // Add the extension list to the page as JavaScript settings.
+ if (isset($element['#upload_validators']['file_validate_extensions'][0])) {
+ $extension_list = implode(',', array_filter(explode(' ', $element['#upload_validators']['file_validate_extensions'][0])));
+ $element['upload']['#attached']['js'] = array(
+ array(
+ 'type' => 'setting',
+ 'data' => array('file' => array('elements' => array('#' . $element['#id'] . '-upload' => $extension_list)))
+ )
+ );
+ }
+
+ // Prefix and suffix used for Ajax replacement.
+ $element['#prefix'] = '
';
+ $element['#suffix'] = '
';
+
+ return $element;
+}
+
+/**
+ * The #value_callback for a managed_file type element.
+ */
+function file_managed_file_value(&$element, $input = FALSE, $form_state = NULL) {
+ $fid = 0;
+
+ // Find the current value of this field from the form state.
+ $form_state_fid = $form_state['values'];
+ foreach ($element['#parents'] as $parent) {
+ $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
+ }
+
+ if ($element['#extended'] && isset($form_state_fid['fid'])) {
+ $fid = $form_state_fid['fid'];
+ }
+ elseif (is_numeric($form_state_fid)) {
+ $fid = $form_state_fid;
+ }
+
+ // Process any input and save new uploads.
+ if ($input !== FALSE) {
+ $return = $input;
+
+ // Uploads take priority over all other values.
+ if ($file = file_managed_file_save_upload($element)) {
+ $fid = $file->fid;
+ }
+ else {
+ // Check for #filefield_value_callback values.
+ // Because FAPI does not allow multiple #value_callback values like it
+ // does for #element_validate and #process, this fills the missing
+ // functionality to allow File fields to be extended through FAPI.
+ if (isset($element['#file_value_callbacks'])) {
+ foreach ($element['#file_value_callbacks'] as $callback) {
+ $callback($element, $input, $form_state);
+ }
+ }
+ // Load file if the FID has changed to confirm it exists.
+ if (isset($input['fid']) && $file = file_load($input['fid'])) {
+ $fid = $file->fid;
+ }
+ }
+ }
+
+ // If there is no input, set the default value.
+ else {
+ if ($element['#extended']) {
+ $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0;
+ $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0);
+ }
+ else {
+ $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0;
+ $return = array('fid' => 0);
+ }
+
+ // Confirm that the file exists when used as a default value.
+ if ($default_fid && $file = file_load($default_fid)) {
+ $fid = $file->fid;
+ }
+ }
+
+ $return['fid'] = $fid;
+
+ return $return;
+}
+
+/**
+ * An #element_validate callback for the managed_file element.
+ */
+function file_managed_file_validate(&$element, &$form_state) {
+ // If referencing an existing file, only allow if there are existing
+ // references. This prevents unmanaged files from being deleted if this
+ // item were to be deleted.
+ $clicked_button = end($form_state['triggering_element']['#parents']);
+ if ($clicked_button != 'remove_button' && !empty($element['fid']['#value'])) {
+ if ($file = file_load($element['fid']['#value'])) {
+ if ($file->status == FILE_STATUS_PERMANENT) {
+ $references = file_usage_list($file);
+ if (empty($references)) {
+ form_error($element, t('The file used in the !name field may not be referenced.', array('!name' => $element['#title'])));
+ }
+ }
+ }
+ else {
+ form_error($element, t('The file referenced by the !name field does not exist.', array('!name' => $element['#title'])));
+ }
+ }
+
+ // Check required property based on the FID.
+ if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('upload_button', 'remove_button'))) {
+ form_error($element['upload'], t('!name field is required.', array('!name' => $element['#title'])));
+ }
+
+ // Consolidate the array value of this field to a single FID.
+ if (!$element['#extended']) {
+ form_set_value($element, $element['fid']['#value'], $form_state);
+ }
+}
+
+/**
+ * Form submission handler for upload / remove buttons of managed_file elements.
+ *
+ * @see file_managed_file_process()
+ */
+function file_managed_file_submit($form, &$form_state) {
+ // Determine whether it was the upload or the remove button that was clicked,
+ // and set $element to the managed_file element that contains that button.
+ $parents = $form_state['triggering_element']['#array_parents'];
+ $button_key = array_pop($parents);
+ $element = drupal_array_get_nested_value($form, $parents);
+
+ // No action is needed here for the upload button, because all file uploads on
+ // the form are processed by file_managed_file_value() regardless of which
+ // button was clicked. Action is needed here for the remove button, because we
+ // only remove a file in response to its remove button being clicked.
+ if ($button_key == 'remove_button') {
+ // If it's a temporary file we can safely remove it immediately, otherwise
+ // it's up to the implementing module to clean up files that are in use.
+ if ($element['#file'] && $element['#file']->status == 0) {
+ file_delete($element['#file']);
+ }
+ // Update both $form_state['values'] and $form_state['input'] to reflect
+ // that the file has been removed, so that the form is rebuilt correctly.
+ // $form_state['values'] must be updated in case additional submit handlers
+ // run, and for form building functions that run during the rebuild, such as
+ // when the managed_file element is part of a field widget.
+ // $form_state['input'] must be updated so that file_managed_file_value()
+ // has correct information during the rebuild.
+ $values_element = $element['#extended'] ? $element['fid'] : $element;
+ form_set_value($values_element, NULL, $form_state);
+ drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL);
+ }
+
+ // Set the form to rebuild so that $form is correctly updated in response to
+ // processing the file removal. Since this function did not change $form_state
+ // if the upload button was clicked, a rebuild isn't necessary in that
+ // situation and setting $form_state['redirect'] to FALSE would suffice.
+ // However, we choose to always rebuild, to keep the form processing workflow
+ // consistent between the two buttons.
+ $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * Saves any files that have been uploaded into a managed_file element.
+ *
+ * @param $element
+ * The FAPI element whose values are being saved.
+ *
+ * @return
+ * The file object representing the file that was saved, or FALSE if no file
+ * was saved.
+ */
+function file_managed_file_save_upload($element) {
+ $upload_name = implode('_', $element['#parents']);
+ if (empty($_FILES['files']['name'][$upload_name])) {
+ return FALSE;
+ }
+
+ $destination = isset($element['#upload_location']) ? $element['#upload_location'] : NULL;
+ if (isset($destination) && !file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) {
+ watchdog('file', 'The upload directory %directory for the file field !name could not be created or is not accessible. A newly uploaded file could not be saved in this directory as a consequence, and the upload was canceled.', array('%directory' => $destination, '!name' => $element['#field_name']));
+ form_set_error($upload_name, t('The file could not be uploaded.'));
+ return FALSE;
+ }
+
+ if (!$file = file_save_upload($upload_name, $element['#upload_validators'], $destination)) {
+ watchdog('file', 'The file upload failed. %upload', array('%upload' => $upload_name));
+ form_set_error($upload_name, t('The file in the !name field was unable to be uploaded.', array('!name' => $element['#title'])));
+ return FALSE;
+ }
+
+ return $file;
+}
+
+/**
+ * Returns HTML for a managed file element.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the file.
+ *
+ * @ingroup themeable
+ */
+function theme_file_managed_file($variables) {
+ $element = $variables['element'];
+
+ $attributes = array();
+ if (isset($element['#id'])) {
+ $attributes['id'] = $element['#id'];
+ }
+ if (!empty($element['#attributes']['class'])) {
+ $attributes['class'] = (array) $element['#attributes']['class'];
+ }
+ $attributes['class'][] = 'form-managed-file';
+
+ // This wrapper is required to apply JS behaviors and CSS styling.
+ $output = '';
+ $output .= '
';
+ return $output;
+}
+
+/**
+ * #pre_render callback to hide display of the upload or remove controls.
+ *
+ * Upload controls are hidden when a file is already uploaded. Remove controls
+ * are hidden when there is no file attached. Controls are hidden here instead
+ * of in file_managed_file_process(), because #access for these buttons depends
+ * on the managed_file element's #value. See the documentation of form_builder()
+ * for more detailed information about the relationship between #process,
+ * #value, and #access.
+ *
+ * Because #access is set here, it affects display only and does not prevent
+ * JavaScript or other untrusted code from submitting the form as though access
+ * were enabled. The form processing functions for these elements should not
+ * assume that the buttons can't be "clicked" just because they are not
+ * displayed.
+ *
+ * @see file_managed_file_process()
+ * @see form_builder()
+ */
+function file_managed_file_pre_render($element) {
+ // If we already have a file, we don't want to show the upload controls.
+ if (!empty($element['#value']['fid'])) {
+ $element['upload']['#access'] = FALSE;
+ $element['upload_button']['#access'] = FALSE;
+ }
+ // If we don't already have a file, there is nothing to remove.
+ else {
+ $element['remove_button']['#access'] = FALSE;
+ }
+ return $element;
+}
+
+/**
+ * Returns HTML for a link to a file.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - file: A file object to which the link will be created.
+ * - icon_directory: (optional) A path to a directory of icons to be used for
+ * files. Defaults to the value of the "file_icon_directory" variable.
+ *
+ * @ingroup themeable
+ */
+function theme_file_link($variables) {
+ $file = $variables['file'];
+ $icon_directory = $variables['icon_directory'];
+
+ $url = file_create_url($file->uri);
+ $icon = theme('file_icon', array('file' => $file, 'icon_directory' => $icon_directory));
+
+ // Set options as per anchor format described at
+ // http://microformats.org/wiki/file-format-examples
+ $options = array(
+ 'attributes' => array(
+ 'type' => $file->filemime . '; length=' . $file->filesize,
+ ),
+ );
+
+ // Use the description as the link text if available.
+ if (empty($file->description)) {
+ $link_text = $file->filename;
+ }
+ else {
+ $link_text = $file->description;
+ $options['attributes']['title'] = check_plain($file->filename);
+ }
+
+ return '' . $icon . ' ' . l($link_text, $url, $options) . '';
+}
+
+/**
+ * Returns HTML for an image with an appropriate icon for the given file.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - file: A file object for which to make an icon.
+ * - icon_directory: (optional) A path to a directory of icons to be used for
+ * files. Defaults to the value of the "file_icon_directory" variable.
+ *
+ * @ingroup themeable
+ */
+function theme_file_icon($variables) {
+ $file = $variables['file'];
+ $icon_directory = $variables['icon_directory'];
+
+ $mime = check_plain($file->filemime);
+ $icon_url = file_icon_url($file, $icon_directory);
+ return '';
+}
+
+/**
+ * Creates a URL to the icon for a file object.
+ *
+ * @param $file
+ * A file object.
+ * @param $icon_directory
+ * (optional) A path to a directory of icons to be used for files. Defaults to
+ * the value of the "file_icon_directory" variable.
+ *
+ * @return
+ * A URL string to the icon, or FALSE if an appropriate icon cannot be found.
+ */
+function file_icon_url($file, $icon_directory = NULL) {
+ if ($icon_path = file_icon_path($file, $icon_directory)) {
+ return base_path() . $icon_path;
+ }
+ return FALSE;
+}
+
+/**
+ * Creates a path to the icon for a file object.
+ *
+ * @param $file
+ * A file object.
+ * @param $icon_directory
+ * (optional) A path to a directory of icons to be used for files. Defaults to
+ * the value of the "file_icon_directory" variable.
+ *
+ * @return
+ * A string to the icon as a local path, or FALSE if an appropriate icon could
+ * not be found.
+ */
+function file_icon_path($file, $icon_directory = NULL) {
+ // Use the default set of icons if none specified.
+ if (!isset($icon_directory)) {
+ $icon_directory = variable_get('file_icon_directory', drupal_get_path('module', 'file') . '/icons');
+ }
+
+ // If there's an icon matching the exact mimetype, go for it.
+ $dashed_mime = strtr($file->filemime, array('/' => '-'));
+ $icon_path = $icon_directory . '/' . $dashed_mime . '.png';
+ if (file_exists($icon_path)) {
+ return $icon_path;
+ }
+
+ // For a few mimetypes, we can "manually" map to a generic icon.
+ $generic_mime = (string) file_icon_map($file);
+ $icon_path = $icon_directory . '/' . $generic_mime . '.png';
+ if ($generic_mime && file_exists($icon_path)) {
+ return $icon_path;
+ }
+
+ // Use generic icons for each category that provides such icons.
+ foreach (array('audio', 'image', 'text', 'video') as $category) {
+ if (strpos($file->filemime, $category . '/') === 0) {
+ $icon_path = $icon_directory . '/' . $category . '-x-generic.png';
+ if (file_exists($icon_path)) {
+ return $icon_path;
+ }
+ }
+ }
+
+ // Try application-octet-stream as last fallback.
+ $icon_path = $icon_directory . '/application-octet-stream.png';
+ if (file_exists($icon_path)) {
+ return $icon_path;
+ }
+
+ // No icon can be found.
+ return FALSE;
+}
+
+/**
+ * Determines the generic icon MIME package based on a file's MIME type.
+ *
+ * @param $file
+ * A file object.
+ *
+ * @return
+ * The generic icon MIME package expected for this file.
+ */
+function file_icon_map($file) {
+ switch ($file->filemime) {
+ // Word document types.
+ case 'application/msword':
+ case 'application/vnd.ms-word.document.macroEnabled.12':
+ case 'application/vnd.oasis.opendocument.text':
+ case 'application/vnd.oasis.opendocument.text-template':
+ case 'application/vnd.oasis.opendocument.text-master':
+ case 'application/vnd.oasis.opendocument.text-web':
+ case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
+ case 'application/vnd.stardivision.writer':
+ case 'application/vnd.sun.xml.writer':
+ case 'application/vnd.sun.xml.writer.template':
+ case 'application/vnd.sun.xml.writer.global':
+ case 'application/vnd.wordperfect':
+ case 'application/x-abiword':
+ case 'application/x-applix-word':
+ case 'application/x-kword':
+ case 'application/x-kword-crypt':
+ return 'x-office-document';
+
+ // Spreadsheet document types.
+ case 'application/vnd.ms-excel':
+ case 'application/vnd.ms-excel.sheet.macroEnabled.12':
+ case 'application/vnd.oasis.opendocument.spreadsheet':
+ case 'application/vnd.oasis.opendocument.spreadsheet-template':
+ case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
+ case 'application/vnd.stardivision.calc':
+ case 'application/vnd.sun.xml.calc':
+ case 'application/vnd.sun.xml.calc.template':
+ case 'application/vnd.lotus-1-2-3':
+ case 'application/x-applix-spreadsheet':
+ case 'application/x-gnumeric':
+ case 'application/x-kspread':
+ case 'application/x-kspread-crypt':
+ return 'x-office-spreadsheet';
+
+ // Presentation document types.
+ case 'application/vnd.ms-powerpoint':
+ case 'application/vnd.ms-powerpoint.presentation.macroEnabled.12':
+ case 'application/vnd.oasis.opendocument.presentation':
+ case 'application/vnd.oasis.opendocument.presentation-template':
+ case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
+ case 'application/vnd.stardivision.impress':
+ case 'application/vnd.sun.xml.impress':
+ case 'application/vnd.sun.xml.impress.template':
+ case 'application/x-kpresenter':
+ return 'x-office-presentation';
+
+ // Compressed archive types.
+ case 'application/zip':
+ case 'application/x-zip':
+ case 'application/stuffit':
+ case 'application/x-stuffit':
+ case 'application/x-7z-compressed':
+ case 'application/x-ace':
+ case 'application/x-arj':
+ case 'application/x-bzip':
+ case 'application/x-bzip-compressed-tar':
+ case 'application/x-compress':
+ case 'application/x-compressed-tar':
+ case 'application/x-cpio-compressed':
+ case 'application/x-deb':
+ case 'application/x-gzip':
+ case 'application/x-java-archive':
+ case 'application/x-lha':
+ case 'application/x-lhz':
+ case 'application/x-lzop':
+ case 'application/x-rar':
+ case 'application/x-rpm':
+ case 'application/x-tzo':
+ case 'application/x-tar':
+ case 'application/x-tarz':
+ case 'application/x-tgz':
+ return 'package-x-generic';
+
+ // Script file types.
+ case 'application/ecmascript':
+ case 'application/javascript':
+ case 'application/mathematica':
+ case 'application/vnd.mozilla.xul+xml':
+ case 'application/x-asp':
+ case 'application/x-awk':
+ case 'application/x-cgi':
+ case 'application/x-csh':
+ case 'application/x-m4':
+ case 'application/x-perl':
+ case 'application/x-php':
+ case 'application/x-ruby':
+ case 'application/x-shellscript':
+ case 'text/vnd.wap.wmlscript':
+ case 'text/x-emacs-lisp':
+ case 'text/x-haskell':
+ case 'text/x-literate-haskell':
+ case 'text/x-lua':
+ case 'text/x-makefile':
+ case 'text/x-matlab':
+ case 'text/x-python':
+ case 'text/x-sql':
+ case 'text/x-tcl':
+ return 'text-x-script';
+
+ // HTML aliases.
+ case 'application/xhtml+xml':
+ return 'text-html';
+
+ // Executable types.
+ case 'application/x-macbinary':
+ case 'application/x-ms-dos-executable':
+ case 'application/x-pef-executable':
+ return 'application-x-executable';
+
+ default:
+ return FALSE;
+ }
+}
+
+/**
+ * @defgroup file-module-api File module public API functions
+ * @{
+ * These functions may be used to determine if and where a file is in use.
+ */
+
+/**
+ * Retrieves a list of references to a file.
+ *
+ * @param $file
+ * A file object.
+ * @param $field
+ * (optional) A field array to be used for this check. If given, limits the
+ * reference check to the given field.
+ * @param $age
+ * (optional) A constant that specifies which references to count. Use
+ * FIELD_LOAD_REVISION to retrieve all references within all revisions or
+ * FIELD_LOAD_CURRENT to retrieve references only in the current revisions.
+ * @param $field_type
+ * (optional) The name of a field type. If given, limits the reference check
+ * to fields of the given type.
+ *
+ * @return
+ * An integer value.
+ */
+function file_get_file_references($file, $field = NULL, $age = FIELD_LOAD_REVISION, $field_type = 'file') {
+ $references = drupal_static(__FUNCTION__, array());
+ $fields = isset($field) ? array($field['field_name'] => $field) : field_info_fields();
+
+ foreach ($fields as $field_name => $file_field) {
+ if ((empty($field_type) || $file_field['type'] == $field_type) && !isset($references[$field_name])) {
+ // Get each time this file is used within a field.
+ $query = new EntityFieldQuery();
+ $query
+ ->fieldCondition($file_field, 'fid', $file->fid)
+ ->age($age);
+ $references[$field_name] = $query->execute();
+ }
+ }
+
+ return isset($field) ? $references[$field['field_name']] : array_filter($references);
+}
+
+/**
+ * @} End of "defgroup file-module-api".
+ */
diff --git a/drupal-dev/modules/file/icons/application-octet-stream.png b/drupal-dev/modules/file/icons/application-octet-stream.png
new file mode 100644
index 0000000000000000000000000000000000000000..d5453217dc5cc30e805d3d0da8fa91e5a0684b86
GIT binary patch
literal 189
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6BuiW)N`mv#O3D+9QW+dm@{>{(
zJaZG%Q-e|yQz{EjrrH1%#e2FqhG+zzJ-d;!!9l>~;>xRRItLPdn;40odGgNTFWaMo
z@1hz$UuIWs>A8|(q_~9L#B04>N(sX=If;bjynH&g_hvsjaH~4qVneIY4QI|P0Zi^1
ljW)lfGh!A#J6O(fSJQPye(j97H9)HvJYD@<);T3K0RUpoL@EFP
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/application-pdf.png b/drupal-dev/modules/file/icons/application-pdf.png
new file mode 100644
index 0000000000000000000000000000000000000000..36107d6e804015e13d122c53cb035d33632678d2
GIT binary patch
literal 346
zcmV-g0j2(lP)La-N5`rLZPA(1#E-uBP;35>7mbRp6V}6(B
zxT4lv`iCzikbIyi{Q#h{EDII-mgRekb58(c43yGRNs>GwUs@m^+qO%|$q)WL@3K!V
z;5g1}1@I8n0
(zm5zgL_2`K;FIZug6JyK<450fiioh$j<32(aZc2h>-%U#~`>7&@PWUoHyp
sTP;<(-4=lMXA(mEo7|%ZD)t2Y023%F88v7N^Z)<=07*qoM6N<$f}Na{Gynhq
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/application-x-executable.png b/drupal-dev/modules/file/icons/application-x-executable.png
new file mode 100644
index 0000000000000000000000000000000000000000..d5453217dc5cc30e805d3d0da8fa91e5a0684b86
GIT binary patch
literal 189
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6BuiW)N`mv#O3D+9QW+dm@{>{(
zJaZG%Q-e|yQz{EjrrH1%#e2FqhG+zzJ-d;!!9l>~;>xRRItLPdn;40odGgNTFWaMo
z@1hz$UuIWs>A8|(q_~9L#B04>N(sX=If;bjynH&g_hvsjaH~4qVneIY4QI|P0Zi^1
ljW)lfGh!A#J6O(fSJQPye(j97H9)HvJYD@<);T3K0RUpoL@EFP
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/audio-x-generic.png b/drupal-dev/modules/file/icons/audio-x-generic.png
new file mode 100644
index 0000000000000000000000000000000000000000..28d7f50862b5dbb153c0809e9119fd879d499788
GIT binary patch
literal 314
zcmV-A0mc4_P)5%T71{KX7x!|A(({5NE)j
zKYzd&q!`9V79(f?48Rm)6QjQYVbB1LUL;A_0Q3+Cf#UU6|05QN{GWC>hp+*`b9w(~
ztkn8{>(wc^0e}Af`v3jc=l}oy{l#wpC^P;0_YW@ysljOgDHx^?00QtBn;M+ki2wiq
M07*qoM6N<$f-Y{2(f|Me
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/image-x-generic.png b/drupal-dev/modules/file/icons/image-x-generic.png
new file mode 100644
index 0000000000000000000000000000000000000000..c1b814f7cb6f4a21e165e88db557ffe4692babad
GIT binary patch
literal 385
zcmV-{0e=38P)FC&>@3L6hucw#aRRw7eSoFNmQtbU{mn3
zG)bWvQA}=a@|t^w&{~q{frt0-Cij6$A^;#HNx~n<`Uo5VM1z1a2HUoqa2%&zG8-!3
zQ$Fi&f{(mN&$+<$nGO|v-Y>&tI*0bR>mlk_mKv^NhYGy!3Xfp_$dBUJkR)7CBSEm+
z&tvt(3u@Myf6#wou4^f%sv0f8
z*HjeoSE!n4RLf~FV*++@oRT_|x_E(p+6OB)jCW;(vT`wi(n&A<53g5)@q(wrBp&v=
zpzVk#>~-StpqsjvqZDPVASbuub|ZoOwg_!k#NB2GZdR#-x}Jy^TxQ#%h!!x$=sQP1
f?}Q5eK_K`6d{(
zJaZG%Q-e|yQz{EjrrH1%E%S774ABVg?Kk8*WFWxuer-(yd%zv$fH&S%}%zr56N`Hooj};y7~CyD5-EIZ$Ti#TlGiAKrWr
zUu>K2@!d{$zmi_R^=IcFiyv;gVvwM;GSR3l=7-;%YNpF=9hs+p|Ck1JF@vY8pUXO@
GgeCy8++)N5
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/text-html.png b/drupal-dev/modules/file/icons/text-html.png
new file mode 100644
index 0000000000000000000000000000000000000000..9c7c7932c25ad93adc9c9e6962d984d1703cd17f
GIT binary patch
literal 265
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`7d%}YLn?0dPCn1oYpr_l6|v$g!-?M+tzwem(bxqx`p9mQA+2tMA_+XME>nQ%bo@!}(>I4c6S;asB4{>%yglS|#6Y
z5Uy)3sXwGNKj!eva0TWHDFLV02B&~;yBsuQ8|JUfy)AI#BlodKT>FxH4HmB4HXGzc
N22WQ%mvv4FO#pgZX`cW9
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/text-plain.png b/drupal-dev/modules/file/icons/text-plain.png
new file mode 100644
index 0000000000000000000000000000000000000000..06804849b8331ed8be3d1ae089311ae58ea79c83
GIT binary patch
literal 220
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6BuiW)N`mv#O3D+9QW+dm@{>{(
zJaZG%Q-e|yQz{EjrrH1%)p@!&hG+y&?K{ZXV8G*Qyqephp*`o3)VoI~yA?RN7G071
zlASBP=H*eFjEv7eG#edwT#p`klJw&vJA1DcCF&6fEwgh&R?%&s{
zX3)B>#qoyRCeH3BDMHWqy{(
zJaZG%Q-e|yQz{EjrrH1%)p@!&hG+y&?K{ZXV8G*Qyqephp*`o3)VoI~yA?RN7G071
zlASBP=H*eFjEv7eG#edwT#p`klJw&vJA1DcCF&6fEwgh&R?%&s{
zX3)B>#qoyRCeH3BDMHWqy{(
zJaZG%Q-e|yQz{EjrrH1%Rd~8MhG?9hI>C^S$&iQTZ$24Q_1|1vj(@wU*2iN@EyC7x_*Z;E$EtTTO(#;cD9cA8~Z#ce1>0B1j4Gf;H
KelF{r5}E*g{z{zy
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/x-office-document.png b/drupal-dev/modules/file/icons/x-office-document.png
new file mode 100644
index 0000000000000000000000000000000000000000..40db538fcb71e1f46147a717cdf7134c4e74a239
GIT binary patch
literal 196
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`-JULvAr-f-PIlyCR^)ND&DOv0
zRA<)*E4jtr1@CkUUb(^(64aDzym=OXf6k=i_m3z%>f0*z|H0LTcMHEOpRi2!@?>C$
z`80=1Bdwad>ct1^?B|7U2RwrtzsDUgN(pW(&J%E2aPlH&QV-Kq28MN}8cR7snxaU19foyFkk>gTe~DWM4f@k31#
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/icons/x-office-presentation.png b/drupal-dev/modules/file/icons/x-office-presentation.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb119e5ba91dd5141e07aad5229754cd06401c99
GIT binary patch
literal 181
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`HJ&bxAr-ftPCm%hpup2sdo6B3
zamj~9)_g=N=}tt*d2P=kD~`cESoNx@a=l5_Qo_IANDkK;`k3KG}0GMXI`*i$b2WRBvo8xw*i
zO$rqacvhQr;DXY8_Dwc|9*eX-+%rG1!AI2Mc~uXm6NkyC*mL@(8cS>LFnm7E_Gf0+
hTH6=BdJ?W&er*c!pDfOvX9v23!PC{xWt~$(696`_L`MJs
literal 0
HcmV?d00001
diff --git a/drupal-dev/modules/file/tests/file.test b/drupal-dev/modules/file/tests/file.test
new file mode 100644
index 0000000..69e711a
--- /dev/null
+++ b/drupal-dev/modules/file/tests/file.test
@@ -0,0 +1,1171 @@
+admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer nodes', 'bypass node access'));
+ $this->drupalLogin($this->admin_user);
+ }
+
+ /**
+ * Retrieves a sample file of the specified type.
+ */
+ function getTestFile($type_name, $size = NULL) {
+ // Get a file to upload.
+ $file = current($this->drupalGetTestFiles($type_name, $size));
+
+ // Add a filesize property to files as would be read by file_load().
+ $file->filesize = filesize($file->uri);
+
+ return $file;
+ }
+
+ /**
+ * Retrieves the fid of the last inserted file.
+ */
+ function getLastFileId() {
+ return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField();
+ }
+
+ /**
+ * Creates a new file field.
+ *
+ * @param $name
+ * The name of the new field (all lowercase), exclude the "field_" prefix.
+ * @param $type_name
+ * The node type that this field will be added to.
+ * @param $field_settings
+ * A list of field settings that will be added to the defaults.
+ * @param $instance_settings
+ * A list of instance settings that will be added to the instance defaults.
+ * @param $widget_settings
+ * A list of widget settings that will be added to the widget defaults.
+ */
+ function createFileField($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) {
+ $field = array(
+ 'field_name' => $name,
+ 'type' => 'file',
+ 'settings' => array(),
+ 'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1,
+ );
+ $field['settings'] = array_merge($field['settings'], $field_settings);
+ field_create_field($field);
+
+ $this->attachFileField($name, 'node', $type_name, $instance_settings, $widget_settings);
+ }
+
+ /**
+ * Attaches a file field to an entity.
+ *
+ * @param $name
+ * The name of the new field (all lowercase), exclude the "field_" prefix.
+ * @param $entity_type
+ * The entity type this field will be added to.
+ * @param $bundle
+ * The bundle this field will be added to.
+ * @param $field_settings
+ * A list of field settings that will be added to the defaults.
+ * @param $instance_settings
+ * A list of instance settings that will be added to the instance defaults.
+ * @param $widget_settings
+ * A list of widget settings that will be added to the widget defaults.
+ */
+ function attachFileField($name, $entity_type, $bundle, $instance_settings = array(), $widget_settings = array()) {
+ $instance = array(
+ 'field_name' => $name,
+ 'label' => $name,
+ 'entity_type' => $entity_type,
+ 'bundle' => $bundle,
+ 'required' => !empty($instance_settings['required']),
+ 'settings' => array(),
+ 'widget' => array(
+ 'type' => 'file_generic',
+ 'settings' => array(),
+ ),
+ );
+ $instance['settings'] = array_merge($instance['settings'], $instance_settings);
+ $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings);
+ field_create_instance($instance);
+ }
+
+ /**
+ * Updates an existing file field with new settings.
+ */
+ function updateFileField($name, $type_name, $instance_settings = array(), $widget_settings = array()) {
+ $instance = field_info_instance('node', $name, $type_name);
+ $instance['settings'] = array_merge($instance['settings'], $instance_settings);
+ $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings);
+
+ field_update_instance($instance);
+ }
+
+ /**
+ * Uploads a file to a node.
+ */
+ function uploadNodeFile($file, $field_name, $nid_or_type, $new_revision = TRUE, $extras = array()) {
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $this->randomName(),
+ 'revision' => (string) (int) $new_revision,
+ );
+
+ if (is_numeric($nid_or_type)) {
+ $nid = $nid_or_type;
+ }
+ else {
+ // Add a new node.
+ $extras['type'] = $nid_or_type;
+ $node = $this->drupalCreateNode($extras);
+ $nid = $node->nid;
+ // Save at least one revision to better simulate a real site.
+ $this->drupalCreateNode(get_object_vars($node));
+ $node = node_load($nid, NULL, TRUE);
+ $this->assertNotEqual($nid, $node->vid, 'Node revision exists.');
+ }
+
+ // Attach a file to the node.
+ $edit['files[' . $field_name . '_' . $langcode . '_0]'] = drupal_realpath($file->uri);
+ $this->drupalPost("node/$nid/edit", $edit, t('Save'));
+
+ return $nid;
+ }
+
+ /**
+ * Removes a file from a node.
+ *
+ * Note that if replacing a file, it must first be removed then added again.
+ */
+ function removeNodeFile($nid, $new_revision = TRUE) {
+ $edit = array(
+ 'revision' => (string) (int) $new_revision,
+ );
+
+ $this->drupalPost('node/' . $nid . '/edit', array(), t('Remove'));
+ $this->drupalPost(NULL, $edit, t('Save'));
+ }
+
+ /**
+ * Replaces a file within a node.
+ */
+ function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) {
+ $edit = array(
+ 'files[' . $field_name . '_' . LANGUAGE_NONE . '_0]' => drupal_realpath($file->uri),
+ 'revision' => (string) (int) $new_revision,
+ );
+
+ $this->drupalPost('node/' . $nid . '/edit', array(), t('Remove'));
+ $this->drupalPost(NULL, $edit, t('Save'));
+ }
+
+ /**
+ * Asserts that a file exists physically on disk.
+ */
+ function assertFileExists($file, $message = NULL) {
+ $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri));
+ $this->assertTrue(is_file($file->uri), $message);
+ }
+
+ /**
+ * Asserts that a file exists in the database.
+ */
+ function assertFileEntryExists($file, $message = NULL) {
+ entity_get_controller('file')->resetCache();
+ $db_file = file_load($file->fid);
+ $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri));
+ $this->assertEqual($db_file->uri, $file->uri, $message);
+ }
+
+ /**
+ * Asserts that a file does not exist on disk.
+ */
+ function assertFileNotExists($file, $message = NULL) {
+ $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri));
+ $this->assertFalse(is_file($file->uri), $message);
+ }
+
+ /**
+ * Asserts that a file does not exist in the database.
+ */
+ function assertFileEntryNotExists($file, $message) {
+ entity_get_controller('file')->resetCache();
+ $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri));
+ $this->assertFalse(file_load($file->fid), $message);
+ }
+
+ /**
+ * Asserts that a file's status is set to permanent in the database.
+ */
+ function assertFileIsPermanent($file, $message = NULL) {
+ $message = isset($message) ? $message : format_string('File %file is permanent.', array('%file' => $file->uri));
+ $this->assertTrue($file->status == FILE_STATUS_PERMANENT, $message);
+ }
+}
+
+/**
+ * Tests the 'managed_file' element type.
+ *
+ * @todo Create a FileTestCase base class and move FileFieldTestCase methods
+ * that aren't related to fields into it.
+ */
+class FileManagedFileElementTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Managed file element test',
+ 'description' => 'Tests the managed_file element type.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests the managed_file element type.
+ */
+ function testManagedFile() {
+ // Check that $element['#size'] is passed to the child upload element.
+ $this->drupalGet('file/test');
+ $this->assertFieldByXpath('//input[@name="files[nested_file]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.');
+
+ // Perform the tests with all permutations of $form['#tree'] and
+ // $element['#extended'].
+ foreach (array(0, 1) as $tree) {
+ foreach (array(0, 1) as $extended) {
+ $test_file = $this->getTestFile('text');
+ $path = 'file/test/' . $tree . '/' . $extended;
+ $input_base_name = $tree ? 'nested_file' : 'file';
+
+ // Submit without a file.
+ $this->drupalPost($path, array(), t('Save'));
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submitted without a file.');
+
+ // Submit a new file, without using the Upload button.
+ $last_fid_prior = $this->getLastFileId();
+ $edit = array('files[' . $input_base_name . ']' => drupal_realpath($test_file->uri));
+ $this->drupalPost($path, $edit, t('Save'));
+ $last_fid = $this->getLastFileId();
+ $this->assertTrue($last_fid > $last_fid_prior, 'New file got saved.');
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Submit handler has correct file info.');
+
+ // Submit no new input, but with a default file.
+ $this->drupalPost($path . '/' . $last_fid, array(), t('Save'));
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Empty submission did not change an existing file.');
+
+ // Now, test the Upload and Remove buttons, with and without Ajax.
+ foreach (array(FALSE, TRUE) as $ajax) {
+ // Upload, then Submit.
+ $last_fid_prior = $this->getLastFileId();
+ $this->drupalGet($path);
+ $edit = array('files[' . $input_base_name . ']' => drupal_realpath($test_file->uri));
+ if ($ajax) {
+ $this->drupalPostAJAX(NULL, $edit, $input_base_name . '_upload_button');
+ }
+ else {
+ $this->drupalPost(NULL, $edit, t('Upload'));
+ }
+ $last_fid = $this->getLastFileId();
+ $this->assertTrue($last_fid > $last_fid_prior, 'New file got uploaded.');
+ $this->drupalPost(NULL, array(), t('Save'));
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => $last_fid)), 'Submit handler has correct file info.');
+
+ // Remove, then Submit.
+ $this->drupalGet($path . '/' . $last_fid);
+ if ($ajax) {
+ $this->drupalPostAJAX(NULL, array(), $input_base_name . '_remove_button');
+ }
+ else {
+ $this->drupalPost(NULL, array(), t('Remove'));
+ }
+ $this->drupalPost(NULL, array(), t('Save'));
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submission after file removal was successful.');
+
+ // Upload, then Remove, then Submit.
+ $this->drupalGet($path);
+ $edit = array('files[' . $input_base_name . ']' => drupal_realpath($test_file->uri));
+ if ($ajax) {
+ $this->drupalPostAJAX(NULL, $edit, $input_base_name . '_upload_button');
+ $this->drupalPostAJAX(NULL, array(), $input_base_name . '_remove_button');
+ }
+ else {
+ $this->drupalPost(NULL, $edit, t('Upload'));
+ $this->drupalPost(NULL, array(), t('Remove'));
+ }
+ $this->drupalPost(NULL, array(), t('Save'));
+ $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submission after file upload and removal was successful.');
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Tests file field widget.
+ */
+class FileFieldWidgetTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field widget test',
+ 'description' => 'Tests the file field widget, single and multi-valued, with and without AJAX, with public and private files.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests upload and remove buttons for a single-valued File field.
+ */
+ function testSingleValuedWidget() {
+ // Use 'page' instead of 'article', so that the 'article' image field does
+ // not conflict with this test. If in the future the 'page' type gets its
+ // own default file or image field, this test can be made more robust by
+ // using a custom node type.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('text');
+
+ foreach (array('nojs', 'js') as $type) {
+ // Create a new node with the uploaded file and ensure it got uploaded
+ // successfully.
+ // @todo This only tests a 'nojs' submission, because drupalPostAJAX()
+ // does not yet support file uploads.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'New file saved to disk on node creation.');
+
+ // Ensure the file can be downloaded.
+ $this->drupalGet(file_create_url($node_file->uri));
+ $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
+
+ // Ensure the edit page has a remove button instead of an upload button.
+ $this->drupalGet("node/$nid/edit");
+ $this->assertNoFieldByXPath('//input[@type="submit"]', t('Upload'), 'Node with file does not display the "Upload" button.');
+ $this->assertFieldByXpath('//input[@type="submit"]', t('Remove'), 'Node with file displays the "Remove" button.');
+
+ // "Click" the remove button (emulating either a nojs or js submission).
+ switch ($type) {
+ case 'nojs':
+ $this->drupalPost(NULL, array(), t('Remove'));
+ break;
+ case 'js':
+ $button = $this->xpath('//input[@type="submit" and @value="' . t('Remove') . '"]');
+ $this->drupalPostAJAX(NULL, array(), array((string) $button[0]['name'] => (string) $button[0]['value']));
+ break;
+ }
+
+ // Ensure the page now has an upload button instead of a remove button.
+ $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), 'After clicking the "Remove" button, it is no longer displayed.');
+ $this->assertFieldByXpath('//input[@type="submit"]', t('Upload'), 'After clicking the "Remove" button, the "Upload" button is displayed.');
+
+ // Save the node and ensure it does not have the file.
+ $this->drupalPost(NULL, array(), t('Save'));
+ $node = node_load($nid, NULL, TRUE);
+ $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), 'File was successfully removed from the node.');
+ }
+ }
+
+ /**
+ * Tests upload and remove buttons for multiple multi-valued File fields.
+ */
+ function testMultiValuedWidget() {
+ // Use 'page' instead of 'article', so that the 'article' image field does
+ // not conflict with this test. If in the future the 'page' type gets its
+ // own default file or image field, this test can be made more robust by
+ // using a custom node type.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $field_name2 = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name, array('cardinality' => 3));
+ $this->createFileField($field_name2, $type_name, array('cardinality' => 3));
+
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $field2 = field_info_field($field_name2);
+ $instance2 = field_info_instance('node', $field_name2, $type_name);
+
+ $test_file = $this->getTestFile('text');
+
+ foreach (array('nojs', 'js') as $type) {
+ // Visit the node creation form, and upload 3 files for each field. Since
+ // the field has cardinality of 3, ensure the "Upload" button is displayed
+ // until after the 3rd file, and after that, isn't displayed. Because
+ // SimpleTest triggers the last button with a given name, so upload to the
+ // second field first.
+ // @todo This is only testing a non-Ajax upload, because drupalPostAJAX()
+ // does not yet emulate jQuery's file upload.
+ //
+ $this->drupalGet("node/add/$type_name");
+ foreach (array($field_name2, $field_name) as $each_field_name) {
+ for ($delta = 0; $delta < 3; $delta++) {
+ $edit = array('files[' . $each_field_name . '_' . LANGUAGE_NONE . '_' . $delta . ']' => drupal_realpath($test_file->uri));
+ // If the Upload button doesn't exist, drupalPost() will automatically
+ // fail with an assertion message.
+ $this->drupalPost(NULL, $edit, t('Upload'));
+ }
+ }
+ $this->assertNoFieldByXpath('//input[@type="submit"]', t('Upload'), 'After uploading 3 files for each field, the "Upload" button is no longer displayed.');
+
+ $num_expected_remove_buttons = 6;
+
+ foreach (array($field_name, $field_name2) as $current_field_name) {
+ // How many uploaded files for the current field are remaining.
+ $remaining = 3;
+ // Test clicking each "Remove" button. For extra robustness, test them out
+ // of sequential order. They are 0-indexed, and get renumbered after each
+ // iteration, so array(1, 1, 0) means:
+ // - First remove the 2nd file.
+ // - Then remove what is then the 2nd file (was originally the 3rd file).
+ // - Then remove the first file.
+ foreach (array(1,1,0) as $delta) {
+ // Ensure we have the expected number of Remove buttons, and that they
+ // are numbered sequentially.
+ $buttons = $this->xpath('//input[@type="submit" and @value="Remove"]');
+ $this->assertTrue(is_array($buttons) && count($buttons) === $num_expected_remove_buttons, format_string('There are %n "Remove" buttons displayed (JSMode=%type).', array('%n' => $num_expected_remove_buttons, '%type' => $type)));
+ foreach ($buttons as $i => $button) {
+ $key = $i >= $remaining ? $i - $remaining : $i;
+ $check_field_name = $field_name2;
+ if ($current_field_name == $field_name && $i < $remaining) {
+ $check_field_name = $field_name;
+ }
+
+ $this->assertIdentical((string) $button['name'], $check_field_name . '_' . LANGUAGE_NONE . '_' . $key. '_remove_button');
+ }
+
+ // "Click" the remove button (emulating either a nojs or js submission).
+ $button_name = $current_field_name . '_' . LANGUAGE_NONE . '_' . $delta . '_remove_button';
+ switch ($type) {
+ case 'nojs':
+ // drupalPost() takes a $submit parameter that is the value of the
+ // button whose click we want to emulate. Since we have multiple
+ // buttons with the value "Remove", and want to control which one we
+ // use, we change the value of the other ones to something else.
+ // Since non-clicked buttons aren't included in the submitted POST
+ // data, and since drupalPost() will result in $this being updated
+ // with a newly rebuilt form, this doesn't cause problems.
+ foreach ($buttons as $button) {
+ if ($button['name'] != $button_name) {
+ $button['value'] = 'DUMMY';
+ }
+ }
+ $this->drupalPost(NULL, array(), t('Remove'));
+ break;
+ case 'js':
+ // drupalPostAJAX() lets us target the button precisely, so we don't
+ // require the workaround used above for nojs.
+ $this->drupalPostAJAX(NULL, array(), array($button_name => t('Remove')));
+ break;
+ }
+ $num_expected_remove_buttons--;
+ $remaining--;
+
+ // Ensure an "Upload" button for the current field is displayed with the
+ // correct name.
+ $upload_button_name = $current_field_name . '_' . LANGUAGE_NONE . '_' . $remaining . '_upload_button';
+ $buttons = $this->xpath('//input[@type="submit" and @value="Upload" and @name=:name]', array(':name' => $upload_button_name));
+ $this->assertTrue(is_array($buttons) && count($buttons) == 1, format_string('The upload button is displayed with the correct name (JSMode=%type).', array('%type' => $type)));
+
+ // Ensure only at most one button per field is displayed.
+ $buttons = $this->xpath('//input[@type="submit" and @value="Upload"]');
+ $expected = $current_field_name == $field_name ? 1 : 2;
+ $this->assertTrue(is_array($buttons) && count($buttons) == $expected, format_string('After removing a file, only one "Upload" button for each possible field is displayed (JSMode=%type).', array('%type' => $type)));
+ }
+ }
+
+ // Ensure the page now has no Remove buttons.
+ $this->assertNoFieldByXPath('//input[@type="submit"]', t('Remove'), format_string('After removing all files, there is no "Remove" button displayed (JSMode=%type).', array('%type' => $type)));
+
+ // Save the node and ensure it does not have any files.
+ $this->drupalPost(NULL, array('title' => $this->randomName()), t('Save'));
+ $matches = array();
+ preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
+ $nid = $matches[1];
+ $node = node_load($nid, NULL, TRUE);
+ $this->assertTrue(empty($node->{$field_name}[LANGUAGE_NONE][0]['fid']), 'Node was successfully saved without any files.');
+ }
+ }
+
+ /**
+ * Tests a file field with a "Private files" upload destination setting.
+ */
+ function testPrivateFileSetting() {
+ // Use 'page' instead of 'article', so that the 'article' image field does
+ // not conflict with this test. If in the future the 'page' type gets its
+ // own default file or image field, this test can be made more robust by
+ // using a custom node type.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('text');
+
+ // Change the field setting to make its files private, and upload a file.
+ $edit = array('field[settings][uri_scheme]' => 'private');
+ $this->drupalPost("admin/structure/types/manage/$type_name/fields/$field_name", $edit, t('Save settings'));
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'New file saved to disk on node creation.');
+
+ // Ensure the private file is available to the user who uploaded it.
+ $this->drupalGet(file_create_url($node_file->uri));
+ $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
+
+ // Ensure we can't change 'uri_scheme' field settings while there are some
+ // entities with uploaded files.
+ $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name");
+ $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and @disabled="disabled"]', 'public', 'Upload destination setting disabled.');
+
+ // Delete node and confirm that setting could be changed.
+ node_delete($nid);
+ $this->drupalGet("admin/structure/types/manage/$type_name/fields/$field_name");
+ $this->assertFieldByXpath('//input[@id="edit-field-settings-uri-scheme-public" and not(@disabled)]', 'public', 'Upload destination setting enabled.');
+ }
+
+ /**
+ * Tests that download restrictions on private files work on comments.
+ */
+ function testPrivateFileComment() {
+ $user = $this->drupalCreateUser(array('access comments'));
+
+ // Remove access comments permission from anon user.
+ $edit = array(
+ DRUPAL_ANONYMOUS_RID . '[access comments]' => FALSE,
+ );
+ $this->drupalPost('admin/people/permissions', $edit, t('Save permissions'));
+
+ // Create a new field.
+ $edit = array(
+ 'fields[_add_new_field][label]' => $label = $this->randomName(),
+ 'fields[_add_new_field][field_name]' => $name = strtolower($this->randomName()),
+ 'fields[_add_new_field][type]' => 'file',
+ 'fields[_add_new_field][widget_type]' => 'file_generic',
+ );
+ $this->drupalPost('admin/structure/types/manage/article/comment/fields', $edit, t('Save'));
+ $edit = array('field[settings][uri_scheme]' => 'private');
+ $this->drupalPost(NULL, $edit, t('Save field settings'));
+ $this->drupalPost(NULL, array(), t('Save settings'));
+
+ // Create node.
+ $text_file = $this->getTestFile('text');
+ $edit = array(
+ 'title' => $this->randomName(),
+ );
+ $this->drupalPost('node/add/article', $edit, t('Save'));
+ $node = $this->drupalGetNodeByTitle($edit['title']);
+
+ // Add a comment with a file.
+ $text_file = $this->getTestFile('text');
+ $edit = array(
+ 'files[field_' . $name . '_' . LANGUAGE_NONE . '_' . 0 . ']' => drupal_realpath($text_file->uri),
+ 'comment_body[' . LANGUAGE_NONE . '][0][value]' => $comment_body = $this->randomName(),
+ );
+ $this->drupalPost(NULL, $edit, t('Save'));
+
+ // Get the comment ID.
+ preg_match('/comment-([0-9]+)/', $this->getUrl(), $matches);
+ $cid = $matches[1];
+
+ // Log in as normal user.
+ $this->drupalLogin($user);
+
+ $comment = comment_load($cid);
+ $comment_file = (object) $comment->{'field_' . $name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($comment_file, 'New file saved to disk on node creation.');
+ // Test authenticated file download.
+ $url = file_create_url($comment_file->uri);
+ $this->assertNotEqual($url, NULL, 'Confirmed that the URL is valid');
+ $this->drupalGet(file_create_url($comment_file->uri));
+ $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
+
+ // Test anonymous file download.
+ $this->drupalLogout();
+ $this->drupalGet(file_create_url($comment_file->uri));
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
+
+ // Unpublishes node.
+ $this->drupalLogin($this->admin_user);
+ $edit = array(
+ 'status' => FALSE,
+ );
+ $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
+
+ // Ensures normal user can no longer download the file.
+ $this->drupalLogin($user);
+ $this->drupalGet(file_create_url($comment_file->uri));
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
+ }
+
+}
+
+/**
+ * Tests file handling with node revisions.
+ */
+class FileFieldRevisionTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field revision test',
+ 'description' => 'Test creating and deleting revisions with files attached.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests creating multiple revisions of a node and managing attached files.
+ *
+ * Expected behaviors:
+ * - Adding a new revision will make another entry in the field table, but
+ * the original file will not be duplicated.
+ * - Deleting a revision should not delete the original file if the file
+ * is in use by another revision.
+ * - When the last revision that uses a file is deleted, the original file
+ * should be deleted also.
+ */
+ function testRevisions() {
+ $type_name = 'article';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ // Attach the same fields to users.
+ $this->attachFileField($field_name, 'user', 'user');
+
+ $test_file = $this->getTestFile('text');
+
+ // Create a new node with the uploaded file.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+ // Check that the file exists on disk and in the database.
+ $node = node_load($nid, NULL, TRUE);
+ $node_file_r1 = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $node_vid_r1 = $node->vid;
+ $this->assertFileExists($node_file_r1, 'New file saved to disk on node creation.');
+ $this->assertFileEntryExists($node_file_r1, 'File entry exists in database on node creation.');
+ $this->assertFileIsPermanent($node_file_r1, 'File is permanent.');
+
+ // Upload another file to the same node in a new revision.
+ $this->replaceNodeFile($test_file, $field_name, $nid);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file_r2 = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $node_vid_r2 = $node->vid;
+ $this->assertFileExists($node_file_r2, 'Replacement file exists on disk after creating new revision.');
+ $this->assertFileEntryExists($node_file_r2, 'Replacement file entry exists in database after creating new revision.');
+ $this->assertFileIsPermanent($node_file_r2, 'Replacement file is permanent.');
+
+ // Check that the original file is still in place on the first revision.
+ $node = node_load($nid, $node_vid_r1, TRUE);
+ $this->assertEqual($node_file_r1, (object) $node->{$field_name}[LANGUAGE_NONE][0], 'Original file still in place after replacing file in new revision.');
+ $this->assertFileExists($node_file_r1, 'Original file still in place after replacing file in new revision.');
+ $this->assertFileEntryExists($node_file_r1, 'Original file entry still in place after replacing file in new revision');
+ $this->assertFileIsPermanent($node_file_r1, 'Original file is still permanent.');
+
+ // Save a new version of the node without any changes.
+ // Check that the file is still the same as the previous revision.
+ $this->drupalPost('node/' . $nid . '/edit', array('revision' => '1'), t('Save'));
+ $node = node_load($nid, NULL, TRUE);
+ $node_file_r3 = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $node_vid_r3 = $node->vid;
+ $this->assertEqual($node_file_r2, $node_file_r3, 'Previous revision file still in place after creating a new revision without a new file.');
+ $this->assertFileIsPermanent($node_file_r3, 'New revision file is permanent.');
+
+ // Revert to the first revision and check that the original file is active.
+ $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r1 . '/revert', array(), t('Revert'));
+ $node = node_load($nid, NULL, TRUE);
+ $node_file_r4 = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $node_vid_r4 = $node->vid;
+ $this->assertEqual($node_file_r1, $node_file_r4, 'Original revision file still in place after reverting to the original revision.');
+ $this->assertFileIsPermanent($node_file_r4, 'Original revision file still permanent after reverting to the original revision.');
+
+ // Delete the second revision and check that the file is kept (since it is
+ // still being used by the third revision).
+ $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r2 . '/delete', array(), t('Delete'));
+ $this->assertFileExists($node_file_r3, 'Second file is still available after deleting second revision, since it is being used by the third revision.');
+ $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting second revision, since it is being used by the third revision.');
+ $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting second revision, since it is being used by the third revision.');
+
+ // Attach the second file to a user.
+ $user = $this->drupalCreateUser();
+ $edit = (array) $user;
+ $edit[$field_name][LANGUAGE_NONE][0] = (array) $node_file_r3;
+ user_save($user, $edit);
+ $this->drupalGet('user/' . $user->uid . '/edit');
+
+ // Delete the third revision and check that the file is not deleted yet.
+ $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', array(), t('Delete'));
+ $this->assertFileExists($node_file_r3, 'Second file is still available after deleting third revision, since it is being used by the user.');
+ $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting third revision, since it is being used by the user.');
+ $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting third revision, since it is being used by the user.');
+
+ // Delete the user and check that the file is also deleted.
+ user_delete($user->uid);
+ // TODO: This seems like a bug in File API. Clearing the stat cache should
+ // not be necessary here. The file really is deleted, but stream wrappers
+ // doesn't seem to think so unless we clear the PHP file stat() cache.
+ clearstatcache();
+ $this->assertFileNotExists($node_file_r3, 'Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
+ $this->assertFileEntryNotExists($node_file_r3, 'Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.');
+
+ // Delete the entire node and check that the original file is deleted.
+ $this->drupalPost('node/' . $nid . '/delete', array(), t('Delete'));
+ $this->assertFileNotExists($node_file_r1, 'Original file is deleted after deleting the entire node with two revisions remaining.');
+ $this->assertFileEntryNotExists($node_file_r1, 'Original file entry is deleted after deleting the entire node with two revisions remaining.');
+ }
+}
+
+/**
+ * Tests that formatters are working properly.
+ */
+class FileFieldDisplayTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field display tests',
+ 'description' => 'Test the display of file fields in node and views.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests normal formatter display on node display.
+ */
+ function testNodeDisplay() {
+ $field_name = strtolower($this->randomName());
+ $type_name = 'article';
+ $field_settings = array(
+ 'display_field' => '1',
+ 'display_default' => '1',
+ );
+ $instance_settings = array(
+ 'description_field' => '1',
+ );
+ $widget_settings = array();
+ $this->createFileField($field_name, $type_name, $field_settings, $instance_settings, $widget_settings);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ // Create a new node *without* the file field set, and check that the field
+ // is not shown for each node display.
+ $node = $this->drupalCreateNode(array('type' => $type_name));
+ $file_formatters = array('file_default', 'file_table', 'file_url_plain', 'hidden');
+ foreach ($file_formatters as $formatter) {
+ $edit = array(
+ "fields[$field_name][type]" => $formatter,
+ );
+ $this->drupalPost("admin/structure/types/manage/$type_name/display", $edit, t('Save'));
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter)));
+ }
+
+ $test_file = $this->getTestFile('text');
+
+ // Create a new node with the uploaded file.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $this->drupalGet('node/' . $nid . '/edit');
+
+ // Check that the default formatter is displaying with the file name.
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $default_output = theme('file_link', array('file' => $node_file));
+ $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
+
+ // Turn the "display" option off and check that the file is no longer displayed.
+ $edit = array($field_name . '[' . LANGUAGE_NONE . '][0][display]' => FALSE);
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+
+ $this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.');
+
+ }
+}
+
+/**
+ * Tests various validations.
+ */
+class FileFieldValidateTestCase extends FileFieldTestCase {
+ protected $field;
+ protected $node_type;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field validation tests',
+ 'description' => 'Tests validation functions such as file type, max file size, max size per node, and required.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests the required property on file fields.
+ */
+ function testRequired() {
+ $type_name = 'article';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name, array(), array('required' => '1'));
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('text');
+
+ // Try to post a new node without uploading a file.
+ $langcode = LANGUAGE_NONE;
+ $edit = array("title" => $this->randomName());
+ $this->drupalPost('node/add/' . $type_name, $edit, t('Save'));
+ $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required file field was empty.');
+
+ // Create a new node with the uploaded file.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $this->assertTrue($nid !== FALSE, format_string('uploadNodeFile(@test_file, @field_name, @type_name) succeeded', array('@test_file' => $test_file->uri, '@field_name' => $field_name, '@type_name' => $type_name)));
+
+ $node = node_load($nid, NULL, TRUE);
+
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'File exists after uploading to the required field.');
+ $this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required field.');
+
+ // Try again with a multiple value field.
+ field_delete_field($field_name);
+ $this->createFileField($field_name, $type_name, array('cardinality' => FIELD_CARDINALITY_UNLIMITED), array('required' => '1'));
+
+ // Try to post a new node without uploading a file in the multivalue field.
+ $edit = array('title' => $this->randomName());
+ $this->drupalPost('node/add/' . $type_name, $edit, t('Save'));
+ $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required multiple value file field was empty.');
+
+ // Create a new node with the uploaded file into the multivalue field.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'File exists after uploading to the required multiple value field.');
+ $this->assertFileEntryExists($node_file, 'File entry exists after uploading to the required multipel value field.');
+
+ // Remove our file field.
+ field_delete_field($field_name);
+ }
+
+ /**
+ * Tests the max file size validator.
+ */
+ function testFileMaxSize() {
+ $type_name = 'article';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name, array(), array('required' => '1'));
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $small_file = $this->getTestFile('text', 131072); // 128KB.
+ $large_file = $this->getTestFile('text', 1310720); // 1.2MB
+
+ // Test uploading both a large and small file with different increments.
+ $sizes = array(
+ '1M' => 1048576,
+ '1024K' => 1048576,
+ '1048576' => 1048576,
+ );
+
+ foreach ($sizes as $max_filesize => $file_limit) {
+ // Set the max file upload size.
+ $this->updateFileField($field_name, $type_name, array('max_filesize' => $max_filesize));
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ // Create a new node with the small file, which should pass.
+ $nid = $this->uploadNodeFile($small_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize)));
+ $this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize)));
+
+ // Check that uploading the large file fails (1M limit).
+ $nid = $this->uploadNodeFile($large_file, $field_name, $type_name);
+ $error_message = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($large_file->filesize), '%maxsize' => format_size($file_limit)));
+ $this->assertRaw($error_message, format_string('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', array('%filesize' => format_size($large_file->filesize), '%maxsize' => $max_filesize)));
+ }
+
+ // Turn off the max filesize.
+ $this->updateFileField($field_name, $type_name, array('max_filesize' => ''));
+
+ // Upload the big file successfully.
+ $nid = $this->uploadNodeFile($large_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, format_string('File exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize))));
+ $this->assertFileEntryExists($node_file, format_string('File entry exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize))));
+
+ // Remove our file field.
+ field_delete_field($field_name);
+ }
+
+ /**
+ * Tests file extension checking.
+ */
+ function testFileExtension() {
+ $type_name = 'article';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('image');
+ list(, $test_file_extension) = explode('.', $test_file->filename);
+
+ // Disable extension checking.
+ $this->updateFileField($field_name, $type_name, array('file_extensions' => ''));
+
+ // Check that the file can be uploaded with no extension checking.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'File exists after uploading a file with no extension checking.');
+ $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with no extension checking.');
+
+ // Enable extension checking for text files.
+ $this->updateFileField($field_name, $type_name, array('file_extensions' => 'txt'));
+
+ // Check that the file with the wrong extension cannot be uploaded.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $error_message = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => 'txt'));
+ $this->assertRaw($error_message, 'Node save failed when file uploaded with the wrong extension.');
+
+ // Enable extension checking for text and image files.
+ $this->updateFileField($field_name, $type_name, array('file_extensions' => "txt $test_file_extension"));
+
+ // Check that the file can be uploaded with extension checking.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertFileExists($node_file, 'File exists after uploading a file with extension checking.');
+ $this->assertFileEntryExists($node_file, 'File entry exists after uploading a file with extension checking.');
+
+ // Remove our file field.
+ field_delete_field($field_name);
+ }
+}
+
+/**
+ * Tests that files are uploaded to proper locations.
+ */
+class FileFieldPathTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File field file path tests',
+ 'description' => 'Test that files are uploaded to the proper location with token support.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Tests the normal formatter display on node display.
+ */
+ function testUploadPath() {
+ $field_name = strtolower($this->randomName());
+ $type_name = 'article';
+ $field = $this->createFileField($field_name, $type_name);
+ $test_file = $this->getTestFile('text');
+
+ // Create a new node.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+ // Check that the file was uploaded to the file root.
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertPathMatch('public://' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri)));
+
+ // Change the path to contain multiple subdirectories.
+ $field = $this->updateFileField($field_name, $type_name, array('file_directory' => 'foo/bar/baz'));
+
+ // Upload a new file into the subdirectories.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+ // Check that the file was uploaded into the subdirectory.
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ $this->assertPathMatch('public://foo/bar/baz/' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path.', array('%file' => $node_file->uri)));
+
+ // Check the path when used with tokens.
+ // Change the path to contain multiple token directories.
+ $field = $this->updateFileField($field_name, $type_name, array('file_directory' => '[current-user:uid]/[current-user:name]'));
+
+ // Upload a new file into the token subdirectories.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+ // Check that the file was uploaded into the subdirectory.
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ // Do token replacement using the same user which uploaded the file, not
+ // the user running the test case.
+ $data = array('user' => $this->admin_user);
+ $subdirectory = token_replace('[user:uid]/[user:name]', $data);
+ $this->assertPathMatch('public://' . $subdirectory . '/' . $test_file->filename, $node_file->uri, format_string('The file %file was uploaded to the correct path with token replacements.', array('%file' => $node_file->uri)));
+ }
+
+ /**
+ * Asserts that a file is uploaded to the right location.
+ *
+ * @param $expected_path
+ * The location where the file is expected to be uploaded. Duplicate file
+ * names to not need to be taken into account.
+ * @param $actual_path
+ * Where the file was actually uploaded.
+ * @param $message
+ * The message to display with this assertion.
+ */
+ function assertPathMatch($expected_path, $actual_path, $message) {
+ // Strip off the extension of the expected path to allow for _0, _1, etc.
+ // suffixes when the file hits a duplicate name.
+ $pos = strrpos($expected_path, '.');
+ $base_path = substr($expected_path, 0, $pos);
+ $extension = substr($expected_path, $pos + 1);
+
+ $result = preg_match('/' . preg_quote($base_path, '/') . '(_[0-9]+)?\.' . preg_quote($extension, '/') . '/', $actual_path);
+ $this->assertTrue($result, $message);
+ }
+}
+
+/**
+ * Tests the file token replacement in strings.
+ */
+class FileTokenReplaceTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'File token replacement',
+ 'description' => 'Generates text using placeholders for dummy content to check file token replacement.',
+ 'group' => 'File',
+ );
+ }
+
+ /**
+ * Creates a file, then tests the tokens generated from it.
+ */
+ function testFileTokenReplacement() {
+ global $language;
+ $url_options = array(
+ 'absolute' => TRUE,
+ 'language' => $language,
+ );
+
+ // Create file field.
+ $type_name = 'article';
+ $field_name = 'field_' . strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, $type_name);
+
+ $test_file = $this->getTestFile('text');
+ // Coping a file to test uploads with non-latin filenames.
+ $filename = drupal_dirname($test_file->uri) . '/текстовый файл.txt';
+ $test_file = file_copy($test_file, $filename);
+
+ // Create a new node with the uploaded file.
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name);
+
+ // Load the node and the file.
+ $node = node_load($nid, NULL, TRUE);
+ $file = file_load($node->{$field_name}[LANGUAGE_NONE][0]['fid']);
+
+ // Generate and test sanitized tokens.
+ $tests = array();
+ $tests['[file:fid]'] = $file->fid;
+ $tests['[file:name]'] = check_plain($file->filename);
+ $tests['[file:path]'] = check_plain($file->uri);
+ $tests['[file:mime]'] = check_plain($file->filemime);
+ $tests['[file:size]'] = format_size($file->filesize);
+ $tests['[file:url]'] = check_plain(file_create_url($file->uri));
+ $tests['[file:timestamp]'] = format_date($file->timestamp, 'medium', '', NULL, $language->language);
+ $tests['[file:timestamp:short]'] = format_date($file->timestamp, 'short', '', NULL, $language->language);
+ $tests['[file:owner]'] = check_plain(format_username($this->admin_user));
+ $tests['[file:owner:uid]'] = $file->uid;
+
+ // Test to make sure that we generated something for each token.
+ $this->assertFalse(in_array(0, array_map('strlen', $tests)), 'No empty tokens generated.');
+
+ foreach ($tests as $input => $expected) {
+ $output = token_replace($input, array('file' => $file), array('language' => $language));
+ $this->assertEqual($output, $expected, format_string('Sanitized file token %token replaced.', array('%token' => $input)));
+ }
+
+ // Generate and test unsanitized tokens.
+ $tests['[file:name]'] = $file->filename;
+ $tests['[file:path]'] = $file->uri;
+ $tests['[file:mime]'] = $file->filemime;
+ $tests['[file:size]'] = format_size($file->filesize);
+
+ foreach ($tests as $input => $expected) {
+ $output = token_replace($input, array('file' => $file), array('language' => $language, 'sanitize' => FALSE));
+ $this->assertEqual($output, $expected, format_string('Unsanitized file token %token replaced.', array('%token' => $input)));
+ }
+ }
+}
+
+/**
+ * Tests file access on private nodes.
+ */
+class FilePrivateTestCase extends FileFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Private file test',
+ 'description' => 'Uploads a test to a private node and checks access.',
+ 'group' => 'File',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('node_access_test', 'field_test'));
+ node_access_rebuild();
+ variable_set('node_access_test_private', TRUE);
+ }
+
+ /**
+ * Tests file access for file uploaded to a private node.
+ */
+ function testPrivateFile() {
+ // Use 'page' instead of 'article', so that the 'article' image field does
+ // not conflict with this test. If in the future the 'page' type gets its
+ // own default file or image field, this test can be made more robust by
+ // using a custom node type.
+ $type_name = 'page';
+ $field_name = strtolower($this->randomName());
+ $this->createFileField($field_name, $type_name, array('uri_scheme' => 'private'));
+
+ // Create a field with no view access - see field_test_field_access().
+ $no_access_field_name = 'field_no_view_access';
+ $this->createFileField($no_access_field_name, $type_name, array('uri_scheme' => 'private'));
+
+ $test_file = $this->getTestFile('text');
+ $nid = $this->uploadNodeFile($test_file, $field_name, $type_name, TRUE, array('private' => TRUE));
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0];
+ // Ensure the file can be downloaded.
+ $this->drupalGet(file_create_url($node_file->uri));
+ $this->assertResponse(200, 'Confirmed that the generated URL is correct by downloading the shipped file.');
+ $this->drupalLogOut();
+ $this->drupalGet(file_create_url($node_file->uri));
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without the needed permission.');
+
+ // Test with the field that should deny access through field access.
+ $this->drupalLogin($this->admin_user);
+ $nid = $this->uploadNodeFile($test_file, $no_access_field_name, $type_name, TRUE, array('private' => TRUE));
+ $node = node_load($nid, NULL, TRUE);
+ $node_file = (object) $node->{$no_access_field_name}[LANGUAGE_NONE][0];
+ // Ensure the file cannot be downloaded.
+ $this->drupalGet(file_create_url($node_file->uri));
+ $this->assertResponse(403, 'Confirmed that access is denied for the file without view field access permission.');
+ }
+}
diff --git a/drupal-dev/modules/file/tests/file_module_test.info b/drupal-dev/modules/file/tests/file_module_test.info
new file mode 100644
index 0000000..4f94acf
--- /dev/null
+++ b/drupal-dev/modules/file/tests/file_module_test.info
@@ -0,0 +1,12 @@
+name = File test
+description = Provides hooks for testing File module functionality.
+package = Core
+version = VERSION
+core = 7.x
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/file/tests/file_module_test.module b/drupal-dev/modules/file/tests/file_module_test.module
new file mode 100644
index 0000000..f66c749
--- /dev/null
+++ b/drupal-dev/modules/file/tests/file_module_test.module
@@ -0,0 +1,69 @@
+ 'Managed file test',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('file_module_test_form'),
+ 'access arguments' => array('access content'),
+ );
+
+ return $items;
+}
+
+/**
+ * Form constructor for testing a 'managed_file' element.
+ *
+ * @see file_module_test_form_submit()
+ * @ingroup forms
+ */
+function file_module_test_form($form, &$form_state, $tree = TRUE, $extended = FALSE, $default_fid = NULL) {
+ $form['#tree'] = (bool) $tree;
+
+ $form['nested']['file'] = array(
+ '#type' => 'managed_file',
+ '#title' => t('Managed file'),
+ '#upload_location' => 'public://test',
+ '#progress_message' => t('Please wait...'),
+ '#extended' => (bool) $extended,
+ '#size' => 13,
+ );
+ if ($default_fid) {
+ $form['nested']['file']['#default_value'] = $extended ? array('fid' => $default_fid) : $default_fid;
+ }
+
+ $form['textfield'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Type a value and ensure it stays'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form submission handler for file_module_test_form().
+ */
+function file_module_test_form_submit($form, &$form_state) {
+ if ($form['#tree']) {
+ $fid = $form['nested']['file']['#extended'] ? $form_state['values']['nested']['file']['fid'] : $form_state['values']['nested']['file'];
+ }
+ else {
+ $fid = $form['nested']['file']['#extended'] ? $form_state['values']['file']['fid'] : $form_state['values']['file'];
+ }
+ drupal_set_message(t('The file id is %fid.', array('%fid' => $fid)));
+}
diff --git a/drupal-dev/modules/filter/filter.admin.inc b/drupal-dev/modules/filter/filter.admin.inc
new file mode 100644
index 0000000..60284d9
--- /dev/null
+++ b/drupal-dev/modules/filter/filter.admin.inc
@@ -0,0 +1,408 @@
+ $format) {
+ // Check whether this is the fallback text format. This format is available
+ // to all roles and cannot be disabled via the admin interface.
+ $form['formats'][$id]['#is_fallback'] = ($id == $fallback_format);
+ if ($form['formats'][$id]['#is_fallback']) {
+ $form['formats'][$id]['name'] = array('#markup' => drupal_placeholder($format->name));
+ $roles_markup = drupal_placeholder(t('All roles may use this format'));
+ }
+ else {
+ $form['formats'][$id]['name'] = array('#markup' => check_plain($format->name));
+ $roles = array_map('check_plain', filter_get_roles_by_format($format));
+ $roles_markup = $roles ? implode(', ', $roles) : t('No roles may use this format');
+ }
+ $form['formats'][$id]['roles'] = array('#markup' => $roles_markup);
+ $form['formats'][$id]['configure'] = array('#type' => 'link', '#title' => t('configure'), '#href' => 'admin/config/content/formats/' . $id);
+ $form['formats'][$id]['disable'] = array('#type' => 'link', '#title' => t('disable'), '#href' => 'admin/config/content/formats/' . $id . '/disable', '#access' => !$form['formats'][$id]['#is_fallback']);
+ $form['formats'][$id]['weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight for @title', array('@title' => $format->name)),
+ '#title_display' => 'invisible',
+ '#default_value' => $format->weight,
+ );
+ }
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save changes'));
+ return $form;
+}
+
+/**
+ * Form submission handler for filter_admin_overview().
+ */
+function filter_admin_overview_submit($form, &$form_state) {
+ foreach ($form_state['values']['formats'] as $id => $data) {
+ if (is_array($data) && isset($data['weight'])) {
+ // Only update if this is a form element with weight.
+ db_update('filter_format')
+ ->fields(array('weight' => $data['weight']))
+ ->condition('format', $id)
+ ->execute();
+ }
+ }
+ filter_formats_reset();
+ drupal_set_message(t('The text format ordering has been saved.'));
+}
+
+/**
+ * Returns HTML for the text format administration overview form.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_filter_admin_overview($variables) {
+ $form = $variables['form'];
+
+ $rows = array();
+ foreach (element_children($form['formats']) as $id) {
+ $form['formats'][$id]['weight']['#attributes']['class'] = array('text-format-order-weight');
+ $rows[] = array(
+ 'data' => array(
+ drupal_render($form['formats'][$id]['name']),
+ drupal_render($form['formats'][$id]['roles']),
+ drupal_render($form['formats'][$id]['weight']),
+ drupal_render($form['formats'][$id]['configure']),
+ drupal_render($form['formats'][$id]['disable']),
+ ),
+ 'class' => array('draggable'),
+ );
+ }
+ $header = array(t('Name'), t('Roles'), t('Weight'), array('data' => t('Operations'), 'colspan' => 2));
+ $output = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('id' => 'text-format-order')));
+ $output .= drupal_render_children($form);
+
+ drupal_add_tabledrag('text-format-order', 'order', 'sibling', 'text-format-order-weight');
+
+ return $output;
+}
+
+/**
+ * Page callback: Displays the text format add/edit form.
+ *
+ * @param object|null $format
+ * (optional) An object representing a format, with the following properties:
+ * - format: A machine-readable name representing the ID of the text format
+ * to save. If this corresponds to an existing text format, that format
+ * will be updated; otherwise, a new format will be created.
+ * - name: The title of the text format.
+ * - cache: (optional) An integer indicating whether the text format is
+ * cacheable (1) or not (0). Defaults to 1.
+ * - status: (optional) An integer indicating whether the text format is
+ * enabled (1) or not (0). Defaults to 1.
+ * - weight: (optional) The weight of the text format, which controls its
+ * placement in text format lists. If omitted, the weight is set to 0.
+ * Defaults to NULL.
+ *
+ * @return
+ * A form array.
+ *
+ * @see filter_menu()
+ */
+function filter_admin_format_page($format = NULL) {
+ if (!isset($format->name)) {
+ drupal_set_title(t('Add text format'));
+ $format = (object) array(
+ 'format' => NULL,
+ 'name' => '',
+ );
+ }
+ return drupal_get_form('filter_admin_format_form', $format);
+}
+
+/**
+ * Form constructor for the text format add/edit form.
+ *
+ * @param $format
+ * A format object having the properties:
+ * - format: A machine-readable name representing the ID of the text format to
+ * save. If this corresponds to an existing text format, that format will be
+ * updated; otherwise, a new format will be created.
+ * - name: The title of the text format.
+ * - cache: An integer indicating whether the text format is cacheable (1) or
+ * not (0). Defaults to 1.
+ * - status: (optional) An integer indicating whether the text format is
+ * enabled (1) or not (0). Defaults to 1.
+ * - weight: (optional) The weight of the text format, which controls its
+ * placement in text format lists. If omitted, the weight is set to 0.
+ *
+ * @see filter_admin_format_form_validate()
+ * @see filter_admin_format_form_submit()
+ * @ingroup forms
+ */
+function filter_admin_format_form($form, &$form_state, $format) {
+ $is_fallback = ($format->format == filter_fallback_format());
+
+ $form['#format'] = $format;
+ $form['#tree'] = TRUE;
+ $form['#attached']['js'][] = drupal_get_path('module', 'filter') . '/filter.admin.js';
+ $form['#attached']['css'][] = drupal_get_path('module', 'filter') . '/filter.css';
+
+ $form['name'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#default_value' => $format->name,
+ '#required' => TRUE,
+ );
+ $form['format'] = array(
+ '#type' => 'machine_name',
+ '#required' => TRUE,
+ '#default_value' => $format->format,
+ '#maxlength' => 255,
+ '#machine_name' => array(
+ 'exists' => 'filter_format_exists',
+ ),
+ '#disabled' => !empty($format->format),
+ );
+
+ // Add user role access selection.
+ $form['roles'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Roles'),
+ '#options' => array_map('check_plain', user_roles()),
+ '#disabled' => $is_fallback,
+ );
+ if ($is_fallback) {
+ $form['roles']['#description'] = t('All roles for this text format must be enabled and cannot be changed.');
+ }
+ if (!empty($format->format)) {
+ // If editing an existing text format, pre-select its current permissions.
+ $form['roles']['#default_value'] = array_keys(filter_get_roles_by_format($format));
+ }
+ elseif ($admin_role = variable_get('user_admin_role', 0)) {
+ // If adding a new text format and the site has an administrative role,
+ // pre-select that role so as to grant administrators access to the new
+ // text format permission by default.
+ $form['roles']['#default_value'] = array($admin_role);
+ }
+
+ // Retrieve available filters and load all configured filters for existing
+ // text formats.
+ $filter_info = filter_get_filters();
+ $filters = !empty($format->format) ? filter_list_format($format->format) : array();
+
+ // Prepare filters for form sections.
+ foreach ($filter_info as $name => $filter) {
+ // Create an empty filter object for new/unconfigured filters.
+ if (!isset($filters[$name])) {
+ $filters[$name] = new stdClass();
+ $filters[$name]->format = $format->format;
+ $filters[$name]->module = $filter['module'];
+ $filters[$name]->name = $name;
+ $filters[$name]->status = 0;
+ $filters[$name]->weight = $filter['weight'];
+ $filters[$name]->settings = array();
+ }
+ }
+ $form['#filters'] = $filters;
+
+ // Filter status.
+ $form['filters']['status'] = array(
+ '#type' => 'item',
+ '#title' => t('Enabled filters'),
+ '#prefix' => '
',
+ '#suffix' => '
',
+ );
+ foreach ($filter_info as $name => $filter) {
+ $form['filters']['status'][$name] = array(
+ '#type' => 'checkbox',
+ '#title' => $filter['title'],
+ '#default_value' => $filters[$name]->status,
+ '#parents' => array('filters', $name, 'status'),
+ '#description' => $filter['description'],
+ '#weight' => $filter['weight'],
+ );
+ }
+
+ // Filter order (tabledrag).
+ $form['filters']['order'] = array(
+ '#type' => 'item',
+ '#title' => t('Filter processing order'),
+ '#theme' => 'filter_admin_format_filter_order',
+ );
+ foreach ($filter_info as $name => $filter) {
+ $form['filters']['order'][$name]['filter'] = array(
+ '#markup' => $filter['title'],
+ );
+ $form['filters']['order'][$name]['weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight for @title', array('@title' => $filter['title'])),
+ '#title_display' => 'invisible',
+ '#delta' => 50,
+ '#default_value' => $filters[$name]->weight,
+ '#parents' => array('filters', $name, 'weight'),
+ );
+ $form['filters']['order'][$name]['#weight'] = $filters[$name]->weight;
+ }
+
+ // Filter settings.
+ $form['filter_settings_title'] = array(
+ '#type' => 'item',
+ '#title' => t('Filter settings'),
+ );
+ $form['filter_settings'] = array(
+ '#type' => 'vertical_tabs',
+ );
+
+ foreach ($filter_info as $name => $filter) {
+ if (isset($filter['settings callback']) && function_exists($filter['settings callback'])) {
+ $function = $filter['settings callback'];
+ // Pass along stored filter settings and default settings, but also the
+ // format object and all filters to allow for complex implementations.
+ $defaults = (isset($filter['default settings']) ? $filter['default settings'] : array());
+ $settings_form = $function($form, $form_state, $filters[$name], $format, $defaults, $filters);
+ if (!empty($settings_form)) {
+ $form['filters']['settings'][$name] = array(
+ '#type' => 'fieldset',
+ '#title' => $filter['title'],
+ '#parents' => array('filters', $name, 'settings'),
+ '#weight' => $filter['weight'],
+ '#group' => 'filter_settings',
+ );
+ $form['filters']['settings'][$name] += $settings_form;
+ }
+ }
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
+
+ return $form;
+}
+
+/**
+ * Returns HTML for a text format's filter order form.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_filter_admin_format_filter_order($variables) {
+ $element = $variables['element'];
+
+ // Filter order (tabledrag).
+ $rows = array();
+ foreach (element_children($element, TRUE) as $name) {
+ $element[$name]['weight']['#attributes']['class'][] = 'filter-order-weight';
+ $rows[] = array(
+ 'data' => array(
+ drupal_render($element[$name]['filter']),
+ drupal_render($element[$name]['weight']),
+ ),
+ 'class' => array('draggable'),
+ );
+ }
+ $output = drupal_render_children($element);
+ $output .= theme('table', array('rows' => $rows, 'attributes' => array('id' => 'filter-order')));
+ drupal_add_tabledrag('filter-order', 'order', 'sibling', 'filter-order-weight', NULL, NULL, TRUE);
+
+ return $output;
+}
+
+/**
+ * Form validation handler for filter_admin_format_form().
+ *
+ * @see filter_admin_format_form_submit()
+ */
+function filter_admin_format_form_validate($form, &$form_state) {
+ $format_format = trim($form_state['values']['format']);
+ $format_name = trim($form_state['values']['name']);
+
+ // Ensure that the values to be saved later are exactly the ones validated.
+ form_set_value($form['format'], $format_format, $form_state);
+ form_set_value($form['name'], $format_name, $form_state);
+
+ $result = db_query("SELECT format FROM {filter_format} WHERE name = :name AND format <> :format", array(':name' => $format_name, ':format' => $format_format))->fetchField();
+ if ($result) {
+ form_set_error('name', t('Text format names must be unique. A format named %name already exists.', array('%name' => $format_name)));
+ }
+}
+
+/**
+ * Form submission handler for filter_admin_format_form().
+ *
+ * @see filter_admin_format_form_validate()
+ */
+function filter_admin_format_form_submit($form, &$form_state) {
+ // Remove unnecessary values.
+ form_state_values_clean($form_state);
+
+ // Add the submitted form values to the text format, and save it.
+ $format = $form['#format'];
+ foreach ($form_state['values'] as $key => $value) {
+ $format->$key = $value;
+ }
+ $status = filter_format_save($format);
+
+ // Save user permissions.
+ if ($permission = filter_permission_name($format)) {
+ foreach ($format->roles as $rid => $enabled) {
+ user_role_change_permissions($rid, array($permission => $enabled));
+ }
+ }
+
+ switch ($status) {
+ case SAVED_NEW:
+ drupal_set_message(t('Added text format %format.', array('%format' => $format->name)));
+ break;
+
+ case SAVED_UPDATED:
+ drupal_set_message(t('The text format %format has been updated.', array('%format' => $format->name)));
+ break;
+ }
+}
+
+/**
+ * Form constructor for the text format deletion confirmation form.
+ *
+ * @param $format
+ * An object representing a text format.
+ *
+ * @see filter_menu()
+ * @see filter_admin_disable_submit()
+ * @ingroup forms
+ */
+function filter_admin_disable($form, &$form_state, $format) {
+ $form['#format'] = $format;
+
+ return confirm_form($form,
+ t('Are you sure you want to disable the text format %format?', array('%format' => $format->name)),
+ 'admin/config/content/formats',
+ t('Disabled text formats are completely removed from the administrative interface, and any content stored with that format will not be displayed. This action cannot be undone.'),
+ t('Disable')
+ );
+}
+
+/**
+ * Form submission handler for filter_admin_disable().
+ */
+function filter_admin_disable_submit($form, &$form_state) {
+ $format = $form['#format'];
+ filter_format_disable($format);
+ drupal_set_message(t('Disabled text format %format.', array('%format' => $format->name)));
+
+ $form_state['redirect'] = 'admin/config/content/formats';
+}
diff --git a/drupal-dev/modules/filter/filter.admin.js b/drupal-dev/modules/filter/filter.admin.js
new file mode 100644
index 0000000..3bc6233
--- /dev/null
+++ b/drupal-dev/modules/filter/filter.admin.js
@@ -0,0 +1,44 @@
+(function ($) {
+
+Drupal.behaviors.filterStatus = {
+ attach: function (context, settings) {
+ $('#filters-status-wrapper input.form-checkbox', context).once('filter-status', function () {
+ var $checkbox = $(this);
+ // Retrieve the tabledrag row belonging to this filter.
+ var $row = $('#' + $checkbox.attr('id').replace(/-status$/, '-weight'), context).closest('tr');
+ // Retrieve the vertical tab belonging to this filter.
+ var tab = $('#' + $checkbox.attr('id').replace(/-status$/, '-settings'), context).data('verticalTab');
+
+ // Bind click handler to this checkbox to conditionally show and hide the
+ // filter's tableDrag row and vertical tab pane.
+ $checkbox.bind('click.filterUpdate', function () {
+ if ($checkbox.is(':checked')) {
+ $row.show();
+ if (tab) {
+ tab.tabShow().updateSummary();
+ }
+ }
+ else {
+ $row.hide();
+ if (tab) {
+ tab.tabHide().updateSummary();
+ }
+ }
+ // Restripe table after toggling visibility of table row.
+ Drupal.tableDrag['filter-order'].restripeTable();
+ });
+
+ // Attach summary for configurable filters (only for screen-readers).
+ if (tab) {
+ tab.fieldset.drupalSetSummary(function (tabContext) {
+ return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
+ });
+ }
+
+ // Trigger our bound click handler to update elements to initial state.
+ $checkbox.triggerHandler('click.filterUpdate');
+ });
+ }
+};
+
+})(jQuery);
diff --git a/drupal-dev/modules/filter/filter.api.php b/drupal-dev/modules/filter/filter.api.php
new file mode 100644
index 0000000..2901eb9
--- /dev/null
+++ b/drupal-dev/modules/filter/filter.api.php
@@ -0,0 +1,323 @@
+ FALSE while developing, but be sure to remove that
+ * setting if it's not needed, when you are no longer in development mode.
+ *
+ * @return
+ * An associative array of filters, whose keys are internal filter names,
+ * which should be unique and therefore prefixed with the name of the module.
+ * Each value is an associative array describing the filter, with the
+ * following elements (all are optional except as noted):
+ * - title: (required) An administrative summary of what the filter does.
+ * - description: Additional administrative information about the filter's
+ * behavior, if needed for clarification.
+ * - settings callback: The name of a function that returns configuration form
+ * elements for the filter. See callback_filter_settings() for details.
+ * - default settings: An associative array containing default settings for
+ * the filter, to be applied when the filter has not been configured yet.
+ * - prepare callback: The name of a function that escapes the content before
+ * the actual filtering happens. See callback_filter_prepare() for
+ * details.
+ * - process callback: (required) The name the function that performs the
+ * actual filtering. See callback_filter_process() for details.
+ * - cache (default TRUE): Specifies whether the filtered text can be cached.
+ * Note that setting this to FALSE makes the entire text format not
+ * cacheable, which may have an impact on the site's overall performance.
+ * See filter_format_allowcache() for details.
+ * - tips callback: The name of a function that returns end-user-facing filter
+ * usage guidelines for the filter. See callback_filter_tips() for
+ * details.
+ * - weight: A default weight for the filter in new text formats.
+ *
+ * @see filter_example.module
+ * @see hook_filter_info_alter()
+ */
+function hook_filter_info() {
+ $filters['filter_html'] = array(
+ 'title' => t('Limit allowed HTML tags'),
+ 'description' => t('Allows you to restrict the HTML tags the user can use. It will also remove harmful content such as JavaScript events, JavaScript URLs and CSS styles from those tags that are not removed.'),
+ 'process callback' => '_filter_html',
+ 'settings callback' => '_filter_html_settings',
+ 'default settings' => array(
+ 'allowed_html' => '
' . t('The Filter module allows administrators to configure text formats. A text format defines the HTML tags, codes, and other input allowed in content and comments, and is a key feature in guarding against potentially damaging input from malicious users. For more information, see the online handbook entry for Filter module.', array('@filter' => 'http://drupal.org/documentation/modules/filter/')) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Configuring text formats') . '
';
+ $output .= '
' . t('Configure text formats on the Text formats page. Improper text format configuration is a security risk. To ensure security, untrusted users should only have access to text formats that restrict them to either plain text or a safe set of HTML tags, since certain HTML tags can allow embedding malicious links or scripts in text. More trusted registered users may be granted permission to use less restrictive text formats in order to create rich content.', array('@formats' => url('admin/config/content/formats'))) . '
';
+ $output .= '
' . t('Applying filters to text') . '
';
+ $output .= '
' . t('Each text format uses filters to manipulate text, and most formats apply several different filters to text in a specific order. Each filter is designed for a specific purpose, and generally either adds, removes, or transforms elements within user-entered text before it is displayed. A filter does not change the actual content, but instead, modifies it temporarily before it is displayed. One filter may remove unapproved HTML tags, while another automatically adds HTML to make URLs display as clickable links.') . '
';
+ $output .= '
' . t('Defining text formats') . '
';
+ $output .= '
' . t('One format is included by default: Plain text (which removes all HTML tags). Additional formats may be created by your installation profile when you install Drupal, and more can be created by an administrator on the Text formats page.', array('@text-formats' => url('admin/config/content/formats'))) . '
';
+ $output .= '
' . t('Choosing a text format') . '
';
+ $output .= '
' . t('Users with access to more than one text format can use the Text format fieldset to choose between available text formats when creating or editing multi-line content. Administrators can define the text formats available to each user role, and control the order of formats listed in the Text format fieldset on the Text formats page.', array('@text-formats' => url('admin/config/content/formats'))) . '
' . t('Text formats define the HTML tags, code, and other formatting that can be used when entering text. Improper text format configuration is a security risk. Learn more on the Filter module help page.', array('@filterhelp' => url('admin/help/filter'))) . '
';
+ $output .= '
' . t('Text formats are presented on content editing pages in the order defined on this page. The first format available to a user will be selected by default.') . '
' . t('A text format contains filters that change the user input, for example stripping out malicious HTML or making URLs clickable. Filters are executed from top to bottom and the order is important, since one filter may prevent another filter from doing its job. For example, when URLs are converted into links before disallowed HTML tags are removed, all links may be removed. When this happens, the order of filters may need to be re-arranged.') . '
';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function filter_theme() {
+ return array(
+ 'filter_admin_overview' => array(
+ 'render element' => 'form',
+ 'file' => 'filter.admin.inc',
+ ),
+ 'filter_admin_format_filter_order' => array(
+ 'render element' => 'element',
+ 'file' => 'filter.admin.inc',
+ ),
+ 'filter_tips' => array(
+ 'variables' => array('tips' => NULL, 'long' => FALSE),
+ 'file' => 'filter.pages.inc',
+ ),
+ 'text_format_wrapper' => array(
+ 'render element' => 'element',
+ ),
+ 'filter_tips_more_info' => array(
+ 'variables' => array(),
+ ),
+ 'filter_guidelines' => array(
+ 'variables' => array('format' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_element_info().
+ *
+ * @see filter_process_format()
+ * @see text_format_wrapper()
+ */
+function filter_element_info() {
+ $type['text_format'] = array(
+ '#process' => array('filter_process_format'),
+ '#base_type' => 'textarea',
+ '#theme_wrappers' => array('text_format_wrapper'),
+ );
+ return $type;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function filter_menu() {
+ $items['filter/tips'] = array(
+ 'title' => 'Compose tips',
+ 'page callback' => 'filter_tips_long',
+ 'access callback' => TRUE,
+ 'type' => MENU_SUGGESTED_ITEM,
+ 'file' => 'filter.pages.inc',
+ );
+ $items['admin/config/content/formats'] = array(
+ 'title' => 'Text formats',
+ 'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('filter_admin_overview'),
+ 'access arguments' => array('administer filters'),
+ 'file' => 'filter.admin.inc',
+ );
+ $items['admin/config/content/formats/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ );
+ $items['admin/config/content/formats/add'] = array(
+ 'title' => 'Add text format',
+ 'page callback' => 'filter_admin_format_page',
+ 'access arguments' => array('administer filters'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'weight' => 1,
+ 'file' => 'filter.admin.inc',
+ );
+ $items['admin/config/content/formats/%filter_format'] = array(
+ 'title callback' => 'filter_admin_format_title',
+ 'title arguments' => array(4),
+ 'page callback' => 'filter_admin_format_page',
+ 'page arguments' => array(4),
+ 'access arguments' => array('administer filters'),
+ 'file' => 'filter.admin.inc',
+ );
+ $items['admin/config/content/formats/%filter_format/disable'] = array(
+ 'title' => 'Disable text format',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('filter_admin_disable', 4),
+ 'access callback' => '_filter_disable_format_access',
+ 'access arguments' => array(4),
+ 'file' => 'filter.admin.inc',
+ );
+ return $items;
+}
+
+/**
+ * Access callback: Checks access for disabling text formats.
+ *
+ * @param $format
+ * A text format object.
+ *
+ * @return
+ * TRUE if the text format can be disabled by the current user, FALSE
+ * otherwise.
+ *
+ * @see filter_menu()
+ */
+function _filter_disable_format_access($format) {
+ // The fallback format can never be disabled.
+ return user_access('administer filters') && ($format->format != filter_fallback_format());
+}
+
+/**
+ * Loads a text format object from the database.
+ *
+ * @param $format_id
+ * The format ID.
+ *
+ * @return
+ * A fully-populated text format object, if the requested format exists and
+ * is enabled. If the format does not exist, or exists in the database but
+ * has been marked as disabled, FALSE is returned.
+ *
+ * @see filter_format_exists()
+ */
+function filter_format_load($format_id) {
+ $formats = filter_formats();
+ return isset($formats[$format_id]) ? $formats[$format_id] : FALSE;
+}
+
+/**
+ * Saves a text format object to the database.
+ *
+ * @param $format
+ * A format object having the properties:
+ * - format: A machine-readable name representing the ID of the text format
+ * to save. If this corresponds to an existing text format, that format
+ * will be updated; otherwise, a new format will be created.
+ * - name: The title of the text format.
+ * - status: (optional) An integer indicating whether the text format is
+ * enabled (1) or not (0). Defaults to 1.
+ * - weight: (optional) The weight of the text format, which controls its
+ * placement in text format lists. If omitted, the weight is set to 0.
+ * - filters: (optional) An associative, multi-dimensional array of filters
+ * assigned to the text format, keyed by the name of each filter and using
+ * the properties:
+ * - weight: (optional) The weight of the filter in the text format. If
+ * omitted, either the currently stored weight is retained (if there is
+ * one), or the filter is assigned a weight of 10, which will usually
+ * put it at the bottom of the list.
+ * - status: (optional) A boolean indicating whether the filter is
+ * enabled in the text format. If omitted, the filter will be disabled.
+ * - settings: (optional) An array of configured settings for the filter.
+ * See hook_filter_info() for details.
+ *
+ * @return
+ * SAVED_NEW or SAVED_UPDATED.
+ */
+function filter_format_save($format) {
+ $format->name = trim($format->name);
+ $format->cache = _filter_format_is_cacheable($format);
+ if (!isset($format->status)) {
+ $format->status = 1;
+ }
+ if (!isset($format->weight)) {
+ $format->weight = 0;
+ }
+
+ // Insert or update the text format.
+ $return = db_merge('filter_format')
+ ->key(array('format' => $format->format))
+ ->fields(array(
+ 'name' => $format->name,
+ 'cache' => (int) $format->cache,
+ 'status' => (int) $format->status,
+ 'weight' => (int) $format->weight,
+ ))
+ ->execute();
+
+ // Programmatic saves may not contain any filters.
+ if (!isset($format->filters)) {
+ $format->filters = array();
+ }
+ $filter_info = filter_get_filters();
+ foreach ($filter_info as $name => $filter) {
+ // If the format does not specify an explicit weight for a filter, assign
+ // a default weight, either defined in hook_filter_info(), or the default of
+ // 0 by filter_get_filters()
+ if (!isset($format->filters[$name]['weight'])) {
+ $format->filters[$name]['weight'] = $filter['weight'];
+ }
+ $format->filters[$name]['status'] = isset($format->filters[$name]['status']) ? $format->filters[$name]['status'] : 0;
+ $format->filters[$name]['module'] = $filter['module'];
+
+ // If settings were passed, only ensure default settings.
+ if (isset($format->filters[$name]['settings'])) {
+ if (isset($filter['default settings'])) {
+ $format->filters[$name]['settings'] = array_merge($filter['default settings'], $format->filters[$name]['settings']);
+ }
+ }
+ // Otherwise, use default settings or fall back to an empty array.
+ else {
+ $format->filters[$name]['settings'] = isset($filter['default settings']) ? $filter['default settings'] : array();
+ }
+
+ $fields = array();
+ $fields['weight'] = $format->filters[$name]['weight'];
+ $fields['status'] = $format->filters[$name]['status'];
+ $fields['module'] = $format->filters[$name]['module'];
+ $fields['settings'] = serialize($format->filters[$name]['settings']);
+
+ db_merge('filter')
+ ->key(array(
+ 'format' => $format->format,
+ 'name' => $name,
+ ))
+ ->fields($fields)
+ ->execute();
+ }
+
+ if ($return == SAVED_NEW) {
+ module_invoke_all('filter_format_insert', $format);
+ }
+ else {
+ module_invoke_all('filter_format_update', $format);
+ // Explicitly indicate that the format was updated. We need to do this
+ // since if the filters were updated but the format object itself was not,
+ // the merge query above would not return an indication that anything had
+ // changed.
+ $return = SAVED_UPDATED;
+
+ // Clear the filter cache whenever a text format is updated.
+ cache_clear_all($format->format . ':', 'cache_filter', TRUE);
+ }
+
+ filter_formats_reset();
+
+ return $return;
+}
+
+/**
+ * Disables a text format.
+ *
+ * There is no core facility to re-enable a disabled format. It is not deleted
+ * to keep information for contrib and to make sure the format ID is never
+ * reused. As there might be content using the disabled format, this would lead
+ * to data corruption.
+ *
+ * @param $format
+ * The text format object to be disabled.
+ */
+function filter_format_disable($format) {
+ db_update('filter_format')
+ ->fields(array('status' => 0))
+ ->condition('format', $format->format)
+ ->execute();
+
+ // Allow modules to react on text format deletion.
+ module_invoke_all('filter_format_disable', $format);
+
+ // Clear the filter cache whenever a text format is disabled.
+ filter_formats_reset();
+ cache_clear_all($format->format . ':', 'cache_filter', TRUE);
+}
+
+/**
+ * Determines if a text format exists.
+ *
+ * @param $format_id
+ * The ID of the text format to check.
+ *
+ * @return
+ * TRUE if the text format exists, FALSE otherwise. Note that for disabled
+ * formats filter_format_exists() will return TRUE while filter_format_load()
+ * will return FALSE.
+ *
+ * @see filter_format_load()
+ */
+function filter_format_exists($format_id) {
+ return (bool) db_query_range('SELECT 1 FROM {filter_format} WHERE format = :format', 0, 1, array(':format' => $format_id))->fetchField();
+}
+
+/**
+ * Displays a text format form title.
+ *
+ * @param object $format
+ * A format object.
+ *
+ * @return string
+ * The name of the format.
+ *
+ * @see filter_menu()
+ */
+function filter_admin_format_title($format) {
+ return $format->name;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function filter_permission() {
+ $perms['administer filters'] = array(
+ 'title' => t('Administer text formats and filters'),
+ 'restrict access' => TRUE,
+ );
+
+ // Generate permissions for each text format. Warn the administrator that any
+ // of them are potentially unsafe.
+ foreach (filter_formats() as $format) {
+ $permission = filter_permission_name($format);
+ if (!empty($permission)) {
+ // Only link to the text format configuration page if the user who is
+ // viewing this will have access to that page.
+ $format_name_replacement = user_access('administer filters') ? l($format->name, 'admin/config/content/formats/' . $format->format) : drupal_placeholder($format->name);
+ $perms[$permission] = array(
+ 'title' => t("Use the !text_format text format", array('!text_format' => $format_name_replacement,)),
+ 'description' => drupal_placeholder(t('Warning: This permission may have security implications depending on how the text format is configured.')),
+ );
+ }
+ }
+ return $perms;
+}
+
+/**
+ * Returns the machine-readable permission name for a provided text format.
+ *
+ * @param $format
+ * An object representing a text format.
+ *
+ * @return
+ * The machine-readable permission name, or FALSE if the provided text format
+ * is malformed or is the fallback format (which is available to all users).
+ */
+function filter_permission_name($format) {
+ if (isset($format->format) && $format->format != filter_fallback_format()) {
+ return 'use text format ' . $format->format;
+ }
+ return FALSE;
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+function filter_modules_enabled($modules) {
+ // Reset the static cache of module-provided filters, in case any of the
+ // newly enabled modules defines a new filter or alters existing ones.
+ drupal_static_reset('filter_get_filters');
+}
+
+/**
+ * Implements hook_modules_disabled().
+ */
+function filter_modules_disabled($modules) {
+ // Reset the static cache of module-provided filters, in case any of the
+ // newly disabled modules defined or altered any filters.
+ drupal_static_reset('filter_get_filters');
+}
+
+/**
+ * Retrieves a list of text formats, ordered by weight.
+ *
+ * @param $account
+ * (optional) If provided, only those formats that are allowed for this user
+ * account will be returned. All formats will be returned otherwise. Defaults
+ * to NULL.
+ *
+ * @return
+ * An array of text format objects, keyed by the format ID and ordered by
+ * weight.
+ *
+ * @see filter_formats_reset()
+ */
+function filter_formats($account = NULL) {
+ global $language;
+ $formats = &drupal_static(__FUNCTION__, array());
+
+ // All available formats are cached for performance.
+ if (!isset($formats['all'])) {
+ if ($cache = cache_get("filter_formats:{$language->language}")) {
+ $formats['all'] = $cache->data;
+ }
+ else {
+ $formats['all'] = db_select('filter_format', 'ff')
+ ->addTag('translatable')
+ ->fields('ff')
+ ->condition('status', 1)
+ ->orderBy('weight')
+ ->execute()
+ ->fetchAllAssoc('format');
+
+ cache_set("filter_formats:{$language->language}", $formats['all']);
+ }
+ }
+
+ // Build a list of user-specific formats.
+ if (isset($account) && !isset($formats['user'][$account->uid])) {
+ $formats['user'][$account->uid] = array();
+ foreach ($formats['all'] as $format) {
+ if (filter_access($format, $account)) {
+ $formats['user'][$account->uid][$format->format] = $format;
+ }
+ }
+ }
+
+ return isset($account) ? $formats['user'][$account->uid] : $formats['all'];
+}
+
+/**
+ * Resets the text format caches.
+ *
+ * @see filter_formats()
+ */
+function filter_formats_reset() {
+ cache_clear_all('filter_formats', 'cache', TRUE);
+ cache_clear_all('filter_list_format', 'cache', TRUE);
+ drupal_static_reset('filter_list_format');
+ drupal_static_reset('filter_formats');
+}
+
+/**
+ * Retrieves a list of roles that are allowed to use a given text format.
+ *
+ * @param $format
+ * An object representing the text format.
+ *
+ * @return
+ * An array of role names, keyed by role ID.
+ */
+function filter_get_roles_by_format($format) {
+ // Handle the fallback format upfront (all roles have access to this format).
+ if ($format->format == filter_fallback_format()) {
+ return user_roles();
+ }
+ // Do not list any roles if the permission does not exist.
+ $permission = filter_permission_name($format);
+ return !empty($permission) ? user_roles(FALSE, $permission) : array();
+}
+
+/**
+ * Retrieves a list of text formats that are allowed for a given role.
+ *
+ * @param $rid
+ * The user role ID to retrieve text formats for.
+ *
+ * @return
+ * An array of text format objects that are allowed for the role, keyed by
+ * the text format ID and ordered by weight.
+ */
+function filter_get_formats_by_role($rid) {
+ $formats = array();
+ foreach (filter_formats() as $format) {
+ $roles = filter_get_roles_by_format($format);
+ if (isset($roles[$rid])) {
+ $formats[$format->format] = $format;
+ }
+ }
+ return $formats;
+}
+
+/**
+ * Returns the ID of the default text format for a particular user.
+ *
+ * The default text format is the first available format that the user is
+ * allowed to access, when the formats are ordered by weight. It should
+ * generally be used as a default choice when presenting the user with a list
+ * of possible text formats (for example, in a node creation form).
+ *
+ * Conversely, when existing content that does not have an assigned text format
+ * needs to be filtered for display, the default text format is the wrong
+ * choice, because it is not guaranteed to be consistent from user to user, and
+ * some trusted users may have an unsafe text format set by default, which
+ * should not be used on text of unknown origin. Instead, the fallback format
+ * returned by filter_fallback_format() should be used, since that is intended
+ * to be a safe, consistent format that is always available to all users.
+ *
+ * @param $account
+ * (optional) The user account to check. Defaults to the currently logged-in
+ * user. Defaults to NULL.
+ *
+ * @return
+ * The ID of the user's default text format.
+ *
+ * @see filter_fallback_format()
+ */
+function filter_default_format($account = NULL) {
+ global $user;
+ if (!isset($account)) {
+ $account = $user;
+ }
+ // Get a list of formats for this user, ordered by weight. The first one
+ // available is the user's default format.
+ $formats = filter_formats($account);
+ $format = reset($formats);
+ return $format->format;
+}
+
+/**
+ * Returns the ID of the fallback text format that all users have access to.
+ *
+ * The fallback text format is a regular text format in every respect, except
+ * it does not participate in the filter permission system and cannot be
+ * disabled. It needs to exist because any user who has permission to create
+ * formatted content must always have at least one text format they can use.
+ *
+ * Because the fallback format is available to all users, it should always be
+ * configured securely. For example, when the Filter module is installed, this
+ * format is initialized to output plain text. Installation profiles and site
+ * administrators have the freedom to configure it further.
+ *
+ * Note that the fallback format is completely distinct from the default format,
+ * which differs per user and is simply the first format which that user has
+ * access to. The default and fallback formats are only guaranteed to be the
+ * same for users who do not have access to any other format; otherwise, the
+ * fallback format's weight determines its placement with respect to the user's
+ * other formats.
+ *
+ * Any modules implementing a format deletion functionality must not delete this
+ * format.
+ *
+ * @return
+ * The ID of the fallback text format.
+ *
+ * @see hook_filter_format_disable()
+ * @see filter_default_format()
+ */
+function filter_fallback_format() {
+ // This variable is automatically set in the database for all installations
+ // of Drupal. In the event that it gets disabled or deleted somehow, there
+ // is no safe default to return, since we do not want to risk making an
+ // existing (and potentially unsafe) text format on the site automatically
+ // available to all users. Returning NULL at least guarantees that this
+ // cannot happen.
+ return variable_get('filter_fallback_format');
+}
+
+/**
+ * Returns the title of the fallback text format.
+ *
+ * @return string
+ * The title of the fallback text format.
+ */
+function filter_fallback_format_title() {
+ $fallback_format = filter_format_load(filter_fallback_format());
+ return filter_admin_format_title($fallback_format);
+}
+
+/**
+ * Returns a list of all filters provided by modules.
+ *
+ * @return array
+ * An array of filter formats.
+ */
+function filter_get_filters() {
+ $filters = &drupal_static(__FUNCTION__, array());
+
+ if (empty($filters)) {
+ foreach (module_implements('filter_info') as $module) {
+ $info = module_invoke($module, 'filter_info');
+ if (isset($info) && is_array($info)) {
+ // Assign the name of the module implementing the filters and ensure
+ // default values.
+ foreach (array_keys($info) as $name) {
+ $info[$name]['module'] = $module;
+ $info[$name] += array(
+ 'description' => '',
+ 'weight' => 0,
+ );
+ }
+ $filters = array_merge($filters, $info);
+ }
+ }
+ // Allow modules to alter filter definitions.
+ drupal_alter('filter_info', $filters);
+
+ uasort($filters, '_filter_list_cmp');
+ }
+
+ return $filters;
+}
+
+/**
+ * Sorts an array of filters by filter name.
+ *
+ * Callback for uasort() within filter_get_filters().
+ */
+function _filter_list_cmp($a, $b) {
+ return strcmp($a['title'], $b['title']);
+}
+
+/**
+ * Checks if the text in a certain text format is allowed to be cached.
+ *
+ * This function can be used to check whether the result of the filtering
+ * process can be cached. A text format may allow caching depending on the
+ * filters enabled.
+ *
+ * @param $format_id
+ * The text format ID to check.
+ *
+ * @return
+ * TRUE if the given text format allows caching, FALSE otherwise.
+ */
+function filter_format_allowcache($format_id) {
+ $format = filter_format_load($format_id);
+ return !empty($format->cache);
+}
+
+/**
+ * Helper function to determine whether the output of a given text format can be cached.
+ *
+ * The output of a given text format can be cached when all enabled filters in
+ * the text format allow caching.
+ *
+ * @param $format
+ * The text format object to check.
+ *
+ * @return
+ * TRUE if all the filters enabled in the given text format allow caching,
+ * FALSE otherwise.
+ *
+ * @see filter_format_save()
+ */
+function _filter_format_is_cacheable($format) {
+ if (empty($format->filters)) {
+ return TRUE;
+ }
+ $filter_info = filter_get_filters();
+ foreach ($format->filters as $name => $filter) {
+ // By default, 'cache' is TRUE for all filters unless specified otherwise.
+ if (!empty($filter['status']) && isset($filter_info[$name]['cache']) && !$filter_info[$name]['cache']) {
+ return FALSE;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * Retrieves a list of filters for a given text format.
+ *
+ * Note that this function returns all associated filters regardless of whether
+ * they are enabled or disabled. All functions working with the filter
+ * information outside of filter administration should test for $filter->status
+ * before performing actions with the filter.
+ *
+ * @param $format_id
+ * The format ID to retrieve filters for.
+ *
+ * @return
+ * An array of filter objects associated to the given text format, keyed by
+ * filter name.
+ */
+function filter_list_format($format_id) {
+ $filters = &drupal_static(__FUNCTION__, array());
+ $filter_info = filter_get_filters();
+
+ if (!isset($filters['all'])) {
+ if ($cache = cache_get('filter_list_format')) {
+ $filters['all'] = $cache->data;
+ }
+ else {
+ $result = db_query('SELECT * FROM {filter} ORDER BY weight, module, name');
+ foreach ($result as $record) {
+ $filters['all'][$record->format][$record->name] = $record;
+ }
+ cache_set('filter_list_format', $filters['all']);
+ }
+ }
+
+ if (!isset($filters[$format_id])) {
+ $format_filters = array();
+ $filter_map = isset($filters['all'][$format_id]) ? $filters['all'][$format_id] : array();
+ foreach ($filter_map as $name => $filter) {
+ if (isset($filter_info[$name])) {
+ $filter->title = $filter_info[$name]['title'];
+ // Unpack stored filter settings.
+ $filter->settings = (isset($filter->settings) ? unserialize($filter->settings) : array());
+ // Merge in default settings.
+ if (isset($filter_info[$name]['default settings'])) {
+ $filter->settings += $filter_info[$name]['default settings'];
+ }
+
+ $format_filters[$name] = $filter;
+ }
+ }
+ $filters[$format_id] = $format_filters;
+ }
+
+ return isset($filters[$format_id]) ? $filters[$format_id] : array();
+}
+
+/**
+ * Runs all the enabled filters on a piece of text.
+ *
+ * Note: Because filters can inject JavaScript or execute PHP code, security is
+ * vital here. When a user supplies a text format, you should validate it using
+ * filter_access() before accepting/using it. This is normally done in the
+ * validation stage of the Form API. You should for example never make a preview
+ * of content in a disallowed format.
+ *
+ * @param $text
+ * The text to be filtered.
+ * @param $format_id
+ * (optional) The format ID of the text to be filtered. If no format is
+ * assigned, the fallback format will be used. Defaults to NULL.
+ * @param $langcode
+ * (optional) The language code of the text to be filtered, e.g. 'en' for
+ * English. This allows filters to be language aware so language specific
+ * text replacement can be implemented. Defaults to an empty string.
+ * @param $cache
+ * (optional) A Boolean indicating whether to cache the filtered output in the
+ * {cache_filter} table. The caller may set this to FALSE when the output is
+ * already cached elsewhere to avoid duplicate cache lookups and storage.
+ * Defaults to FALSE.
+ *
+ * @return
+ * The filtered text.
+ *
+ * @ingroup sanitization
+ */
+function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE) {
+ if (!isset($format_id)) {
+ $format_id = filter_fallback_format();
+ }
+ // If the requested text format does not exist, the text cannot be filtered.
+ if (!$format = filter_format_load($format_id)) {
+ watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT);
+ return '';
+ }
+
+ // Check for a cached version of this piece of text.
+ $cache = $cache && !empty($format->cache);
+ $cache_id = '';
+ if ($cache) {
+ $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);
+ if ($cached = cache_get($cache_id, 'cache_filter')) {
+ return $cached->data;
+ }
+ }
+
+ // Convert all Windows and Mac newlines to a single newline, so filters only
+ // need to deal with one possibility.
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ // Get a complete list of filters, ordered properly.
+ $filters = filter_list_format($format->format);
+ $filter_info = filter_get_filters();
+
+ // Give filters the chance to escape HTML-like data such as code or formulas.
+ foreach ($filters as $name => $filter) {
+ if ($filter->status && isset($filter_info[$name]['prepare callback']) && function_exists($filter_info[$name]['prepare callback'])) {
+ $function = $filter_info[$name]['prepare callback'];
+ $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
+ }
+ }
+
+ // Perform filtering.
+ foreach ($filters as $name => $filter) {
+ if ($filter->status && isset($filter_info[$name]['process callback']) && function_exists($filter_info[$name]['process callback'])) {
+ $function = $filter_info[$name]['process callback'];
+ $text = $function($text, $filter, $format, $langcode, $cache, $cache_id);
+ }
+ }
+
+ // Cache the filtered text. This cache is infinitely valid. It becomes
+ // obsolete when $text changes (which leads to a new $cache_id). It is
+ // automatically flushed when the text format is updated.
+ // @see filter_format_save()
+ if ($cache) {
+ cache_set($cache_id, $text, 'cache_filter');
+ }
+
+ return $text;
+}
+
+/**
+ * Expands an element into a base element with text format selector attached.
+ *
+ * The form element will be expanded into two separate form elements, one
+ * holding the original element, and the other holding the text format selector:
+ * - value: Holds the original element, having its #type changed to the value of
+ * #base_type or 'textarea' by default.
+ * - format: Holds the text format fieldset and the text format selection, using
+ * the text format id specified in #format or the user's default format by
+ * default, if NULL.
+ *
+ * The resulting value for the element will be an array holding the value and
+ * the format. For example, the value for the body element will be:
+ * @code
+ * $form_state['values']['body']['value'] = 'foo';
+ * $form_state['values']['body']['format'] = 'foo';
+ * @endcode
+ *
+ * @param $element
+ * The form element to process. Properties used:
+ * - #base_type: The form element #type to use for the 'value' element.
+ * 'textarea' by default.
+ * - #format: (optional) The text format ID to preselect. If NULL or not set,
+ * the default format for the current user will be used.
+ *
+ * @return
+ * The expanded element.
+ */
+function filter_process_format($element) {
+ global $user;
+
+ // Ensure that children appear as subkeys of this element.
+ $element['#tree'] = TRUE;
+ $blacklist = array(
+ // Make form_builder() regenerate child properties.
+ '#parents',
+ '#id',
+ '#name',
+ // Do not copy this #process function to prevent form_builder() from
+ // recursing infinitely.
+ '#process',
+ // Description is handled by theme_text_format_wrapper().
+ '#description',
+ // Ensure proper ordering of children.
+ '#weight',
+ // Properties already processed for the parent element.
+ '#prefix',
+ '#suffix',
+ '#attached',
+ '#processed',
+ '#theme_wrappers',
+ );
+ // Move this element into sub-element 'value'.
+ unset($element['value']);
+ foreach (element_properties($element) as $key) {
+ if (!in_array($key, $blacklist)) {
+ $element['value'][$key] = $element[$key];
+ }
+ }
+
+ $element['value']['#type'] = $element['#base_type'];
+ $element['value'] += element_info($element['#base_type']);
+
+ // Turn original element into a text format wrapper.
+ $path = drupal_get_path('module', 'filter');
+ $element['#attached']['js'][] = $path . '/filter.js';
+ $element['#attached']['css'][] = $path . '/filter.css';
+
+ // Setup child container for the text format widget.
+ $element['format'] = array(
+ '#type' => 'fieldset',
+ '#attributes' => array('class' => array('filter-wrapper')),
+ );
+
+ // Prepare text format guidelines.
+ $element['format']['guidelines'] = array(
+ '#type' => 'container',
+ '#attributes' => array('class' => array('filter-guidelines')),
+ '#weight' => 20,
+ );
+ // Get a list of formats that the current user has access to.
+ $formats = filter_formats($user);
+ foreach ($formats as $format) {
+ $options[$format->format] = $format->name;
+ $element['format']['guidelines'][$format->format] = array(
+ '#theme' => 'filter_guidelines',
+ '#format' => $format,
+ );
+ }
+
+ // Use the default format for this user if none was selected.
+ if (!isset($element['#format'])) {
+ $element['#format'] = filter_default_format($user);
+ }
+
+ $element['format']['format'] = array(
+ '#type' => 'select',
+ '#title' => t('Text format'),
+ '#options' => $options,
+ '#default_value' => $element['#format'],
+ '#access' => count($formats) > 1,
+ '#weight' => 10,
+ '#attributes' => array('class' => array('filter-list')),
+ '#parents' => array_merge($element['#parents'], array('format')),
+ );
+
+ $element['format']['help'] = array(
+ '#type' => 'container',
+ '#theme' => 'filter_tips_more_info',
+ '#attributes' => array('class' => array('filter-help')),
+ '#weight' => 0,
+ );
+
+ $all_formats = filter_formats();
+ $format_exists = isset($all_formats[$element['#format']]);
+ $user_has_access = isset($formats[$element['#format']]);
+ $user_is_admin = user_access('administer filters');
+
+ // If the stored format does not exist, administrators have to assign a new
+ // format.
+ if (!$format_exists && $user_is_admin) {
+ $element['format']['format']['#required'] = TRUE;
+ $element['format']['format']['#default_value'] = NULL;
+ // Force access to the format selector (it may have been denied above if
+ // the user only has access to a single format).
+ $element['format']['format']['#access'] = TRUE;
+ }
+ // Disable this widget, if the user is not allowed to use the stored format,
+ // or if the stored format does not exist. The 'administer filters' permission
+ // only grants access to the filter administration, not to all formats.
+ elseif (!$user_has_access || !$format_exists) {
+ // Overload default values into #value to make them unalterable.
+ $element['value']['#value'] = $element['value']['#default_value'];
+ $element['format']['format']['#value'] = $element['format']['format']['#default_value'];
+
+ // Prepend #pre_render callback to replace field value with user notice
+ // prior to rendering.
+ $element['value'] += array('#pre_render' => array());
+ array_unshift($element['value']['#pre_render'], 'filter_form_access_denied');
+
+ // Cosmetic adjustments.
+ if (isset($element['value']['#rows'])) {
+ $element['value']['#rows'] = 3;
+ }
+ $element['value']['#disabled'] = TRUE;
+ $element['value']['#resizable'] = FALSE;
+
+ // Hide the text format selector and any other child element (such as text
+ // field's summary).
+ foreach (element_children($element) as $key) {
+ if ($key != 'value') {
+ $element[$key]['#access'] = FALSE;
+ }
+ }
+ }
+
+ return $element;
+}
+
+/**
+ * Render API callback: Hides the field value of 'text_format' elements.
+ *
+ * To not break form processing and previews if a user does not have access to a
+ * stored text format, the expanded form elements in filter_process_format() are
+ * forced to take over the stored #default_values for 'value' and 'format'.
+ * However, to prevent the unfiltered, original #value from being displayed to
+ * the user, we replace it with a friendly notice here.
+ *
+ * @see filter_process_format()
+ */
+function filter_form_access_denied($element) {
+ $element['#value'] = t('This field has been disabled because you do not have sufficient permissions to edit it.');
+ return $element;
+}
+
+/**
+ * Returns HTML for a text format-enabled form element.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element containing #children and #description.
+ *
+ * @ingroup themeable
+ */
+function theme_text_format_wrapper($variables) {
+ $element = $variables['element'];
+ $output = '
\n";
+
+ return $output;
+}
+
+/**
+ * Checks if a user has access to a particular text format.
+ *
+ * @param $format
+ * An object representing the text format.
+ * @param $account
+ * (optional) The user account to check access for; if omitted, the currently
+ * logged-in user is used. Defaults to NULL.
+ *
+ * @return
+ * Boolean TRUE if the user is allowed to access the given format.
+ */
+function filter_access($format, $account = NULL) {
+ global $user;
+ if (!isset($account)) {
+ $account = $user;
+ }
+ // Handle special cases up front. All users have access to the fallback
+ // format.
+ if ($format->format == filter_fallback_format()) {
+ return TRUE;
+ }
+ // Check the permission if one exists; otherwise, we have a non-existent
+ // format so we return FALSE.
+ $permission = filter_permission_name($format);
+ return !empty($permission) && user_access($permission, $account);
+}
+
+/**
+ * Retrieves the filter tips.
+ *
+ * @param $format_id
+ * The ID of the text format for which to retrieve tips, or -1 to return tips
+ * for all formats accessible to the current user.
+ * @param $long
+ * (optional) Boolean indicating whether the long form of tips should be
+ * returned. Defaults to FALSE.
+ *
+ * @return
+ * An associative array of filtering tips, keyed by filter name. Each
+ * filtering tip is an associative array with elements:
+ * - tip: Tip text.
+ * - id: Filter ID.
+ */
+function _filter_tips($format_id, $long = FALSE) {
+ global $user;
+
+ $formats = filter_formats($user);
+ $filter_info = filter_get_filters();
+
+ $tips = array();
+
+ // If only listing one format, extract it from the $formats array.
+ if ($format_id != -1) {
+ $formats = array($formats[$format_id]);
+ }
+
+ foreach ($formats as $format) {
+ $filters = filter_list_format($format->format);
+ $tips[$format->name] = array();
+ foreach ($filters as $name => $filter) {
+ if ($filter->status && isset($filter_info[$name]['tips callback']) && function_exists($filter_info[$name]['tips callback'])) {
+ $tip = $filter_info[$name]['tips callback']($filter, $format, $long);
+ if (isset($tip)) {
+ $tips[$format->name][$name] = array('tip' => $tip, 'id' => $name);
+ }
+ }
+ }
+ }
+
+ return $tips;
+}
+
+/**
+ * Parses an HTML snippet and returns it as a DOM object.
+ *
+ * This function loads the body part of a partial (X)HTML document and returns
+ * a full DOMDocument object that represents this document. You can use
+ * filter_dom_serialize() to serialize this DOMDocument back to a XHTML
+ * snippet.
+ *
+ * @param $text
+ * The partial (X)HTML snippet to load. Invalid mark-up will be corrected on
+ * import.
+ * @return
+ * A DOMDocument that represents the loaded (X)HTML snippet.
+ */
+function filter_dom_load($text) {
+ $dom_document = new DOMDocument();
+ // Ignore warnings during HTML soup loading.
+ @$dom_document->loadHTML('' . $text . '');
+
+ return $dom_document;
+}
+
+/**
+ * Converts a DOM object back to an HTML snippet.
+ *
+ * The function serializes the body part of a DOMDocument back to an XHTML
+ * snippet. The resulting XHTML snippet will be properly formatted to be
+ * compatible with HTML user agents.
+ *
+ * @param $dom_document
+ * A DOMDocument object to serialize, only the tags below
+ * the first node will be converted.
+ *
+ * @return
+ * A valid (X)HTML snippet, as a string.
+ */
+function filter_dom_serialize($dom_document) {
+ $body_node = $dom_document->getElementsByTagName('body')->item(0);
+ $body_content = '';
+
+ foreach ($body_node->getElementsByTagName('script') as $node) {
+ filter_dom_serialize_escape_cdata_element($dom_document, $node);
+ }
+
+ foreach ($body_node->getElementsByTagName('style') as $node) {
+ filter_dom_serialize_escape_cdata_element($dom_document, $node, '/*', '*/');
+ }
+
+ foreach ($body_node->childNodes as $child_node) {
+ $body_content .= $dom_document->saveXML($child_node);
+ }
+ return preg_replace('|<([^> ]*)/>|i', '<$1 />', $body_content);
+}
+
+/**
+ * Adds comments around the childNodes as $node) {
+ if (get_class($node) == 'DOMCdataSection') {
+ // See drupal_get_js(). This code is more or less duplicated there.
+ $embed_prefix = "\n{$comment_end}\n";
+
+ // Prevent invalid cdata escaping as this would throw a DOM error.
+ // This is the same behavior as found in libxml2.
+ // Related W3C standard: http://www.w3.org/TR/REC-xml/#dt-cdsection
+ // Fix explanation: http://en.wikipedia.org/wiki/CDATA#Nesting
+ $data = str_replace(']]>', ']]]]>', $node->data);
+
+ $fragment = $dom_document->createDocumentFragment();
+ $fragment->appendXML($embed_prefix . $data . $embed_suffix);
+ $dom_element->appendChild($fragment);
+ $dom_element->removeChild($node);
+ }
+ }
+}
+
+/**
+ * Returns HTML for a link to the more extensive filter tips.
+ *
+ * @ingroup themeable
+ */
+function theme_filter_tips_more_info() {
+ return '
' . l(t('More information about text formats'), 'filter/tips', array('attributes' => array('target' => '_blank'))) . '
';
+}
+
+/**
+ * Returns HTML for guidelines for a text format.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - format: An object representing a text format.
+ *
+ * @ingroup themeable
+ */
+function theme_filter_guidelines($variables) {
+ $format = $variables['format'];
+ $attributes['class'][] = 'filter-guidelines-item';
+ $attributes['class'][] = 'filter-guidelines-' . $format->format;
+ $output = '
';
+ $tips = array(
+ 'a' => array(t('Anchors are used to make links to other pages.'), '' . check_plain(variable_get('site_name', 'Drupal')) . ''),
+ 'br' => array(t('By default line break tags are automatically added, so use this tag to add additional ones. Use of this tag is different because it is not used with an open/close pair like all the others. Use the extra " /" inside the tag to maintain XHTML 1.0 compatibility'), t('Text with line break')),
+ 'p' => array(t('By default paragraph tags are automatically added, so use this tag to add additional ones.'), '
'),
+ 'tr' => NULL, 'td' => NULL, 'th' => NULL,
+ 'del' => array(t('Deleted'), '' . t('Deleted') . ''),
+ 'ins' => array(t('Inserted'), '' . t('Inserted') . ''),
+ // Assumes and describes li.
+ 'ol' => array(t('Ordered list - use the <li> to begin each list item'), '
' . t('First item') . '
' . t('Second item') . '
'),
+ 'ul' => array(t('Unordered list - use the <li> to begin each list item'), '
' . t('First item') . '
' . t('Second item') . '
'),
+ 'li' => NULL,
+ // Assumes and describes dt and dd.
+ 'dl' => array(t('Definition lists are similar to other HTML lists. <dl> begins the definition list, <dt> begins the definition term and <dd> begins the definition description.'), '
' . t('Most unusual characters can be directly entered without any problems.') . '
';
+ $output .= '
' . t('If you do encounter problems, try using HTML character entities. A common example looks like & for an ampersand & character. For a full list of entities see HTML\'s entities page. Some of the available characters include:', array('@html-entities' => 'http://www.w3.org/TR/html4/sgml/entities.html')) . '
';
+
+ $entities = array(
+ array(t('Ampersand'), '&'),
+ array(t('Greater than'), '>'),
+ array(t('Less than'), '<'),
+ array(t('Quotation mark'), '"'),
+ );
+ $header = array(t('Character Description'), t('You Type'), t('You Get'));
+ unset($rows);
+ foreach ($entities as $entity) {
+ $rows[] = array(
+ array('data' => $entity[0], 'class' => array('description')),
+ array('data' => '' . check_plain($entity[1]) . '', 'class' => array('type')),
+ array('data' => $entity[1], 'class' => array('get'))
+ );
+ }
+ $output .= theme('table', array('header' => $header, 'rows' => $rows));
+ return $output;
+}
+
+/**
+ * Implements callback_filter_settings().
+ *
+ * Provides settings for the URL filter.
+ *
+ * @see filter_filter_info()
+ */
+function _filter_url_settings($form, &$form_state, $filter, $format, $defaults) {
+ $filter->settings += $defaults;
+
+ $settings['filter_url_length'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum link text length'),
+ '#default_value' => $filter->settings['filter_url_length'],
+ '#size' => 5,
+ '#maxlength' => 4,
+ '#field_suffix' => t('characters'),
+ '#description' => t('URLs longer than this number of characters will be truncated to prevent long strings that break formatting. The link itself will be retained; just the text portion of the link will be truncated.'),
+ '#element_validate' => array('element_validate_integer_positive'),
+ );
+ return $settings;
+}
+
+/**
+ * Implements callback_filter_process().
+ *
+ * Converts text into hyperlinks automatically.
+ *
+ * This filter identifies and makes clickable three types of "links".
+ * - URLs like http://example.com.
+ * - E-mail addresses like name@example.com.
+ * - Web addresses without the "http://" protocol defined, like www.example.com.
+ * Each type must be processed separately, as there is no one regular
+ * expression that could possibly match all of the cases in one pass.
+ */
+function _filter_url($text, $filter) {
+ // Tags to skip and not recurse into.
+ $ignore_tags = 'a|script|style|code|pre';
+
+ // Pass length to regexp callback.
+ _filter_url_trim(NULL, $filter->settings['filter_url_length']);
+
+ // Create an array which contains the regexps for each type of link.
+ // The key to the regexp is the name of a function that is used as
+ // callback function to process matches of the regexp. The callback function
+ // is to return the replacement for the match. The array is used and
+ // matching/replacement done below inside some loops.
+ $tasks = array();
+
+ // Prepare protocols pattern for absolute URLs.
+ // check_url() will replace any bad protocols with HTTP, so we need to support
+ // the identical list. While '//' is technically optional for MAILTO only,
+ // we cannot cleanly differ between protocols here without hard-coding MAILTO,
+ // so '//' is optional for all protocols.
+ // @see filter_xss_bad_protocol()
+ $protocols = variable_get('filter_allowed_protocols', array('ftp', 'http', 'https', 'irc', 'mailto', 'news', 'nntp', 'rtsp', 'sftp', 'ssh', 'tel', 'telnet', 'webcal'));
+ $protocols = implode(':(?://)?|', $protocols) . ':(?://)?';
+
+ // Prepare domain name pattern.
+ // The ICANN seems to be on track towards accepting more diverse top level
+ // domains, so this pattern has been "future-proofed" to allow for TLDs
+ // of length 2-64.
+ $domain = '(?:[A-Za-z0-9._+-]+\.)?[A-Za-z]{2,64}\b';
+ $ip = '(?:[0-9]{1,3}\.){3}[0-9]{1,3}';
+ $auth = '[a-zA-Z0-9:%_+*~#?&=.,/;-]+@';
+ $trail = '[a-zA-Z0-9:%_+*~#&\[\]=/;?!\.,-]*[a-zA-Z0-9:%_+*~#&\[\]=/;-]';
+
+ // Prepare pattern for optional trailing punctuation.
+ // Even these characters could have a valid meaning for the URL, such usage is
+ // rare compared to using a URL at the end of or within a sentence, so these
+ // trailing characters are optionally excluded.
+ $punctuation = '[\.,?!]*?';
+
+ // Match absolute URLs.
+ $url_pattern = "(?:$auth)?(?:$domain|$ip)/?(?:$trail)?";
+ $pattern = "`((?:$protocols)(?:$url_pattern))($punctuation)`";
+ $tasks['_filter_url_parse_full_links'] = $pattern;
+
+ // Match e-mail addresses.
+ $url_pattern = "[A-Za-z0-9._-]{1,254}@(?:$domain)";
+ $pattern = "`($url_pattern)`";
+ $tasks['_filter_url_parse_email_links'] = $pattern;
+
+ // Match www domains.
+ $url_pattern = "www\.(?:$domain)/?(?:$trail)?";
+ $pattern = "`($url_pattern)($punctuation)`";
+ $tasks['_filter_url_parse_partial_links'] = $pattern;
+
+ // Each type of URL needs to be processed separately. The text is joined and
+ // re-split after each task, since all injected HTML tags must be correctly
+ // protected before the next task.
+ foreach ($tasks as $task => $pattern) {
+ // HTML comments need to be handled separately, as they may contain HTML
+ // markup, especially a '>'. Therefore, remove all comment contents and add
+ // them back later.
+ _filter_url_escape_comments('', TRUE);
+ $text = preg_replace_callback('``s', '_filter_url_escape_comments', $text);
+
+ // Split at all tags; ensures that no tags or attributes are processed.
+ $chunks = preg_split('/(<.+?>)/is', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // PHP ensures that the array consists of alternating delimiters and
+ // literals, and begins and ends with a literal (inserting NULL as
+ // required). Therefore, the first chunk is always text:
+ $chunk_type = 'text';
+ // If a tag of $ignore_tags is found, it is stored in $open_tag and only
+ // removed when the closing tag is found. Until the closing tag is found,
+ // no replacements are made.
+ $open_tag = '';
+
+ for ($i = 0; $i < count($chunks); $i++) {
+ if ($chunk_type == 'text') {
+ // Only process this text if there are no unclosed $ignore_tags.
+ if ($open_tag == '') {
+ // If there is a match, inject a link into this chunk via the callback
+ // function contained in $task.
+ $chunks[$i] = preg_replace_callback($pattern, $task, $chunks[$i]);
+ }
+ // Text chunk is done, so next chunk must be a tag.
+ $chunk_type = 'tag';
+ }
+ else {
+ // Only process this tag if there are no unclosed $ignore_tags.
+ if ($open_tag == '') {
+ // Check whether this tag is contained in $ignore_tags.
+ if (preg_match("`<($ignore_tags)(?:\s|>)`i", $chunks[$i], $matches)) {
+ $open_tag = $matches[1];
+ }
+ }
+ // Otherwise, check whether this is the closing tag for $open_tag.
+ else {
+ if (preg_match("`<\/$open_tag>`i", $chunks[$i], $matches)) {
+ $open_tag = '';
+ }
+ }
+ // Tag chunk is done, so next chunk must be text.
+ $chunk_type = 'text';
+ }
+ }
+
+ $text = implode($chunks);
+ // Revert back to the original comment contents
+ _filter_url_escape_comments('', FALSE);
+ $text = preg_replace_callback('``', '_filter_url_escape_comments', $text);
+ }
+
+ return $text;
+}
+
+/**
+ * Makes links out of absolute URLs.
+ *
+ * Callback for preg_replace_callback() within _filter_url().
+ */
+function _filter_url_parse_full_links($match) {
+ // The $i:th parenthesis in the regexp contains the URL.
+ $i = 1;
+
+ $match[$i] = decode_entities($match[$i]);
+ $caption = check_plain(_filter_url_trim($match[$i]));
+ $match[$i] = check_plain($match[$i]);
+ return '' . $caption . '' . $match[$i + 1];
+}
+
+/**
+ * Makes links out of e-mail addresses.
+ *
+ * Callback for preg_replace_callback() within _filter_url().
+ */
+function _filter_url_parse_email_links($match) {
+ // The $i:th parenthesis in the regexp contains the URL.
+ $i = 0;
+
+ $match[$i] = decode_entities($match[$i]);
+ $caption = check_plain(_filter_url_trim($match[$i]));
+ $match[$i] = check_plain($match[$i]);
+ return '' . $caption . '';
+}
+
+/**
+ * Makes links out of domain names starting with "www."
+ *
+ * Callback for preg_replace_callback() within _filter_url().
+ */
+function _filter_url_parse_partial_links($match) {
+ // The $i:th parenthesis in the regexp contains the URL.
+ $i = 1;
+
+ $match[$i] = decode_entities($match[$i]);
+ $caption = check_plain(_filter_url_trim($match[$i]));
+ $match[$i] = check_plain($match[$i]);
+ return '' . $caption . '' . $match[$i + 1];
+}
+
+/**
+ * Escapes the contents of HTML comments.
+ *
+ * Callback for preg_replace_callback() within _filter_url().
+ *
+ * @param $match
+ * An array containing matches to replace from preg_replace_callback(),
+ * whereas $match[1] is expected to contain the content to be filtered.
+ * @param $escape
+ * (optional) A Boolean indicating whether to escape (TRUE) or unescape
+ * comments (FALSE). Defaults to NULL, indicating neither. If TRUE, statically
+ * cached $comments are reset.
+ */
+function _filter_url_escape_comments($match, $escape = NULL) {
+ static $mode, $comments = array();
+
+ if (isset($escape)) {
+ $mode = $escape;
+ if ($escape){
+ $comments = array();
+ }
+ return;
+ }
+
+ // Replace all HTML coments with a '' placeholder.
+ if ($mode) {
+ $content = $match[1];
+ $hash = md5($content);
+ $comments[$hash] = $content;
+ return "";
+ }
+ // Or replace placeholders with actual comment contents.
+ else {
+ $hash = $match[1];
+ $hash = trim($hash);
+ $content = $comments[$hash];
+ return "";
+ }
+}
+
+/**
+ * Shortens long URLs to http://www.example.com/long/url...
+ */
+function _filter_url_trim($text, $length = NULL) {
+ static $_length;
+ if ($length !== NULL) {
+ $_length = $length;
+ }
+
+ // Use +3 for '...' string length.
+ if ($_length && strlen($text) > $_length + 3) {
+ $text = substr($text, 0, $_length) . '...';
+ }
+
+ return $text;
+}
+
+/**
+ * Implements callback_filter_tips().
+ *
+ * Provides help for the URL filter.
+ *
+ * @see filter_filter_info()
+ */
+function _filter_url_tips($filter, $format, $long = FALSE) {
+ return t('Web page addresses and e-mail addresses turn into links automatically.');
+}
+
+/**
+ * Implements callback_filter_process().
+ *
+ * Scans the input and makes sure that HTML tags are properly closed.
+ */
+function _filter_htmlcorrector($text) {
+ return filter_dom_serialize(filter_dom_load($text));
+}
+
+/**
+ * Implements callback_filter_process().
+ *
+ * Converts line breaks into
and in an intelligent fashion.
+ *
+ * Based on: http://photomatt.net/scripts/autop
+ */
+function _filter_autop($text) {
+ // All block level tags
+ $block = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|p|h[1-6]|hr)';
+
+ // Split at opening and closing PRE, SCRIPT, STYLE, OBJECT, IFRAME tags
+ // and comments. We don't apply any processing to the contents of these tags
+ // to avoid messing up code. We look for matched pairs and allow basic
+ // nesting. For example:
+ // "processed
ignored ignored
processed"
+ $chunks = preg_split('@(|?(?:pre|script|style|object|iframe|!--)[^>]*>)@i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Note: PHP ensures the array consists of alternating delimiters and literals
+ // and begins and ends with a literal (inserting NULL as required).
+ $ignore = FALSE;
+ $ignoretag = '';
+ $output = '';
+ foreach ($chunks as $i => $chunk) {
+ if ($i % 2) {
+ $comment = (substr($chunk, 0, 4) == ' Two.\n\n" => array(
+ '' => TRUE,
+ "" => TRUE,
+ ),
+ // Resulting HTML should produce matching paragraph tags.
+ '
+Quoted text linking to www.example.com, written by person@example.com, originating from http://origin.example.com. @see www.usage.example.com or www.example.info bla bla.
+
+
+Outro.
+' => array(
+ 'href="http://www.example.com"' => TRUE,
+ 'href="mailto:person@example.com"' => TRUE,
+ 'href="http://origin.example.com"' => TRUE,
+ 'http://www.usage.example.com' => FALSE,
+ 'http://www.example.info' => FALSE,
+ 'Intro.' => TRUE,
+ 'Outro.' => TRUE,
+ ),
+ '
+Unknown tag containing x and www.example.com? And a tag beginning with p and containing www.example.pooh with p?
+' => array(
+ 'href="http://www.example.com"' => TRUE,
+ 'href="http://www.example.pooh"' => TRUE,
+ ),
+ '
+
Test <br/>: This is a www.example17.com example with various http://www.example18.com tags. *
+ It is important www.example19.com to * test different URLs and http://www.example20.com in the same paragraph. *
+HTML www.example21.com soup by person@example22.com can litererally http://www.example23.com contain *img* anything. Just a www.example24.com with http://www.example25.com thrown in. www.example26.com from person@example27.com with extra http://www.example28.com.
+' => array(
+ 'href="http://www.example17.com"' => TRUE,
+ 'href="http://www.example18.com"' => TRUE,
+ 'href="http://www.example19.com"' => TRUE,
+ 'href="http://www.example20.com"' => TRUE,
+ 'href="http://www.example21.com"' => TRUE,
+ 'href="mailto:person@example22.com"' => TRUE,
+ 'href="http://www.example23.com"' => TRUE,
+ 'href="http://www.example24.com"' => TRUE,
+ 'href="http://www.example25.com"' => TRUE,
+ 'href="http://www.example26.com"' => TRUE,
+ 'href="mailto:person@example27.com"' => TRUE,
+ 'href="http://www.example28.com"' => TRUE,
+ ),
+ '
+
+' => array(
+ 'href="http://www.example.com"' => FALSE,
+ 'href="http://example.net"' => FALSE,
+ ),
+ '
+
+' => array(
+ 'href' => FALSE,
+ ),
+ '
+
+' => array(
+ 'href' => FALSE,
+ ),
+ '
+
+' => array(
+ 'href' => FALSE,
+ ),
+ '
+
+' => array(
+ 'href' => FALSE,
+ ),
+ '
+
+
www.example.com
+
http://example.com
+
person@example.com
+
Check www.example.net
+
Some text around http://www.example.info by person@example.info?
'
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Tests URL filter on longer content.
+ *
+ * Filters based on regular expressions should also be tested with a more
+ * complex content than just isolated test lines.
+ * The most common errors are:
+ * - accidental '*' (greedy) match instead of '*?' (minimal) match.
+ * - only matching first occurrence instead of all.
+ * - newlines not matching '.*'.
+ *
+ * This test covers:
+ * - Document with multiple newlines and paragraphs (two newlines).
+ * - Mix of several HTML tags, invalid non-HTML tags, tags to ignore and HTML
+ * comments.
+ * - Empty HTML tags (BR, IMG).
+ * - Mix of absolute and partial URLs, and e-mail addresses in one content.
+ */
+ function testUrlFilterContent() {
+ // Setup dummy filter object.
+ $filter = new stdClass();
+ $filter->settings = array(
+ 'filter_url_length' => 496,
+ );
+ $path = drupal_get_path('module', 'filter') . '/tests';
+
+ $input = file_get_contents($path . '/filter.url-input.txt');
+ $expected = file_get_contents($path . '/filter.url-output.txt');
+ $result = _filter_url($input, $filter);
+ $this->assertIdentical($result, $expected, 'Complex HTML document was correctly processed.');
+ }
+
+ /**
+ * Tests the HTML corrector filter.
+ *
+ * @todo This test could really use some validity checking function.
+ */
+ function testHtmlCorrectorFilter() {
+ // Tag closing.
+ $f = _filter_htmlcorrector('
text');
+ $this->assertEqual($f, '
text
', 'HTML corrector -- tag closing at the end of input.');
+
+ $f = _filter_htmlcorrector('
', 'HTML corrector -- Convert uppercased tags to proper lowercased ones.');
+
+ $f = _filter_htmlcorrector('test');
+ $this->assertEqual($f, 'test', 'HTML corrector -- Let proper XHTML pass through.');
+
+ $f = _filter_htmlcorrector('test');
+ $this->assertEqual($f, 'test', 'HTML corrector -- Let proper XHTML pass through, but ensure there is a single space before the closing slash.');
+
+ $f = _filter_htmlcorrector('test');
+ $this->assertEqual($f, 'test', 'HTML corrector -- Let proper XHTML pass through, but ensure there are not too many spaces before the closing slash.');
+
+ $f = _filter_htmlcorrector('');
+ $this->assertEqual($f, '', 'HTML corrector -- Convert XHTML that is properly formed but that would not be compatible with typical HTML user agents.');
+
+ $f = _filter_htmlcorrector('test1 test2');
+ $this->assertEqual($f, 'test1 test2', 'HTML corrector -- Automatically close single tags.');
+
+ $f = _filter_htmlcorrector('line1line2');
+ $this->assertEqual($f, 'line1line2', 'HTML corrector -- Automatically close single tags.');
+
+ $f = _filter_htmlcorrector('line1line2');
+ $this->assertEqual($f, 'line1line2', 'HTML corrector -- Automatically close single tags.');
+
+ $f = _filter_htmlcorrector('test');
+ $this->assertEqual($f, 'test', 'HTML corrector -- Automatically close single tags.');
+
+ $f = _filter_htmlcorrector(' ');
+ $this->assertEqual($f, ' ', "HTML corrector -- Transform empty tags to a single closed tag if the tag's content model is EMPTY.");
+
+ $f = _filter_htmlcorrector('');
+ $this->assertEqual($f, '', "HTML corrector -- Do not transform empty tags to a single closed tag if the tag's content model is not EMPTY.");
+
+ $f = _filter_htmlcorrector('
line1
line2');
+ $this->assertEqual($f, '
line1
line2', 'HTML corrector -- Move non-inline elements outside of inline containers.');
+
+ $f = _filter_htmlcorrector('
line1
line2
');
+ $this->assertEqual($f, '
line1
line2
', 'HTML corrector -- Move non-inline elements outside of inline containers.');
+
+ $f = _filter_htmlcorrector('
');
+ $this->assertEqual($filtered_data, '',
+ format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '
+');
+ $this->assertEqual($filtered_data, '',
+ format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '
+',
+ format_string('HTML corrector -- Existing cdata section @pattern_name properly escaped', array('@pattern_name' => '// assertTrue(strpos(strtolower(decode_entities($haystack)), $needle) !== FALSE, $message, $group);
+ }
+
+ /**
+ * Asserts that text transformed to lowercase with HTML entities decoded does not contain a given string.
+ *
+ * Otherwise fails the test with a given message, similar to all the
+ * SimpleTest assert* functions.
+ *
+ * Note that this does not remove nulls, new lines, and other character that
+ * could be used to obscure a tag or an attribute name.
+ *
+ * @param $haystack
+ * Text to look in.
+ * @param $needle
+ * Lowercase, plain text to look for.
+ * @param $message
+ * (optional) Message to display if failed. Defaults to an empty string.
+ * @param $group
+ * (optional) The group this message belongs to. Defaults to 'Other'.
+ * @return
+ * TRUE on pass, FALSE on fail.
+ */
+ function assertNoNormalized($haystack, $needle, $message = '', $group = 'Other') {
+ return $this->assertTrue(strpos(strtolower(decode_entities($haystack)), $needle) === FALSE, $message, $group);
+ }
+}
+
+/**
+ * Tests for Filter's hook invocations.
+ */
+class FilterHooksTestCase extends DrupalWebTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Filter format hooks',
+ 'description' => 'Test hooks for text formats insert/update/disable.',
+ 'group' => 'Filter',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('block', 'filter_test');
+ $admin_user = $this->drupalCreateUser(array('administer filters', 'administer blocks'));
+ $this->drupalLogin($admin_user);
+ }
+
+ /**
+ * Tests hooks on format management.
+ *
+ * Tests that hooks run correctly on creating, editing, and deleting a text
+ * format.
+ */
+ function testFilterHooks() {
+ // Add a text format.
+ $name = $this->randomName();
+ $edit = array();
+ $edit['format'] = drupal_strtolower($this->randomName());
+ $edit['name'] = $name;
+ $edit['roles[' . DRUPAL_ANONYMOUS_RID . ']'] = 1;
+ $this->drupalPost('admin/config/content/formats/add', $edit, t('Save configuration'));
+ $this->assertRaw(t('Added text format %format.', array('%format' => $name)), 'New format created.');
+ $this->assertText('hook_filter_format_insert invoked.', 'hook_filter_format_insert was invoked.');
+
+ $format_id = $edit['format'];
+
+ // Update text format.
+ $edit = array();
+ $edit['roles[' . DRUPAL_AUTHENTICATED_RID . ']'] = 1;
+ $this->drupalPost('admin/config/content/formats/' . $format_id, $edit, t('Save configuration'));
+ $this->assertRaw(t('The text format %format has been updated.', array('%format' => $name)), 'Format successfully updated.');
+ $this->assertText('hook_filter_format_update invoked.', 'hook_filter_format_update() was invoked.');
+
+ // Add a new custom block.
+ $custom_block = array();
+ $custom_block['info'] = $this->randomName(8);
+ $custom_block['title'] = $this->randomName(8);
+ $custom_block['body[value]'] = $this->randomName(32);
+ // Use the format created.
+ $custom_block['body[format]'] = $format_id;
+ $this->drupalPost('admin/structure/block/add', $custom_block, t('Save block'));
+ $this->assertText(t('The block has been created.'), 'New block successfully created.');
+
+ // Verify the new block is in the database.
+ $bid = db_query("SELECT bid FROM {block_custom} WHERE info = :info", array(':info' => $custom_block['info']))->fetchField();
+ $this->assertNotNull($bid, 'New block found in database');
+
+ // Disable the text format.
+ $this->drupalPost('admin/config/content/formats/' . $format_id . '/disable', array(), t('Disable'));
+ $this->assertRaw(t('Disabled text format %format.', array('%format' => $name)), 'Format successfully disabled.');
+ $this->assertText('hook_filter_format_disable invoked.', 'hook_filter_format_disable() was invoked.');
+ }
+}
+
+/**
+ * Tests filter settings.
+ */
+class FilterSettingsTestCase extends DrupalWebTestCase {
+ /**
+ * The installation profile to use with this test class.
+ *
+ * @var string
+ */
+ protected $profile = 'testing';
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Filter settings',
+ 'description' => 'Tests filter settings.',
+ 'group' => 'Filter',
+ );
+ }
+
+ /**
+ * Tests explicit and implicit default settings for filters.
+ */
+ function testFilterDefaults() {
+ $filter_info = filter_filter_info();
+ $filters = array_fill_keys(array_keys($filter_info), array());
+
+ // Create text format using filter default settings.
+ $filter_defaults_format = (object) array(
+ 'format' => 'filter_defaults',
+ 'name' => 'Filter defaults',
+ 'filters' => $filters,
+ );
+ filter_format_save($filter_defaults_format);
+
+ // Verify that default weights defined in hook_filter_info() were applied.
+ $saved_settings = array();
+ foreach ($filter_defaults_format->filters as $name => $settings) {
+ $expected_weight = (isset($filter_info[$name]['weight']) ? $filter_info[$name]['weight'] : 0);
+ $this->assertEqual($settings['weight'], $expected_weight, format_string('@name filter weight %saved equals %default', array(
+ '@name' => $name,
+ '%saved' => $settings['weight'],
+ '%default' => $expected_weight,
+ )));
+ $saved_settings[$name]['weight'] = $expected_weight;
+ }
+
+ // Re-save the text format.
+ filter_format_save($filter_defaults_format);
+ // Reload it from scratch.
+ filter_formats_reset();
+ $filter_defaults_format = filter_format_load($filter_defaults_format->format);
+ $filter_defaults_format->filters = filter_list_format($filter_defaults_format->format);
+
+ // Verify that saved filter settings have not been changed.
+ foreach ($filter_defaults_format->filters as $name => $settings) {
+ $this->assertEqual($settings->weight, $saved_settings[$name]['weight'], format_string('@name filter weight %saved equals %previous', array(
+ '@name' => $name,
+ '%saved' => $settings->weight,
+ '%previous' => $saved_settings[$name]['weight'],
+ )));
+ }
+ }
+}
diff --git a/drupal-dev/modules/filter/tests/filter.url-input.txt b/drupal-dev/modules/filter/tests/filter.url-input.txt
new file mode 100644
index 0000000..7b33af5
--- /dev/null
+++ b/drupal-dev/modules/filter/tests/filter.url-input.txt
@@ -0,0 +1,36 @@
+This is just a www.test.com. paragraph with person@test.com. some http://www.test.com. urls thrown in and also using www.test.com the code tag.
+
+
+This is just a www.test.com. paragraph with person@test.com. some http://www.test.com. urls thrown in and also using www.test.com the code tag.
+
+
+Testing code tag http://www.test.com abc
+
+http://www.test.com
+www.test.com
+person@test.com
+www.test.com
+
+What about tags that don't exist like x say www.test.com? And what about tag beginning www.test.com with p?
+
+Test <br/>: This is just a www.test.com. paragraph with some http://www.test.com urls thrown in. * This is just a www.test.com paragraph * with some http://www.test.com urls thrown in. * This is just a www.test.com paragraph person@test.com with some http://www.test.com urls *img* thrown in. This is just a www.test.com paragraph with some http://www.test.com urls thrown in. This is just a www.test.com paragraph person@test.com with some http://www.test.com urls thrown in.
+
+This is just a www.test.com paragraph with some http://www.test.com urls thrown in. This is just a www.test.com paragraph with some http://www.test.com urls thrown in. This is just a www.test.com paragraph person@test.com with some http://www.test.com urls thrown in. This is just a www.test.com paragraph with some http://www.test.com urls thrown in. This is just a www.test.com paragraph person@test.com with some http://www.test.com urls thrown in.
+
+The old URL filter has problems with this kind of link with www address as part of text in title. www.test.com
+
+
+
+
+
www.test.com
+
http://www.test.com
+
person@test.com
+
check www.test.com
+
this with some text around: http://www.test.com not so easy person@test.com now?
+
+
+
+
+This is the end!
\ No newline at end of file
diff --git a/drupal-dev/modules/filter/tests/filter.url-output.txt b/drupal-dev/modules/filter/tests/filter.url-output.txt
new file mode 100644
index 0000000..9cc5073
--- /dev/null
+++ b/drupal-dev/modules/filter/tests/filter.url-output.txt
@@ -0,0 +1,36 @@
+This is just a www.test.com. paragraph with person@test.com. some http://www.test.com. urls thrown in and also using www.test.com the code tag.
+
+
+
+
+
+This is the end!
\ No newline at end of file
diff --git a/drupal-dev/modules/forum/forum-icon.tpl.php b/drupal-dev/modules/forum/forum-icon.tpl.php
new file mode 100644
index 0000000..fd1cd13
--- /dev/null
+++ b/drupal-dev/modules/forum/forum-icon.tpl.php
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/drupal-dev/modules/forum/forum-list.tpl.php b/drupal-dev/modules/forum/forum-list.tpl.php
new file mode 100644
index 0000000..01c74a3
--- /dev/null
+++ b/drupal-dev/modules/forum/forum-list.tpl.php
@@ -0,0 +1,77 @@
+is_container: TRUE if the forum can contain other forums. FALSE
+ * if the forum can contain only topics.
+ * - $forum->depth: How deep the forum is in the current hierarchy.
+ * - $forum->zebra: 'even' or 'odd' string used for row class.
+ * - $forum->icon_class: 'default' or 'new' string used for forum icon class.
+ * - $forum->icon_title: Text alternative for the forum icon.
+ * - $forum->name: The name of the forum.
+ * - $forum->link: The URL to link to this forum.
+ * - $forum->description: The description of this forum.
+ * - $forum->new_topics: TRUE if the forum contains unread posts.
+ * - $forum->new_url: A URL to the forum's unread posts.
+ * - $forum->new_text: Text for the above URL, which tells how many new posts.
+ * - $forum->old_topics: A count of posts that have already been read.
+ * - $forum->num_posts: The total number of posts in the forum.
+ * - $forum->last_reply: Text representing the last time a forum was posted or
+ * commented in.
+ * - $forum_id: Forum ID for the current forum. Parent to all items within the
+ * $forums array.
+ *
+ * @see template_preprocess_forum_list()
+ * @see theme_forum_list()
+ *
+ * @ingroup themeable
+ */
+?>
+
diff --git a/drupal-dev/modules/forum/forum-rtl.css b/drupal-dev/modules/forum/forum-rtl.css
new file mode 100644
index 0000000..3f2a88b
--- /dev/null
+++ b/drupal-dev/modules/forum/forum-rtl.css
@@ -0,0 +1,24 @@
+/**
+ * @file
+ * Right-to-left styling for the Forum module.
+ */
+
+#forum td.forum .icon {
+ float: right;
+ margin: 0 0 0 9px;
+}
+#forum div.indent {
+ margin-left: 0;
+ margin-right: 20px;
+}
+.forum-topic-navigation {
+ padding: 1em 3em 0 0;
+}
+.forum-topic-navigation .topic-previous {
+ text-align: left;
+ float: right;
+}
+.forum-topic-navigation .topic-next {
+ text-align: right;
+ float: left;
+}
diff --git a/drupal-dev/modules/forum/forum-submitted.tpl.php b/drupal-dev/modules/forum/forum-submitted.tpl.php
new file mode 100644
index 0000000..18fea8f
--- /dev/null
+++ b/drupal-dev/modules/forum/forum-submitted.tpl.php
@@ -0,0 +1,30 @@
+
+
+
+ $time,
+ '!author' => $author,
+ )); ?>
+
+
+
+
diff --git a/drupal-dev/modules/forum/forum-topic-list.tpl.php b/drupal-dev/modules/forum/forum-topic-list.tpl.php
new file mode 100644
index 0000000..6427814
--- /dev/null
+++ b/drupal-dev/modules/forum/forum-topic-list.tpl.php
@@ -0,0 +1,72 @@
+icon: The icon to display.
+ * - $topic->moved: A flag to indicate whether the topic has been moved to
+ * another forum.
+ * - $topic->title: The title of the topic. Safe to output.
+ * - $topic->message: If the topic has been moved, this contains an
+ * explanation and a link.
+ * - $topic->zebra: 'even' or 'odd' string used for row class.
+ * - $topic->comment_count: The number of replies on this topic.
+ * - $topic->new_replies: A flag to indicate whether there are unread
+ * comments.
+ * - $topic->new_url: If there are unread replies, this is a link to them.
+ * - $topic->new_text: Text containing the translated, properly pluralized
+ * count.
+ * - $topic->created: A string representing when the topic was posted. Safe
+ * to output.
+ * - $topic->last_reply: An outputtable string representing when the topic was
+ * last replied to.
+ * - $topic->timestamp: The raw timestamp this topic was posted.
+ * - $topic_id: Numeric ID for the current forum topic.
+ *
+ * @see template_preprocess_forum_topic_list()
+ * @see theme_forum_topic_list()
+ *
+ * @ingroup themeable
+ */
+?>
+
+
diff --git a/drupal-dev/modules/forum/forum.admin.inc b/drupal-dev/modules/forum/forum.admin.inc
new file mode 100644
index 0000000..712cf54
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.admin.inc
@@ -0,0 +1,352 @@
+ '',
+ 'description' => '',
+ 'tid' => NULL,
+ 'weight' => 0,
+ );
+ $form['name'] = array('#type' => 'textfield',
+ '#title' => t('Forum name'),
+ '#default_value' => $edit['name'],
+ '#maxlength' => 255,
+ '#description' => t('Short but meaningful name for this collection of threaded discussions.'),
+ '#required' => TRUE,
+ );
+ $form['description'] = array('#type' => 'textarea',
+ '#title' => t('Description'),
+ '#default_value' => $edit['description'],
+ '#description' => t('Description and guidelines for discussions within this forum.'),
+ );
+ $form['parent']['#tree'] = TRUE;
+ $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'forum');
+ $form['weight'] = array('#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $edit['weight'],
+ '#description' => t('Forums are displayed in ascending order by weight (forums with equal weights are displayed alphabetically).'),
+ );
+
+ $form['vid'] = array('#type' => 'hidden', '#value' => variable_get('forum_nav_vocabulary', ''));
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
+ if ($edit['tid']) {
+ $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['tid'] = array('#type' => 'hidden', '#value' => $edit['tid']);
+ }
+ $form['#submit'][] = 'forum_form_submit';
+ $form['#theme'] = 'forum_form';
+
+ return $form;
+}
+
+/**
+ * Form submission handler for forum_form_forum() and forum_form_container().
+ */
+function forum_form_submit($form, &$form_state) {
+ if ($form['form_id']['#value'] == 'forum_form_container') {
+ $container = TRUE;
+ $type = t('forum container');
+ }
+ else {
+ $container = FALSE;
+ $type = t('forum');
+ }
+
+ $term = (object) $form_state['values'];
+ $status = taxonomy_term_save($term);
+ switch ($status) {
+ case SAVED_NEW:
+ if ($container) {
+ $containers = variable_get('forum_containers', array());
+ $containers[] = $term->tid;
+ variable_set('forum_containers', $containers);
+ }
+ $form_state['values']['tid'] = $term->tid;
+ drupal_set_message(t('Created new @type %term.', array('%term' => $form_state['values']['name'], '@type' => $type)));
+ break;
+ case SAVED_UPDATED:
+ drupal_set_message(t('The @type %term has been updated.', array('%term' => $form_state['values']['name'], '@type' => $type)));
+ // Clear the page and block caches to avoid stale data.
+ cache_clear_all();
+ break;
+ }
+ $form_state['redirect'] = 'admin/structure/forum';
+ return;
+}
+
+/**
+ * Returns HTML for a forum form.
+ *
+ * By default this does not alter the appearance of a form at all, but is
+ * provided as a convenience for themers.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_forum_form($variables) {
+ return drupal_render_children($variables['form']);
+}
+
+/**
+ * Form constructor for adding and editing forum containers.
+ *
+ * @param $edit
+ * (optional) Associative array containing a container term to be added or edited.
+ * Defaults to an empty array.
+ *
+ * @see forum_form_submit()
+ * @ingroup forms
+ */
+function forum_form_container($form, &$form_state, $edit = array()) {
+ $edit += array(
+ 'name' => '',
+ 'description' => '',
+ 'tid' => NULL,
+ 'weight' => 0,
+ );
+ // Handle a delete operation.
+ $form['name'] = array(
+ '#title' => t('Container name'),
+ '#type' => 'textfield',
+ '#default_value' => $edit['name'],
+ '#maxlength' => 255,
+ '#description' => t('Short but meaningful name for this collection of related forums.'),
+ '#required' => TRUE
+ );
+
+ $form['description'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#default_value' => $edit['description'],
+ '#description' => t('Description and guidelines for forums within this container.')
+ );
+ $form['parent']['#tree'] = TRUE;
+ $form['parent'][0] = _forum_parent_select($edit['tid'], t('Parent'), 'container');
+ $form['weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight'),
+ '#default_value' => $edit['weight'],
+ '#description' => t('Containers are displayed in ascending order by weight (containers with equal weights are displayed alphabetically).')
+ );
+
+ $form['vid'] = array(
+ '#type' => 'hidden',
+ '#value' => variable_get('forum_nav_vocabulary', ''),
+ );
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Save')
+ );
+ if ($edit['tid']) {
+ $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
+ $form['tid'] = array('#type' => 'value', '#value' => $edit['tid']);
+ }
+ $form['#submit'][] = 'forum_form_submit';
+ $form['#theme'] = 'forum_form';
+
+ return $form;
+}
+
+/**
+ * Form constructor for confirming deletion of a forum taxonomy term.
+ *
+ * @param $tid
+ * ID of the term to be deleted.
+ *
+ * @see forum_confirm_delete_submit()
+ * @ingroup forms
+ */
+function forum_confirm_delete($form, &$form_state, $tid) {
+ $term = taxonomy_term_load($tid);
+
+ $form['tid'] = array('#type' => 'value', '#value' => $tid);
+ $form['name'] = array('#type' => 'value', '#value' => $term->name);
+
+ return confirm_form($form, t('Are you sure you want to delete the forum %name?', array('%name' => $term->name)), 'admin/structure/forum', t('Deleting a forum or container will also delete its sub-forums, if any. To delete posts in this forum, visit content administration first. This action cannot be undone.', array('@content' => url('admin/content'))), t('Delete'), t('Cancel'));
+}
+
+/**
+ * Form submission handler for forum_confirm_delete().
+ */
+function forum_confirm_delete_submit($form, &$form_state) {
+ taxonomy_term_delete($form_state['values']['tid']);
+ drupal_set_message(t('The forum %term and all sub-forums have been deleted.', array('%term' => $form_state['values']['name'])));
+ watchdog('content', 'forum: deleted %term and all its sub-forums.', array('%term' => $form_state['values']['name']));
+
+ $form_state['redirect'] = 'admin/structure/forum';
+ return;
+}
+
+/**
+ * Form constructor for the forum settings page.
+ *
+ * @see forum_menu()
+ * @see system_settings_form()
+ * @ingroup forms
+ */
+function forum_admin_settings($form) {
+ $number = drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 80, 100, 150, 200, 250, 300, 350, 400, 500));
+ $form['forum_hot_topic'] = array('#type' => 'select',
+ '#title' => t('Hot topic threshold'),
+ '#default_value' => variable_get('forum_hot_topic', 15),
+ '#options' => $number,
+ '#description' => t('The number of replies a topic must have to be considered "hot".'),
+ );
+ $number = drupal_map_assoc(array(10, 25, 50, 75, 100));
+ $form['forum_per_page'] = array('#type' => 'select',
+ '#title' => t('Topics per page'),
+ '#default_value' => variable_get('forum_per_page', 25),
+ '#options' => $number,
+ '#description' => t('Default number of forum topics displayed per page.'),
+ );
+ $forder = array(1 => t('Date - newest first'), 2 => t('Date - oldest first'), 3 => t('Posts - most active first'), 4 => t('Posts - least active first'));
+ $form['forum_order'] = array('#type' => 'radios',
+ '#title' => t('Default order'),
+ '#default_value' => variable_get('forum_order', 1),
+ '#options' => $forder,
+ '#description' => t('Default display order for topics.'),
+ );
+ return system_settings_form($form);
+}
+
+/**
+ * Form constructor for the forum overview form.
+ *
+ * Returns a form for controlling the hierarchy of existing forums and
+ * containers.
+ *
+ * @see forum_menu()
+ * @ingroup forms
+ */
+function forum_overview($form, &$form_state) {
+ module_load_include('inc', 'taxonomy', 'taxonomy.admin');
+
+ $vid = variable_get('forum_nav_vocabulary', '');
+ $vocabulary = taxonomy_vocabulary_load($vid);
+ $form = taxonomy_overview_terms($form, $form_state, $vocabulary);
+
+ foreach (element_children($form) as $key) {
+ if (isset($form[$key]['#term'])) {
+ $term = $form[$key]['#term'];
+ $form[$key]['view']['#href'] = 'forum/' . $term['tid'];
+ if (in_array($form[$key]['#term']['tid'], variable_get('forum_containers', array()))) {
+ $form[$key]['edit']['#title'] = t('edit container');
+ $form[$key]['edit']['#href'] = 'admin/structure/forum/edit/container/' . $term['tid'];
+ }
+ else {
+ $form[$key]['edit']['#title'] = t('edit forum');
+ $form[$key]['edit']['#href'] = 'admin/structure/forum/edit/forum/' . $term['tid'];
+ }
+ }
+ }
+
+ // Remove the alphabetical reset.
+ unset($form['actions']['reset_alphabetical']);
+
+ // The form needs to have submit and validate handlers set explicitly.
+ $form['#theme'] = 'taxonomy_overview_terms';
+ $form['#submit'] = array('taxonomy_overview_terms_submit'); // Use the existing taxonomy overview submit handler.
+ $form['#empty_text'] = t('No containers or forums available. Add container or Add forum.', array('@container' => url('admin/structure/forum/add/container'), '@forum' => url('admin/structure/forum/add/forum')));
+ return $form;
+}
+
+/**
+ * Returns a select box for available parent terms.
+ *
+ * @param $tid
+ * ID of the term that is being added or edited.
+ * @param $title
+ * Title for the select box.
+ * @param $child_type
+ * Whether the child is a forum or a container.
+ *
+ * @return
+ * A select form element.
+ */
+function _forum_parent_select($tid, $title, $child_type) {
+
+ $parents = taxonomy_get_parents($tid);
+ if ($parents) {
+ $parent = array_shift($parents);
+ $parent = $parent->tid;
+ }
+ else {
+ $parent = 0;
+ }
+
+ $vid = variable_get('forum_nav_vocabulary', '');
+ $children = taxonomy_get_tree($vid, $tid);
+
+ // A term can't be the child of itself, nor of its children.
+ foreach ($children as $child) {
+ $exclude[] = $child->tid;
+ }
+ $exclude[] = $tid;
+
+ $tree = taxonomy_get_tree($vid);
+ $options[0] = '<' . t('root') . '>';
+ if ($tree) {
+ foreach ($tree as $term) {
+ if (!in_array($term->tid, $exclude)) {
+ $options[$term->tid] = str_repeat(' -- ', $term->depth) . $term->name;
+ }
+ }
+ }
+ if ($child_type == 'container') {
+ $description = t('Containers are usually placed at the top (root) level, but may also be placed inside another container or forum.');
+ }
+ elseif ($child_type == 'forum') {
+ $description = t('Forums may be placed at the top (root) level, or inside another container or forum.');
+ }
+
+ return array('#type' => 'select', '#title' => $title, '#default_value' => $parent, '#options' => $options, '#description' => $description, '#required' => TRUE);
+}
diff --git a/drupal-dev/modules/forum/forum.css b/drupal-dev/modules/forum/forum.css
new file mode 100644
index 0000000..480e07b
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.css
@@ -0,0 +1,54 @@
+/**
+ * @file
+ * Styling for the Forum module.
+ */
+
+#forum .description {
+ font-size: 0.9em;
+ margin: 0.5em;
+}
+#forum td.created,
+#forum td.posts,
+#forum td.topics,
+#forum td.last-reply,
+#forum td.replies,
+#forum td.pager {
+ white-space: nowrap;
+}
+
+#forum td.forum .icon {
+ background-image: url(../../misc/forum-icons.png);
+ background-repeat: no-repeat;
+ float: left; /* LTR */
+ height: 24px;
+ margin: 0 9px 0 0; /* LTR */
+ width: 24px;
+}
+#forum td.forum .forum-status-new {
+ background-position: -24px 0;
+}
+
+#forum div.indent {
+ margin-left: 20px; /* LTR */
+}
+#forum .icon div {
+ background-image: url(../../misc/forum-icons.png);
+ background-repeat: no-repeat;
+ width: 24px;
+ height: 24px;
+}
+#forum .icon .topic-status-new {
+ background-position: -24px 0;
+}
+#forum .icon .topic-status-hot {
+ background-position: -48px 0;
+}
+#forum .icon .topic-status-hot-new {
+ background-position: -72px 0;
+}
+#forum .icon .topic-status-sticky {
+ background-position: -96px 0;
+}
+#forum .icon .topic-status-closed {
+ background-position: -120px 0;
+}
diff --git a/drupal-dev/modules/forum/forum.info b/drupal-dev/modules/forum/forum.info
new file mode 100644
index 0000000..610bd7b
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.info
@@ -0,0 +1,16 @@
+name = Forum
+description = Provides discussion forums.
+dependencies[] = taxonomy
+dependencies[] = comment
+package = Core
+version = VERSION
+core = 7.x
+files[] = forum.test
+configure = admin/structure/forum
+stylesheets[all][] = forum.css
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/forum/forum.install b/drupal-dev/modules/forum/forum.install
new file mode 100644
index 0000000..57e116b
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.install
@@ -0,0 +1,467 @@
+fields(array('weight' => 1))
+ ->condition('name', 'forum')
+ ->execute();
+ // Forum topics are published by default, but do not have any other default
+ // options set (for example, they are not promoted to the front page).
+ variable_set('node_options_forum', array('status'));
+}
+
+/**
+ * Implements hook_enable().
+ */
+function forum_enable() {
+ // If we enable forum at the same time as taxonomy we need to call
+ // field_associate_fields() as otherwise the field won't be enabled until
+ // hook modules_enabled is called which takes place after hook_enable events.
+ field_associate_fields('taxonomy');
+ // Create the forum vocabulary if it does not exist.
+ $vocabulary = taxonomy_vocabulary_load(variable_get('forum_nav_vocabulary', 0));
+ if (!$vocabulary) {
+ $edit = array(
+ 'name' => t('Forums'),
+ 'machine_name' => 'forums',
+ 'description' => t('Forum navigation vocabulary'),
+ 'hierarchy' => 1,
+ 'module' => 'forum',
+ 'weight' => -10,
+ );
+ $vocabulary = (object) $edit;
+ taxonomy_vocabulary_save($vocabulary);
+ variable_set('forum_nav_vocabulary', $vocabulary->vid);
+ }
+
+ // Create the 'taxonomy_forums' field if it doesn't already exist.
+ if (!field_info_field('taxonomy_forums')) {
+ $field = array(
+ 'field_name' => 'taxonomy_forums',
+ 'type' => 'taxonomy_term_reference',
+ 'settings' => array(
+ 'allowed_values' => array(
+ array(
+ 'vocabulary' => $vocabulary->machine_name,
+ 'parent' => 0,
+ ),
+ ),
+ ),
+ );
+ field_create_field($field);
+
+ // Create a default forum so forum posts can be created.
+ $edit = array(
+ 'name' => t('General discussion'),
+ 'description' => '',
+ 'parent' => array(0),
+ 'vid' => $vocabulary->vid,
+ );
+ $term = (object) $edit;
+ taxonomy_term_save($term);
+
+ // Create the instance on the bundle.
+ $instance = array(
+ 'field_name' => 'taxonomy_forums',
+ 'entity_type' => 'node',
+ 'label' => $vocabulary->name,
+ 'bundle' => 'forum',
+ 'required' => TRUE,
+ 'widget' => array(
+ 'type' => 'options_select',
+ ),
+ 'display' => array(
+ 'default' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ 'teaser' => array(
+ 'type' => 'taxonomy_term_reference_link',
+ 'weight' => 10,
+ ),
+ ),
+ );
+ field_create_instance($instance);
+ }
+
+ // Ensure the forum node type is available.
+ node_types_rebuild();
+ $types = node_type_get_types();
+ node_add_body_field($types['forum']);
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function forum_uninstall() {
+ // Load the dependent Taxonomy module, in case it has been disabled.
+ drupal_load('module', 'taxonomy');
+
+ variable_del('forum_containers');
+ variable_del('forum_hot_topic');
+ variable_del('forum_per_page');
+ variable_del('forum_order');
+ variable_del('forum_block_num_active');
+ variable_del('forum_block_num_new');
+ variable_del('node_options_forum');
+
+ field_delete_field('taxonomy_forums');
+ // Purge field data now to allow taxonomy module to be uninstalled
+ // if this is the only field remaining.
+ field_purge_batch(10);
+}
+
+/**
+ * Implements hook_schema().
+ */
+function forum_schema() {
+ $schema['forum'] = array(
+ 'description' => 'Stores the relationship of nodes to forum terms.',
+ 'fields' => array(
+ 'nid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {node}.nid of the node.',
+ ),
+ 'vid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'Primary Key: The {node}.vid of the node.',
+ ),
+ 'tid' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The {taxonomy_term_data}.tid of the forum term assigned to the node.',
+ ),
+ ),
+ 'indexes' => array(
+ 'forum_topic' => array('nid', 'tid'),
+ 'tid' => array('tid'),
+ ),
+ 'primary key' => array('vid'),
+ 'foreign keys' => array(
+ 'forum_node' => array(
+ 'table' => 'node',
+ 'columns' => array(
+ 'nid' => 'nid',
+ 'vid' => 'vid',
+ ),
+ ),
+ ),
+ );
+
+ $schema['forum_index'] = array(
+ 'description' => 'Maintains denormalized information about node/term relationships.',
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => 'The {node}.nid this record tracks.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'title' => array(
+ 'description' => 'The title of this node, always treated as non-markup plain text.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'tid' => array(
+ 'description' => 'The term ID.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'sticky' => array(
+ 'description' => 'Boolean indicating whether the node is sticky.',
+ 'type' => 'int',
+ 'not null' => FALSE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the node was created.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default'=> 0,
+ ),
+ 'last_comment_timestamp' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.',
+ ),
+ 'comment_count' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The total number of comments on this node.',
+ ),
+ ),
+ 'indexes' => array(
+ 'forum_topics' => array('nid', 'tid', 'sticky', 'last_comment_timestamp'),
+ 'created' => array('created'),
+ 'last_comment_timestamp' => array('last_comment_timestamp'),
+ ),
+ 'foreign keys' => array(
+ 'tracked_node' => array(
+ 'table' => 'node',
+ 'columns' => array('nid' => 'nid'),
+ ),
+ 'term' => array(
+ 'table' => 'taxonomy_term_data',
+ 'columns' => array(
+ 'tid' => 'tid',
+ ),
+ ),
+ ),
+ );
+
+
+ return $schema;
+}
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function forum_update_dependencies() {
+ $dependencies['forum'][7003] = array(
+ // Forum update 7003 uses field API update functions, so must run after
+ // Field API has been enabled.
+ 'system' => 7020,
+ // Forum update 7003 relies on updated taxonomy module schema. Ensure it
+ // runs after all taxonomy updates.
+ 'taxonomy' => 7010,
+ );
+ return $dependencies;
+}
+
+/**
+ * Add new index to forum table.
+ */
+function forum_update_7000() {
+ db_drop_index('forum', 'nid');
+ db_add_index('forum', 'forum_topic', array('nid', 'tid'));
+}
+
+/**
+ * Create new {forum_index} table.
+ */
+function forum_update_7001() {
+ $forum_index = array(
+ 'description' => 'Maintains denormalized information about node/term relationships.',
+ 'fields' => array(
+ 'nid' => array(
+ 'description' => 'The {node}.nid this record tracks.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'title' => array(
+ 'description' => 'The title of this node, always treated as non-markup plain text.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ 'tid' => array(
+ 'description' => 'The term ID.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'sticky' => array(
+ 'description' => 'Boolean indicating whether the node is sticky.',
+ 'type' => 'int',
+ 'not null' => FALSE,
+ 'default' => 0,
+ 'size' => 'tiny',
+ ),
+ 'created' => array(
+ 'description' => 'The Unix timestamp when the node was created.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default'=> 0,
+ ),
+ 'last_comment_timestamp' => array(
+ 'type' => 'int',
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The Unix timestamp of the last comment that was posted within this node, from {comment}.timestamp.',
+ ),
+ 'comment_count' => array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ 'description' => 'The total number of comments on this node.',
+ ),
+ ),
+ 'indexes' => array(
+ 'forum_topics' => array('tid', 'sticky', 'last_comment_timestamp'),
+ ),
+ 'foreign keys' => array(
+ 'tracked_node' => array(
+ 'table' => 'node',
+ 'columns' => array('nid' => 'nid'),
+ ),
+ 'term' => array(
+ 'table' => 'taxonomy_term_data',
+ 'columns' => array(
+ 'tid' => 'tid',
+ ),
+ ),
+ ),
+ );
+ db_create_table('forum_index', $forum_index);
+
+ $select = db_select('node', 'n');
+ $forum_alias = $select->join('forum', 'f', 'n.vid = f.vid');
+ $ncs_alias = $select->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+ $select
+ ->fields('n', array('nid', 'title', 'sticky', 'created'))
+ ->fields($forum_alias, array('tid'))
+ ->fields($ncs_alias, array('last_comment_timestamp', 'comment_count'));
+
+ db_insert('forum_index')
+ ->fields(array('nid', 'title', 'sticky', 'created', 'tid', 'last_comment_timestamp', 'comment_count'))
+ ->from($select)
+ ->execute();
+}
+
+/**
+ * @addtogroup updates-7.x-extra
+ * @{
+ */
+
+/**
+ * Add new index to forum_index table.
+ */
+function forum_update_7002() {
+ db_drop_index('forum_index', 'forum_topics');
+ db_add_index('forum_index', 'forum_topics', array('nid', 'tid', 'sticky', 'last_comment_timestamp'));
+}
+
+/**
+ * Rename field to 'taxonomy_forums'.
+ */
+function forum_update_7003() {
+ $messages = array();
+
+ $new_field_name = 'taxonomy_forums';
+
+ // Test to see if the taxonomy_forums field exists.
+ $fields = _update_7000_field_read_fields(array('field_name' => $new_field_name));
+ if ($fields) {
+ // Since the field exists, we're done.
+ return;
+ }
+
+ // Calculate the old field name.
+ $vid = variable_get('forum_nav_vocabulary', 0);
+ $vocabulary_machine_name = db_select('taxonomy_vocabulary', 'tv')
+ ->fields('tv', array('machine_name'))
+ ->condition('vid', $vid)
+ ->execute()
+ ->fetchField();
+ $old_field_name = 'taxonomy_' . $vocabulary_machine_name;
+
+ // Read the old fields.
+ $old_fields = _update_7000_field_read_fields(array('field_name' => $old_field_name));
+ foreach ($old_fields as $old_field) {
+ if ($old_field['storage']['type'] != 'field_sql_storage') {
+ $messages[] = t('Cannot rename field %id (%old_field_name) to %new_field_name because it does not use the field_sql_storage storage type.', array(
+ '%id' => $old_field['id'],
+ '%old_field_name' => $old_field_name,
+ '%new_field_name' => $new_field_name,
+ ));
+ continue;
+ }
+
+ // Update {field_config}.
+ db_update('field_config')
+ ->fields(array('field_name' => $new_field_name))
+ ->condition('id', $old_field['id'])
+ ->execute();
+
+ // Update {field_config_instance}.
+ db_update('field_config_instance')
+ ->fields(array('field_name' => $new_field_name))
+ ->condition('field_id', $old_field['id'])
+ ->execute();
+
+ // The tables that need updating in the form 'old_name' => 'new_name'.
+ $tables = array(
+ 'field_data_' . $old_field_name => 'field_data_' . $new_field_name,
+ 'field_revision_' . $old_field_name => 'field_revision_' . $new_field_name,
+ );
+ foreach ($tables as $old_table => $new_table) {
+ $old_column_name = $old_field_name . '_tid';
+ $new_column_name = $new_field_name . '_tid';
+
+ // Rename the column.
+ db_drop_index($old_table, $old_column_name);
+ db_change_field($old_table, $old_column_name, $new_column_name, array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => FALSE,
+ ));
+ db_drop_index($old_table, $new_column_name);
+ db_add_index($old_table, $new_column_name, array($new_column_name));
+
+ // Rename the table.
+ db_rename_table($old_table, $new_table);
+ }
+ }
+
+ cache_clear_all('*', 'cache_field', TRUE);
+
+ return $messages;
+}
+
+/**
+ * Update {forum_index} so that only published nodes are indexed.
+ */
+function forum_update_7011() {
+ $select = db_select('node', 'n')
+ ->fields('n', array('nid'))
+ ->condition('status', 0 );
+
+ db_delete('forum_index')
+ ->condition('nid', $select, 'IN')
+ ->execute();
+}
+
+/**
+ * Add 'created' and 'last_comment_timestamp' indexes.
+ */
+function forum_update_7012() {
+ db_add_index('forum_index', 'created', array('created'));
+ db_add_index('forum_index', 'last_comment_timestamp', array('last_comment_timestamp'));
+}
+
+/**
+ * @} End of "addtogroup updates-7.x-extra".
+ */
diff --git a/drupal-dev/modules/forum/forum.module b/drupal-dev/modules/forum/forum.module
new file mode 100644
index 0000000..575de36
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.module
@@ -0,0 +1,1396 @@
+' . t('About') . '';
+ $output .= '
' . t('The Forum module lets you create threaded discussion forums with functionality similar to other message board systems. Forums are useful because they allow community members to discuss topics with one another while ensuring those conversations are archived for later reference. In a forum, users post topics and threads in nested hierarchies, allowing discussions to be categorized and grouped. The forum hierarchy consists of:') . '
';
+ $output .= '
';
+ $output .= '
' . t('Optional containers (for example, Support), which can hold:') . '
';
+ $output .= '
' . t('Forums (for example, Installing Drupal), which can hold:') . '
';
+ $output .= '
' . t('Forum topics submitted by users (for example, How to start a Drupal 6 Multisite), which start discussions and are starting points for:') . '
';
+ $output .= '
' . t('Threaded comments submitted by users (for example, You have these options...).') . '
';
+ $output .= '
';
+ $output .= '
';
+ $output .= '
';
+ $output .= '
';
+ $output .= '
' . t('For more information, see the online handbook entry for Forum module.', array('@forum' => 'http://drupal.org/documentation/modules/forum')) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Setting up forum structure') . '
';
+ $output .= '
' . t('Visit the Forums page to set up containers and forums to hold your discussion topics.', array('@forums' => url('admin/structure/forum'))) . '
';
+ $output .= '
' . t('Starting a discussion') . '
';
+ $output .= '
' . t('The Forum topic link on the Add new content page creates the first post of a new threaded discussion, or thread.', array('@create-topic' => url('node/add/forum'), '@content-add' => url('node/add'))) . '
';
+ $output .= '
' . t('Navigation') . '
';
+ $output .= '
' . t('Enabling the Forum module provides a default Forums menu item in the navigation menu that links to the Forums page.', array('@forums' => url('forum'))) . '
';
+ $output .= '
' . t('Moving forum topics') . '
';
+ $output .= '
' . t('A forum topic (and all of its comments) may be moved between forums by selecting a different forum while editing a forum topic. When moving a forum topic between forums, the Leave shadow copy option creates a link in the original forum pointing to the new location.') . '
';
+ $output .= '
' . t('Locking and disabling comments') . '
';
+ $output .= '
' . t('Selecting Closed under Comment settings while editing a forum topic will lock (prevent new comments on) the thread. Selecting Hidden under Comment settings while editing a forum topic will hide all existing comments on the thread, and prevent new ones.') . '
' . t('Use containers to group related forums.') . '
';
+ case 'admin/structure/forum/add/forum':
+ return '
' . t('A forum holds related forum topics.') . '
';
+ case 'admin/structure/forum/settings':
+ return '
' . t('Adjust the display of your forum topics. Organize the forums on the forum structure page.', array('@forum-structure' => url('admin/structure/forum'))) . '
';
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function forum_theme() {
+ return array(
+ 'forums' => array(
+ 'template' => 'forums',
+ 'variables' => array('forums' => NULL, 'topics' => NULL, 'parents' => NULL, 'tid' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
+ ),
+ 'forum_list' => array(
+ 'template' => 'forum-list',
+ 'variables' => array('forums' => NULL, 'parents' => NULL, 'tid' => NULL),
+ ),
+ 'forum_topic_list' => array(
+ 'template' => 'forum-topic-list',
+ 'variables' => array('tid' => NULL, 'topics' => NULL, 'sortby' => NULL, 'forum_per_page' => NULL),
+ ),
+ 'forum_icon' => array(
+ 'template' => 'forum-icon',
+ 'variables' => array('new_posts' => NULL, 'num_posts' => 0, 'comment_mode' => 0, 'sticky' => 0, 'first_new' => FALSE),
+ ),
+ 'forum_submitted' => array(
+ 'template' => 'forum-submitted',
+ 'variables' => array('topic' => NULL),
+ ),
+ 'forum_form' => array(
+ 'render element' => 'form',
+ 'file' => 'forum.admin.inc',
+ ),
+ );
+}
+
+/**
+ * Implements hook_menu().
+ */
+function forum_menu() {
+ $items['forum'] = array(
+ 'title' => 'Forums',
+ 'page callback' => 'forum_page',
+ 'access arguments' => array('access content'),
+ 'file' => 'forum.pages.inc',
+ );
+ $items['forum/%forum_forum'] = array(
+ 'title' => 'Forums',
+ 'page callback' => 'forum_page',
+ 'page arguments' => array(1),
+ 'access arguments' => array('access content'),
+ 'file' => 'forum.pages.inc',
+ );
+ $items['admin/structure/forum'] = array(
+ 'title' => 'Forums',
+ 'description' => 'Control forum hierarchy settings.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('forum_overview'),
+ 'access arguments' => array('administer forums'),
+ 'file' => 'forum.admin.inc',
+ );
+ $items['admin/structure/forum/list'] = array(
+ 'title' => 'List',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['admin/structure/forum/add/container'] = array(
+ 'title' => 'Add container',
+ 'page callback' => 'forum_form_main',
+ 'page arguments' => array('container'),
+ 'access arguments' => array('administer forums'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'parent' => 'admin/structure/forum',
+ 'file' => 'forum.admin.inc',
+ );
+ $items['admin/structure/forum/add/forum'] = array(
+ 'title' => 'Add forum',
+ 'page callback' => 'forum_form_main',
+ 'page arguments' => array('forum'),
+ 'access arguments' => array('administer forums'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'parent' => 'admin/structure/forum',
+ 'file' => 'forum.admin.inc',
+ );
+ $items['admin/structure/forum/settings'] = array(
+ 'title' => 'Settings',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('forum_admin_settings'),
+ 'access arguments' => array('administer forums'),
+ 'weight' => 5,
+ 'type' => MENU_LOCAL_TASK,
+ 'parent' => 'admin/structure/forum',
+ 'file' => 'forum.admin.inc',
+ );
+ $items['admin/structure/forum/edit/container/%taxonomy_term'] = array(
+ 'title' => 'Edit container',
+ 'page callback' => 'forum_form_main',
+ 'page arguments' => array('container', 5),
+ 'access arguments' => array('administer forums'),
+ 'file' => 'forum.admin.inc',
+ );
+ $items['admin/structure/forum/edit/forum/%taxonomy_term'] = array(
+ 'title' => 'Edit forum',
+ 'page callback' => 'forum_form_main',
+ 'page arguments' => array('forum', 5),
+ 'access arguments' => array('administer forums'),
+ 'file' => 'forum.admin.inc',
+ );
+ return $items;
+}
+
+/**
+ * Implements hook_menu_local_tasks_alter().
+ */
+function forum_menu_local_tasks_alter(&$data, $router_item, $root_path) {
+ global $user;
+
+ // Add action link to 'node/add/forum' on 'forum' sub-pages.
+ if ($root_path == 'forum' || $root_path == 'forum/%') {
+ $tid = (isset($router_item['page_arguments'][0]) ? $router_item['page_arguments'][0]->tid : 0);
+ $forum_term = forum_forum_load($tid);
+ if ($forum_term) {
+ $links = array();
+ // Loop through all bundles for forum taxonomy vocabulary field.
+ $field = field_info_field('taxonomy_forums');
+ foreach ($field['bundles']['node'] as $type) {
+ if (node_access('create', $type)) {
+ $links[$type] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => array(
+ 'title' => t('Add new @node_type', array('@node_type' => node_type_get_name($type))),
+ 'href' => 'node/add/' . str_replace('_', '-', $type) . '/' . $forum_term->tid,
+ ),
+ );
+ }
+ }
+ if (empty($links)) {
+ // Authenticated user does not have access to create new topics.
+ if ($user->uid) {
+ $links['disallowed'] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => array(
+ 'title' => t('You are not allowed to post new content in the forum.'),
+ ),
+ );
+ }
+ // Anonymous user does not have access to create new topics.
+ else {
+ $links['login'] = array(
+ '#theme' => 'menu_local_action',
+ '#link' => array(
+ 'title' => t('Log in to post new content in the forum.', array(
+ '@login' => url('user/login', array('query' => drupal_get_destination())),
+ )),
+ 'localized_options' => array('html' => TRUE),
+ ),
+ );
+ }
+ }
+ $data['actions']['output'] = array_merge($data['actions']['output'], $links);
+ }
+ }
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function forum_entity_info_alter(&$info) {
+ // Take over URI construction for taxonomy terms that are forums.
+ if ($vid = variable_get('forum_nav_vocabulary', 0)) {
+ // Within hook_entity_info(), we can't invoke entity_load() as that would
+ // cause infinite recursion, so we call taxonomy_vocabulary_get_names()
+ // instead of taxonomy_vocabulary_load(). All we need is the machine name
+ // of $vid, so retrieving and iterating all the vocabulary names is somewhat
+ // inefficient, but entity info is cached across page requests, and an
+ // iteration of all vocabularies once per cache clearing isn't a big deal,
+ // and is done as part of taxonomy_entity_info() anyway.
+ foreach (taxonomy_vocabulary_get_names() as $machine_name => $vocabulary) {
+ if ($vid == $vocabulary->vid) {
+ $info['taxonomy_term']['bundles'][$machine_name]['uri callback'] = 'forum_uri';
+ }
+ }
+ }
+}
+
+/**
+ * Implements callback_entity_info_uri().
+ *
+ * Entity URI callback used in forum_entity_info_alter().
+ */
+function forum_uri($forum) {
+ return array(
+ 'path' => 'forum/' . $forum->tid,
+ );
+}
+
+/**
+ * Checks whether a node can be used in a forum, based on its content type.
+ *
+ * @param $node
+ * A node object.
+ *
+ * @return
+ * Boolean indicating if the node can be assigned to a forum.
+ */
+function _forum_node_check_node_type($node) {
+ // Fetch information about the forum field.
+ $field = field_info_instance('node', 'taxonomy_forums', $node->type);
+
+ return is_array($field);
+}
+
+/**
+ * Implements hook_node_view().
+ */
+function forum_node_view($node, $view_mode) {
+ $vid = variable_get('forum_nav_vocabulary', 0);
+ $vocabulary = taxonomy_vocabulary_load($vid);
+ if (_forum_node_check_node_type($node)) {
+ if ($view_mode == 'full' && node_is_page($node)) {
+ // Breadcrumb navigation
+ $breadcrumb[] = l(t('Home'), NULL);
+ $breadcrumb[] = l($vocabulary->name, 'forum');
+ if ($parents = taxonomy_get_parents_all($node->forum_tid)) {
+ $parents = array_reverse($parents);
+ foreach ($parents as $parent) {
+ $breadcrumb[] = l($parent->name, 'forum/' . $parent->tid);
+ }
+ }
+ drupal_set_breadcrumb($breadcrumb);
+
+ }
+ }
+}
+
+/**
+ * Implements hook_node_validate().
+ *
+ * Checks in particular that the node is assigned only a "leaf" term in the
+ * forum taxonomy.
+ */
+function forum_node_validate($node, $form) {
+ if (_forum_node_check_node_type($node)) {
+ $langcode = $form['taxonomy_forums']['#language'];
+ // vocabulary is selected, not a "container" term.
+ if (!empty($node->taxonomy_forums[$langcode])) {
+ // Extract the node's proper topic ID.
+ $containers = variable_get('forum_containers', array());
+ foreach ($node->taxonomy_forums[$langcode] as $delta => $item) {
+ // If no term was selected (e.g. when no terms exist yet), remove the
+ // item.
+ if (empty($item['tid'])) {
+ unset($node->taxonomy_forums[$langcode][$delta]);
+ continue;
+ }
+ $term = taxonomy_term_load($item['tid']);
+ if (!$term) {
+ form_set_error('taxonomy_forums', t('Select a forum.'));
+ continue;
+ }
+ $used = db_query_range('SELECT 1 FROM {taxonomy_term_data} WHERE tid = :tid AND vid = :vid',0 , 1, array(
+ ':tid' => $term->tid,
+ ':vid' => $term->vid,
+ ))->fetchField();
+ if ($used && in_array($term->tid, $containers)) {
+ form_set_error('taxonomy_forums', t('The item %forum is a forum container, not a forum. Select one of the forums below instead.', array('%forum' => $term->name)));
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_node_presave().
+ *
+ * Assigns the forum taxonomy when adding a topic from within a forum.
+ */
+function forum_node_presave($node) {
+ if (_forum_node_check_node_type($node)) {
+ // Make sure all fields are set properly:
+ $node->icon = !empty($node->icon) ? $node->icon : '';
+ reset($node->taxonomy_forums);
+ $langcode = key($node->taxonomy_forums);
+ if (!empty($node->taxonomy_forums[$langcode])) {
+ $node->forum_tid = $node->taxonomy_forums[$langcode][0]['tid'];
+ if (isset($node->nid)) {
+ $old_tid = db_query_range("SELECT f.tid FROM {forum} f INNER JOIN {node} n ON f.vid = n.vid WHERE n.nid = :nid ORDER BY f.vid DESC", 0, 1, array(':nid' => $node->nid))->fetchField();
+ if ($old_tid && isset($node->forum_tid) && ($node->forum_tid != $old_tid) && !empty($node->shadow)) {
+ // A shadow copy needs to be created. Retain new term and add old term.
+ $node->taxonomy_forums[$langcode][] = array('tid' => $old_tid);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_node_update().
+ */
+function forum_node_update($node) {
+ if (_forum_node_check_node_type($node)) {
+ if (empty($node->revision) && db_query('SELECT tid FROM {forum} WHERE nid=:nid', array(':nid' => $node->nid))->fetchField()) {
+ if (!empty($node->forum_tid)) {
+ db_update('forum')
+ ->fields(array('tid' => $node->forum_tid))
+ ->condition('vid', $node->vid)
+ ->execute();
+ }
+ // The node is removed from the forum.
+ else {
+ db_delete('forum')
+ ->condition('nid', $node->nid)
+ ->execute();
+ }
+ }
+ else {
+ if (!empty($node->forum_tid)) {
+ db_insert('forum')
+ ->fields(array(
+ 'tid' => $node->forum_tid,
+ 'vid' => $node->vid,
+ 'nid' => $node->nid,
+ ))
+ ->execute();
+ }
+ }
+ // If the node has a shadow forum topic, update the record for this
+ // revision.
+ if (!empty($node->shadow)) {
+ db_delete('forum')
+ ->condition('nid', $node->nid)
+ ->condition('vid', $node->vid)
+ ->execute();
+ db_insert('forum')
+ ->fields(array(
+ 'nid' => $node->nid,
+ 'vid' => $node->vid,
+ 'tid' => $node->forum_tid,
+ ))
+ ->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_node_insert().
+ */
+function forum_node_insert($node) {
+ if (_forum_node_check_node_type($node)) {
+ if (!empty($node->forum_tid)) {
+ $nid = db_insert('forum')
+ ->fields(array(
+ 'tid' => $node->forum_tid,
+ 'vid' => $node->vid,
+ 'nid' => $node->nid,
+ ))
+ ->execute();
+ }
+ }
+}
+
+/**
+ * Implements hook_node_delete().
+ */
+function forum_node_delete($node) {
+ if (_forum_node_check_node_type($node)) {
+ db_delete('forum')
+ ->condition('nid', $node->nid)
+ ->execute();
+ db_delete('forum_index')
+ ->condition('nid', $node->nid)
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_node_load().
+ */
+function forum_node_load($nodes) {
+ $node_vids = array();
+ foreach ($nodes as $node) {
+ if (_forum_node_check_node_type($node)) {
+ $node_vids[] = $node->vid;
+ }
+ }
+ if (!empty($node_vids)) {
+ $query = db_select('forum', 'f');
+ $query
+ ->fields('f', array('nid', 'tid'))
+ ->condition('f.vid', $node_vids);
+ $result = $query->execute();
+ foreach ($result as $record) {
+ $nodes[$record->nid]->forum_tid = $record->tid;
+ }
+ }
+}
+
+/**
+ * Implements hook_node_info().
+ */
+function forum_node_info() {
+ return array(
+ 'forum' => array(
+ 'name' => t('Forum topic'),
+ 'base' => 'forum',
+ 'description' => t('A forum topic starts a new discussion thread within a forum.'),
+ 'title_label' => t('Subject'),
+ )
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function forum_permission() {
+ $perms = array(
+ 'administer forums' => array(
+ 'title' => t('Administer forums'),
+ ),
+ );
+ return $perms;
+}
+
+/**
+ * Implements hook_taxonomy_term_delete().
+ */
+function forum_taxonomy_term_delete($term) {
+ // For containers, remove the tid from the forum_containers variable.
+ $containers = variable_get('forum_containers', array());
+ $key = array_search($term->tid, $containers);
+ if ($key !== FALSE) {
+ unset($containers[$key]);
+ }
+ variable_set('forum_containers', $containers);
+}
+
+/**
+ * Implements hook_comment_publish().
+ *
+ * This actually handles the insertion and update of published nodes since
+ * comment_save() calls hook_comment_publish() for all published comments.
+ */
+function forum_comment_publish($comment) {
+ _forum_update_forum_index($comment->nid);
+}
+
+/**
+ * Implements hook_comment_update().
+ *
+ * The Comment module doesn't call hook_comment_unpublish() when saving
+ * individual comments, so we need to check for those here.
+ */
+function forum_comment_update($comment) {
+ // comment_save() calls hook_comment_publish() for all published comments,
+ // so we need to handle all other values here.
+ if (!$comment->status) {
+ _forum_update_forum_index($comment->nid);
+ }
+}
+
+/**
+ * Implements hook_comment_unpublish().
+ */
+function forum_comment_unpublish($comment) {
+ _forum_update_forum_index($comment->nid);
+}
+
+/**
+ * Implements hook_comment_delete().
+ */
+function forum_comment_delete($comment) {
+ _forum_update_forum_index($comment->nid);
+}
+
+/**
+ * Implements hook_field_storage_pre_insert().
+ */
+function forum_field_storage_pre_insert($entity_type, $entity, &$skip_fields) {
+ if ($entity_type == 'node' && $entity->status && _forum_node_check_node_type($entity)) {
+ $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
+ foreach ($entity->taxonomy_forums as $language) {
+ foreach ($language as $item) {
+ $query->values(array(
+ 'nid' => $entity->nid,
+ 'title' => $entity->title,
+ 'tid' => $item['tid'],
+ 'sticky' => $entity->sticky,
+ 'created' => $entity->created,
+ 'comment_count' => 0,
+ 'last_comment_timestamp' => $entity->created,
+ ));
+ }
+ }
+ $query->execute();
+ }
+}
+
+/**
+ * Implements hook_field_storage_pre_update().
+ */
+function forum_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
+ $first_call = &drupal_static(__FUNCTION__, array());
+
+ if ($entity_type == 'node' && _forum_node_check_node_type($entity)) {
+
+ // If the node is published, update the forum index.
+ if ($entity->status) {
+
+ // We don't maintain data for old revisions, so clear all previous values
+ // from the table. Since this hook runs once per field, per object, make
+ // sure we only wipe values once.
+ if (!isset($first_call[$entity->nid])) {
+ $first_call[$entity->nid] = FALSE;
+ db_delete('forum_index')->condition('nid', $entity->nid)->execute();
+ }
+ $query = db_insert('forum_index')->fields(array('nid', 'title', 'tid', 'sticky', 'created', 'comment_count', 'last_comment_timestamp'));
+ foreach ($entity->taxonomy_forums as $language) {
+ foreach ($language as $item) {
+ $query->values(array(
+ 'nid' => $entity->nid,
+ 'title' => $entity->title,
+ 'tid' => $item['tid'],
+ 'sticky' => $entity->sticky,
+ 'created' => $entity->created,
+ 'comment_count' => 0,
+ 'last_comment_timestamp' => $entity->created,
+ ));
+ }
+ }
+ $query->execute();
+ // The logic for determining last_comment_count is fairly complex, so
+ // call _forum_update_forum_index() too.
+ _forum_update_forum_index($entity->nid);
+ }
+
+ // When a forum node is unpublished, remove it from the forum_index table.
+ else {
+ db_delete('forum_index')->condition('nid', $entity->nid)->execute();
+ }
+
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for taxonomy_form_vocabulary().
+ */
+function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_id) {
+ $vid = variable_get('forum_nav_vocabulary', 0);
+ if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
+ $form['help_forum_vocab'] = array(
+ '#markup' => t('This is the designated forum vocabulary. Some of the normal vocabulary options have been removed.'),
+ '#weight' => -1,
+ );
+ // Forum's vocabulary always has single hierarchy. Forums and containers
+ // have only one parent or no parent for root items. By default this value
+ // is 0.
+ $form['hierarchy']['#value'] = 1;
+ // Do not allow to delete forum's vocabulary.
+ $form['actions']['delete']['#access'] = FALSE;
+ }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for taxonomy_form_term().
+ */
+function forum_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
+ $vid = variable_get('forum_nav_vocabulary', 0);
+ if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
+ // Hide multiple parents select from forum terms.
+ $form['relations']['parent']['#access'] = FALSE;
+ }
+}
+
+/**
+ * Implements hook_form_BASE_FORM_ID_alter() for node_form().
+ */
+function forum_form_node_form_alter(&$form, &$form_state, $form_id) {
+ if (isset($form['taxonomy_forums'])) {
+ $langcode = $form['taxonomy_forums']['#language'];
+ // Make the vocabulary required for 'real' forum-nodes.
+ $form['taxonomy_forums'][$langcode]['#required'] = TRUE;
+ $form['taxonomy_forums'][$langcode]['#multiple'] = FALSE;
+ if (empty($form['taxonomy_forums'][$langcode]['#default_value'])) {
+ // If there is no default forum already selected, try to get the forum
+ // ID from the URL (e.g., if we are on a page like node/add/forum/2, we
+ // expect "2" to be the ID of the forum that was requested).
+ $requested_forum_id = arg(3);
+ $form['taxonomy_forums'][$langcode]['#default_value'] = is_numeric($requested_forum_id) ? $requested_forum_id : '';
+ }
+ }
+}
+
+/**
+ * Implements hook_block_info().
+ */
+function forum_block_info() {
+ $blocks['active'] = array(
+ 'info' => t('Active forum topics'),
+ 'cache' => DRUPAL_CACHE_CUSTOM,
+ 'properties' => array('administrative' => TRUE),
+ );
+ $blocks['new'] = array(
+ 'info' => t('New forum topics'),
+ 'cache' => DRUPAL_CACHE_CUSTOM,
+ 'properties' => array('administrative' => TRUE),
+ );
+ return $blocks;
+}
+
+/**
+ * Implements hook_block_configure().
+ */
+function forum_block_configure($delta = '') {
+ $form['forum_block_num_' . $delta] = array(
+ '#type' => 'select',
+ '#title' => t('Number of topics'),
+ '#default_value' => variable_get('forum_block_num_' . $delta, '5'),
+ '#options' => drupal_map_assoc(range(2, 20))
+ );
+ return $form;
+}
+
+/**
+ * Implements hook_block_save().
+ */
+function forum_block_save($delta = '', $edit = array()) {
+ variable_set('forum_block_num_' . $delta, $edit['forum_block_num_' . $delta]);
+}
+
+/**
+ * Implements hook_block_view().
+ *
+ * Generates a block containing the currently active forum topics and the most
+ * recently added forum topics.
+ */
+function forum_block_view($delta = '') {
+ $query = db_select('forum_index', 'f')
+ ->fields('f')
+ ->addTag('node_access');
+ switch ($delta) {
+ case 'active':
+ $title = t('Active forum topics');
+ $query
+ ->orderBy('f.last_comment_timestamp', 'DESC')
+ ->range(0, variable_get('forum_block_num_active', '5'));
+ break;
+
+ case 'new':
+ $title = t('New forum topics');
+ $query
+ ->orderBy('f.created', 'DESC')
+ ->range(0, variable_get('forum_block_num_new', '5'));
+ break;
+ }
+
+ $block['subject'] = $title;
+ // Cache based on the altered query. Enables us to cache with node access enabled.
+ $block['content'] = drupal_render_cache_by_query($query, 'forum_block_view');
+ $block['content']['#access'] = user_access('access content');
+ return $block;
+}
+
+/**
+ * Render API callback: Lists nodes based on the element's #query property.
+ *
+ * This function can be used as a #pre_render callback.
+ *
+ * @see forum_block_view()
+ */
+function forum_block_view_pre_render($elements) {
+ $result = $elements['#query']->execute();
+ if ($node_title_list = node_title_list($result)) {
+ $elements['forum_list'] = $node_title_list;
+ $elements['forum_more'] = array('#theme' => 'more_link', '#url' => 'forum', '#title' => t('Read the latest forum topics.'));
+ }
+ return $elements;
+}
+
+/**
+ * Implements hook_form().
+ */
+function forum_form($node, $form_state) {
+ $type = node_type_get_type($node);
+ $form['title'] = array(
+ '#type' => 'textfield',
+ '#title' => check_plain($type->title_label),
+ '#default_value' => !empty($node->title) ? $node->title : '',
+ '#required' => TRUE, '#weight' => -5
+ );
+
+ if (!empty($node->nid)) {
+ $forum_terms = $node->taxonomy_forums;
+ // If editing, give option to leave shadows.
+ $shadow = (count($forum_terms) > 1);
+ $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.'));
+ $form['forum_tid'] = array('#type' => 'value', '#value' => $node->forum_tid);
+ }
+
+ return $form;
+}
+
+/**
+ * Returns a tree of all forums for a given taxonomy term ID.
+ *
+ * @param $tid
+ * (optional) Taxonomy term ID of the forum. If not given all forums will be
+ * returned.
+ *
+ * @return
+ * A tree of taxonomy objects, with the following additional properties:
+ * - num_topics: Number of topics in the forum.
+ * - num_posts: Total number of posts in all topics.
+ * - last_post: Most recent post for the forum.
+ * - forums: An array of child forums.
+ */
+function forum_forum_load($tid = NULL) {
+ $cache = &drupal_static(__FUNCTION__, array());
+
+ // Return a cached forum tree if available.
+ if (!isset($tid)) {
+ $tid = 0;
+ }
+ if (isset($cache[$tid])) {
+ return $cache[$tid];
+ }
+
+ $vid = variable_get('forum_nav_vocabulary', 0);
+
+ // Load and validate the parent term.
+ if ($tid) {
+ $forum_term = taxonomy_term_load($tid);
+ if (!$forum_term || ($forum_term->vid != $vid)) {
+ return $cache[$tid] = FALSE;
+ }
+ }
+ // If $tid is 0, create an empty object to hold the child terms.
+ elseif ($tid === 0) {
+ $forum_term = (object) array(
+ 'tid' => 0,
+ );
+ }
+
+ // Determine if the requested term is a container.
+ if (!$forum_term->tid || in_array($forum_term->tid, variable_get('forum_containers', array()))) {
+ $forum_term->container = 1;
+ }
+
+ // Load parent terms.
+ $forum_term->parents = taxonomy_get_parents_all($forum_term->tid);
+
+ // Load the tree below.
+ $forums = array();
+ $_forums = taxonomy_get_tree($vid, $tid);
+
+ if (count($_forums)) {
+ $query = db_select('node', 'n');
+ $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+ $query->join('forum', 'f', 'n.vid = f.vid');
+ $query->addExpression('COUNT(n.nid)', 'topic_count');
+ $query->addExpression('SUM(ncs.comment_count)', 'comment_count');
+ $counts = $query
+ ->fields('f', array('tid'))
+ ->condition('n.status', 1)
+ ->groupBy('tid')
+ ->addTag('node_access')
+ ->execute()
+ ->fetchAllAssoc('tid');
+ }
+
+ foreach ($_forums as $forum) {
+ // Determine if the child term is a container.
+ if (in_array($forum->tid, variable_get('forum_containers', array()))) {
+ $forum->container = 1;
+ }
+
+ // Merge in the topic and post counters.
+ if (!empty($counts[$forum->tid])) {
+ $forum->num_topics = $counts[$forum->tid]->topic_count;
+ $forum->num_posts = $counts[$forum->tid]->topic_count + $counts[$forum->tid]->comment_count;
+ }
+ else {
+ $forum->num_topics = 0;
+ $forum->num_posts = 0;
+ }
+
+ // Query "Last Post" information for this forum.
+ $query = db_select('node', 'n');
+ $query->join('users', 'u1', 'n.uid = u1.uid');
+ $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $forum->tid));
+ $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+ $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
+ $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
+
+ $topic = $query
+ ->fields('ncs', array('last_comment_timestamp', 'last_comment_uid'))
+ ->condition('n.status', 1)
+ ->orderBy('last_comment_timestamp', 'DESC')
+ ->range(0, 1)
+ ->addTag('node_access')
+ ->execute()
+ ->fetchObject();
+
+ // Merge in the "Last Post" information.
+ $last_post = new stdClass();
+ if (!empty($topic->last_comment_timestamp)) {
+ $last_post->created = $topic->last_comment_timestamp;
+ $last_post->name = $topic->last_comment_name;
+ $last_post->uid = $topic->last_comment_uid;
+ }
+ $forum->last_post = $last_post;
+
+ $forums[$forum->tid] = $forum;
+ }
+
+ // Cache the result, and return the tree.
+ $forum_term->forums = $forums;
+ $cache[$tid] = $forum_term;
+ return $forum_term;
+}
+
+/**
+ * Calculates the number of new posts in a forum that the user has not yet read.
+ *
+ * Nodes are new if they are newer than NODE_NEW_LIMIT.
+ *
+ * @param $term
+ * The term ID of the forum.
+ * @param $uid
+ * The user ID.
+ *
+ * @return
+ * The number of new posts in the forum that have not been read by the user.
+ */
+function _forum_topics_unread($term, $uid) {
+ $query = db_select('node', 'n');
+ $query->join('forum', 'f', 'n.vid = f.vid AND f.tid = :tid', array(':tid' => $term));
+ $query->leftJoin('history', 'h', 'n.nid = h.nid AND h.uid = :uid', array(':uid' => $uid));
+ $query->addExpression('COUNT(n.nid)', 'count');
+ return $query
+ ->condition('status', 1)
+ ->condition('n.created', NODE_NEW_LIMIT, '>')
+ ->isNull('h.nid')
+ ->addTag('node_access')
+ ->execute()
+ ->fetchField();
+}
+
+/**
+ * Gets all the topics in a forum.
+ *
+ * @param $tid
+ * The term ID of the forum.
+ * @param $sortby
+ * One of the following integers indicating the sort criteria:
+ * - 1: Date - newest first.
+ * - 2: Date - oldest first.
+ * - 3: Posts with the most comments first.
+ * - 4: Posts with the least comments first.
+ * @param $forum_per_page
+ * The maximum number of topics to display per page.
+ *
+ * @return
+ * A list of all the topics in a forum.
+ */
+function forum_get_topics($tid, $sortby, $forum_per_page) {
+ global $user, $forum_topic_list_header;
+
+ $forum_topic_list_header = array(
+ NULL,
+ array('data' => t('Topic'), 'field' => 'f.title'),
+ array('data' => t('Replies'), 'field' => 'f.comment_count'),
+ array('data' => t('Last reply'), 'field' => 'f.last_comment_timestamp'),
+ );
+
+ $order = _forum_get_topic_order($sortby);
+ for ($i = 0; $i < count($forum_topic_list_header); $i++) {
+ if ($forum_topic_list_header[$i]['field'] == $order['field']) {
+ $forum_topic_list_header[$i]['sort'] = $order['sort'];
+ }
+ }
+
+ $query = db_select('forum_index', 'f')->extend('PagerDefault')->extend('TableSort');
+ $query->fields('f');
+ $query
+ ->condition('f.tid', $tid)
+ ->addTag('node_access')
+ ->orderBy('f.sticky', 'DESC')
+ ->orderByHeader($forum_topic_list_header)
+ ->limit($forum_per_page);
+
+ $count_query = db_select('forum_index', 'f');
+ $count_query->condition('f.tid', $tid);
+ $count_query->addExpression('COUNT(*)');
+ $count_query->addTag('node_access');
+
+ $query->setCountQuery($count_query);
+ $result = $query->execute();
+ $nids = array();
+ foreach ($result as $record) {
+ $nids[] = $record->nid;
+ }
+ if ($nids) {
+ $query = db_select('node', 'n')->extend('TableSort');
+ $query->fields('n', array('title', 'nid', 'type', 'sticky', 'created', 'uid'));
+ $query->addField('n', 'comment', 'comment_mode');
+
+ $query->join('node_comment_statistics', 'ncs', 'n.nid = ncs.nid');
+ $query->fields('ncs', array('cid', 'last_comment_uid', 'last_comment_timestamp', 'comment_count'));
+
+ $query->join('forum_index', 'f', 'f.nid = ncs.nid');
+ $query->addField('f', 'tid', 'forum_tid');
+
+ $query->join('users', 'u', 'n.uid = u.uid');
+ $query->addField('u', 'name');
+
+ $query->join('users', 'u2', 'ncs.last_comment_uid = u2.uid');
+
+ $query->addExpression('CASE ncs.last_comment_uid WHEN 0 THEN ncs.last_comment_name ELSE u2.name END', 'last_comment_name');
+
+ $query
+ ->orderBy('f.sticky', 'DESC')
+ ->orderByHeader($forum_topic_list_header)
+ ->condition('n.nid', $nids);
+
+ $result = $query->execute();
+ }
+ else {
+ $result = array();
+ }
+
+ $topics = array();
+ $first_new_found = FALSE;
+ foreach ($result as $topic) {
+ if ($user->uid) {
+ // A forum is new if the topic is new, or if there are new comments since
+ // the user's last visit.
+ if ($topic->forum_tid != $tid) {
+ $topic->new = 0;
+ }
+ else {
+ $history = _forum_user_last_visit($topic->nid);
+ $topic->new_replies = comment_num_new($topic->nid, $history);
+ $topic->new = $topic->new_replies || ($topic->last_comment_timestamp > $history);
+ }
+ }
+ else {
+ // Do not track "new replies" status for topics if the user is anonymous.
+ $topic->new_replies = 0;
+ $topic->new = 0;
+ }
+
+ // Make sure only one topic is indicated as the first new topic.
+ $topic->first_new = FALSE;
+ if ($topic->new != 0 && !$first_new_found) {
+ $topic->first_new = TRUE;
+ $first_new_found = TRUE;
+ }
+
+ if ($topic->comment_count > 0) {
+ $last_reply = new stdClass();
+ $last_reply->created = $topic->last_comment_timestamp;
+ $last_reply->name = $topic->last_comment_name;
+ $last_reply->uid = $topic->last_comment_uid;
+ $topic->last_reply = $last_reply;
+ }
+ $topics[] = $topic;
+ }
+
+ return $topics;
+}
+
+/**
+ * Preprocesses variables for forums.tpl.php.
+ *
+ * @param $variables
+ * An array containing the following elements:
+ * - forums: An array of all forum objects to display for the given taxonomy
+ * term ID. If tid = 0 then all the top-level forums are displayed.
+ * - topics: An array of all the topics in the current forum.
+ * - parents: An array of taxonomy term objects that are ancestors of the
+ * current term ID.
+ * - tid: Taxonomy term ID of the current forum.
+ * - sortby: One of the following integers indicating the sort criteria:
+ * - 1: Date - newest first.
+ * - 2: Date - oldest first.
+ * - 3: Posts with the most comments first.
+ * - 4: Posts with the least comments first.
+ * - forum_per_page: The maximum number of topics to display per page.
+ *
+ * @see forums.tpl.php
+ */
+function template_preprocess_forums(&$variables) {
+ global $user;
+
+ $vid = variable_get('forum_nav_vocabulary', 0);
+ $vocabulary = taxonomy_vocabulary_load($vid);
+ $title = !empty($vocabulary->name) ? $vocabulary->name : '';
+
+ // Breadcrumb navigation:
+ $breadcrumb[] = l(t('Home'), NULL);
+ if ($variables['tid']) {
+ $breadcrumb[] = l($vocabulary->name, 'forum');
+ }
+ if ($variables['parents']) {
+ $variables['parents'] = array_reverse($variables['parents']);
+ foreach ($variables['parents'] as $p) {
+ if ($p->tid == $variables['tid']) {
+ $title = $p->name;
+ }
+ else {
+ $breadcrumb[] = l($p->name, 'forum/' . $p->tid);
+ }
+ }
+ }
+ drupal_set_breadcrumb($breadcrumb);
+ drupal_set_title($title);
+
+ if ($variables['forums_defined'] = count($variables['forums']) || count($variables['parents'])) {
+ if (!empty($variables['forums'])) {
+ $variables['forums'] = theme('forum_list', $variables);
+ }
+ else {
+ $variables['forums'] = '';
+ }
+
+ if ($variables['tid'] && !in_array($variables['tid'], variable_get('forum_containers', array()))) {
+ $variables['topics'] = theme('forum_topic_list', $variables);
+ drupal_add_feed('taxonomy/term/' . $variables['tid'] . '/feed', 'RSS - ' . $title);
+ }
+ else {
+ $variables['topics'] = '';
+ }
+
+ // Provide separate template suggestions based on what's being output. Topic id is also accounted for.
+ // Check both variables to be safe then the inverse. Forums with topic ID's take precedence.
+ if ($variables['forums'] && !$variables['topics']) {
+ $variables['theme_hook_suggestions'][] = 'forums__containers';
+ $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
+ $variables['theme_hook_suggestions'][] = 'forums__containers__' . $variables['tid'];
+ }
+ elseif (!$variables['forums'] && $variables['topics']) {
+ $variables['theme_hook_suggestions'][] = 'forums__topics';
+ $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
+ $variables['theme_hook_suggestions'][] = 'forums__topics__' . $variables['tid'];
+ }
+ else {
+ $variables['theme_hook_suggestions'][] = 'forums__' . $variables['tid'];
+ }
+
+ }
+ else {
+ drupal_set_title(t('No forums defined'));
+ $variables['forums'] = '';
+ $variables['topics'] = '';
+ }
+}
+
+/**
+ * Preprocesses variables for forum-list.tpl.php.
+ *
+ * @param $variables
+ * An array containing the following elements:
+ * - forums: An array of all forum objects to display for the given taxonomy
+ * term ID. If tid = 0 then all the top-level forums are displayed.
+ * - parents: An array of taxonomy term objects that are ancestors of the
+ * current term ID.
+ * - tid: Taxonomy term ID of the current forum.
+ *
+ * @see forum-list.tpl.php
+ * @see theme_forum_list()
+ */
+function template_preprocess_forum_list(&$variables) {
+ global $user;
+ $row = 0;
+ // Sanitize each forum so that the template can safely print the data.
+ foreach ($variables['forums'] as $id => $forum) {
+ $variables['forums'][$id]->description = !empty($forum->description) ? filter_xss_admin($forum->description) : '';
+ $variables['forums'][$id]->link = url("forum/$forum->tid");
+ $variables['forums'][$id]->name = check_plain($forum->name);
+ $variables['forums'][$id]->is_container = !empty($forum->container);
+ $variables['forums'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
+ $row++;
+
+ $variables['forums'][$id]->new_text = '';
+ $variables['forums'][$id]->new_url = '';
+ $variables['forums'][$id]->new_topics = 0;
+ $variables['forums'][$id]->old_topics = $forum->num_topics;
+ $variables['forums'][$id]->icon_class = 'default';
+ $variables['forums'][$id]->icon_title = t('No new posts');
+ if ($user->uid) {
+ $variables['forums'][$id]->new_topics = _forum_topics_unread($forum->tid, $user->uid);
+ if ($variables['forums'][$id]->new_topics) {
+ $variables['forums'][$id]->new_text = format_plural($variables['forums'][$id]->new_topics, '1 new', '@count new');
+ $variables['forums'][$id]->new_url = url("forum/$forum->tid", array('fragment' => 'new'));
+ $variables['forums'][$id]->icon_class = 'new';
+ $variables['forums'][$id]->icon_title = t('New posts');
+ }
+ $variables['forums'][$id]->old_topics = $forum->num_topics - $variables['forums'][$id]->new_topics;
+ }
+ $variables['forums'][$id]->last_reply = theme('forum_submitted', array('topic' => $forum->last_post));
+ }
+ // Give meaning to $tid for themers. $tid actually stands for term id.
+ $variables['forum_id'] = $variables['tid'];
+ unset($variables['tid']);
+}
+
+/**
+ * Preprocesses variables for forum-topic-list.tpl.php.
+ *
+ * @param $variables
+ * An array containing the following elements:
+ * - tid: Taxonomy term ID of the current forum.
+ * - topics: An array of all the topics in the current forum.
+ * - forum_per_page: The maximum number of topics to display per page.
+ *
+ * @see forum-topic-list.tpl.php
+ * @see theme_forum_topic_list()
+ */
+function template_preprocess_forum_topic_list(&$variables) {
+ global $forum_topic_list_header;
+
+ // Create the tablesorting header.
+ $ts = tablesort_init($forum_topic_list_header);
+ $header = '';
+ foreach ($forum_topic_list_header as $cell) {
+ $cell = tablesort_header($cell, $forum_topic_list_header, $ts);
+ $header .= _theme_table_cell($cell, TRUE);
+ }
+ $variables['header'] = $header;
+
+ if (!empty($variables['topics'])) {
+ $row = 0;
+ foreach ($variables['topics'] as $id => $topic) {
+ $variables['topics'][$id]->icon = theme('forum_icon', array('new_posts' => $topic->new, 'num_posts' => $topic->comment_count, 'comment_mode' => $topic->comment_mode, 'sticky' => $topic->sticky, 'first_new' => $topic->first_new));
+ $variables['topics'][$id]->zebra = $row % 2 == 0 ? 'odd' : 'even';
+ $row++;
+
+ // We keep the actual tid in forum table, if it's different from the
+ // current tid then it means the topic appears in two forums, one of
+ // them is a shadow copy.
+ if ($variables['tid'] != $topic->forum_tid) {
+ $variables['topics'][$id]->moved = TRUE;
+ $variables['topics'][$id]->title = check_plain($topic->title);
+ $variables['topics'][$id]->message = l(t('This topic has been moved'), "forum/$topic->forum_tid");
+ }
+ else {
+ $variables['topics'][$id]->moved = FALSE;
+ $variables['topics'][$id]->title = l($topic->title, "node/$topic->nid");
+ $variables['topics'][$id]->message = '';
+ }
+ $variables['topics'][$id]->created = theme('forum_submitted', array('topic' => $topic));
+ $variables['topics'][$id]->last_reply = theme('forum_submitted', array('topic' => isset($topic->last_reply) ? $topic->last_reply : NULL));
+
+ $variables['topics'][$id]->new_text = '';
+ $variables['topics'][$id]->new_url = '';
+ if ($topic->new_replies) {
+ $variables['topics'][$id]->new_text = format_plural($topic->new_replies, '1 new', '@count new');
+ $variables['topics'][$id]->new_url = url("node/$topic->nid", array('query' => comment_new_page_count($topic->comment_count, $topic->new_replies, $topic), 'fragment' => 'new'));
+ }
+
+ }
+ }
+ else {
+ // Make this safe for the template.
+ $variables['topics'] = array();
+ }
+ // Give meaning to $tid for themers. $tid actually stands for term id.
+ $variables['topic_id'] = $variables['tid'];
+ unset($variables['tid']);
+
+ $variables['pager'] = theme('pager');
+}
+
+/**
+ * Preprocesses variables for forum-icon.tpl.php.
+ *
+ * @param $variables
+ * An array containing the following elements:
+ * - new_posts: Indicates whether or not the topic contains new posts.
+ * - num_posts: The total number of posts in all topics.
+ * - comment_mode: An integer indicating whether comments are open, closed,
+ * or hidden.
+ * - sticky: Indicates whether the topic is sticky.
+ * - first_new: Indicates whether this is the first topic with new posts.
+ *
+ * @see forum-icon.tpl.php
+ * @see theme_forum_icon()
+ */
+function template_preprocess_forum_icon(&$variables) {
+ $variables['hot_threshold'] = variable_get('forum_hot_topic', 15);
+ if ($variables['num_posts'] > $variables['hot_threshold']) {
+ $variables['icon_class'] = $variables['new_posts'] ? 'hot-new' : 'hot';
+ $variables['icon_title'] = $variables['new_posts'] ? t('Hot topic, new comments') : t('Hot topic');
+ }
+ else {
+ $variables['icon_class'] = $variables['new_posts'] ? 'new' : 'default';
+ $variables['icon_title'] = $variables['new_posts'] ? t('New comments') : t('Normal topic');
+ }
+
+ if ($variables['comment_mode'] == COMMENT_NODE_CLOSED || $variables['comment_mode'] == COMMENT_NODE_HIDDEN) {
+ $variables['icon_class'] = 'closed';
+ $variables['icon_title'] = t('Closed topic');
+ }
+
+ if ($variables['sticky'] == 1) {
+ $variables['icon_class'] = 'sticky';
+ $variables['icon_title'] = t('Sticky topic');
+ }
+}
+
+/**
+ * Preprocesses variables for forum-submitted.tpl.php.
+ *
+ * The submission information will be displayed in the forum list and topic
+ * list.
+ *
+ * @param $variables
+ * An array containing the following elements:
+ * - topic: The topic object.
+ *
+ * @see forum-submitted.tpl.php
+ * @see theme_forum_submitted()
+ */
+function template_preprocess_forum_submitted(&$variables) {
+ $variables['author'] = isset($variables['topic']->uid) ? theme('username', array('account' => $variables['topic'])) : '';
+ $variables['time'] = isset($variables['topic']->created) ? format_interval(REQUEST_TIME - $variables['topic']->created) : '';
+}
+
+/**
+ * Gets the last time the user viewed a node.
+ *
+ * @param $nid
+ * The node ID.
+ *
+ * @return
+ * The timestamp when the user last viewed this node, if the user has
+ * previously viewed the node; otherwise NODE_NEW_LIMIT.
+ */
+function _forum_user_last_visit($nid) {
+ global $user;
+ $history = &drupal_static(__FUNCTION__, array());
+
+ if (empty($history)) {
+ $result = db_query('SELECT nid, timestamp FROM {history} WHERE uid = :uid', array(':uid' => $user->uid));
+ foreach ($result as $t) {
+ $history[$t->nid] = $t->timestamp > NODE_NEW_LIMIT ? $t->timestamp : NODE_NEW_LIMIT;
+ }
+ }
+ return isset($history[$nid]) ? $history[$nid] : NODE_NEW_LIMIT;
+}
+
+/**
+ * Gets topic sorting information based on an integer code.
+ *
+ * @param $sortby
+ * One of the following integers indicating the sort criteria:
+ * - 1: Date - newest first.
+ * - 2: Date - oldest first.
+ * - 3: Posts with the most comments first.
+ * - 4: Posts with the least comments first.
+ *
+ * @return
+ * An array with the following values:
+ * - field: A field for an SQL query.
+ * - sort: 'asc' or 'desc'.
+ */
+function _forum_get_topic_order($sortby) {
+ switch ($sortby) {
+ case 1:
+ return array('field' => 'f.last_comment_timestamp', 'sort' => 'desc');
+ break;
+ case 2:
+ return array('field' => 'f.last_comment_timestamp', 'sort' => 'asc');
+ break;
+ case 3:
+ return array('field' => 'f.comment_count', 'sort' => 'desc');
+ break;
+ case 4:
+ return array('field' => 'f.comment_count', 'sort' => 'asc');
+ break;
+ }
+}
+
+/**
+ * Updates the taxonomy index for a given node.
+ *
+ * @param $nid
+ * The ID of the node to update.
+ */
+function _forum_update_forum_index($nid) {
+ $count = db_query('SELECT COUNT(cid) FROM {comment} c INNER JOIN {forum_index} i ON c.nid = i.nid WHERE c.nid = :nid AND c.status = :status', array(
+ ':nid' => $nid,
+ ':status' => COMMENT_PUBLISHED,
+ ))->fetchField();
+
+ if ($count > 0) {
+ // Comments exist.
+ $last_reply = db_query_range('SELECT cid, name, created, uid FROM {comment} WHERE nid = :nid AND status = :status ORDER BY cid DESC', 0, 1, array(
+ ':nid' => $nid,
+ ':status' => COMMENT_PUBLISHED,
+ ))->fetchObject();
+ db_update('forum_index')
+ ->fields( array(
+ 'comment_count' => $count,
+ 'last_comment_timestamp' => $last_reply->created,
+ ))
+ ->condition('nid', $nid)
+ ->execute();
+ }
+ else {
+ // Comments do not exist.
+ $node = db_query('SELECT uid, created FROM {node} WHERE nid = :nid', array(':nid' => $nid))->fetchObject();
+ db_update('forum_index')
+ ->fields( array(
+ 'comment_count' => 0,
+ 'last_comment_timestamp' => $node->created,
+ ))
+ ->condition('nid', $nid)
+ ->execute();
+ }
+}
+
+/**
+ * Implements hook_rdf_mapping().
+ */
+function forum_rdf_mapping() {
+ return array(
+ array(
+ 'type' => 'node',
+ 'bundle' => 'forum',
+ 'mapping' => array(
+ 'rdftype' => array('sioc:Post', 'sioct:BoardPost'),
+ 'taxonomy_forums' => array(
+ 'predicates' => array('sioc:has_container'),
+ 'type' => 'rel',
+ ),
+ ),
+ ),
+ array(
+ 'type' => 'taxonomy_term',
+ 'bundle' => 'forums',
+ 'mapping' => array(
+ 'rdftype' => array('sioc:Container', 'sioc:Forum'),
+ ),
+ ),
+ );
+}
diff --git a/drupal-dev/modules/forum/forum.pages.inc b/drupal-dev/modules/forum/forum.pages.inc
new file mode 100644
index 0000000..8538310
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.pages.inc
@@ -0,0 +1,37 @@
+container)) {
+ $topics = forum_get_topics($forum_term->tid, $sortby, $forum_per_page);
+ }
+ else {
+ $topics = '';
+ }
+
+ return theme('forums', array('forums' => $forum_term->forums, 'topics' => $topics, 'parents' => $forum_term->parents, 'tid' => $forum_term->tid, 'sortby' => $sortby, 'forums_per_page' => $forum_per_page));
+}
diff --git a/drupal-dev/modules/forum/forum.test b/drupal-dev/modules/forum/forum.test
new file mode 100644
index 0000000..bc68a3e
--- /dev/null
+++ b/drupal-dev/modules/forum/forum.test
@@ -0,0 +1,687 @@
+ 'Forum functionality',
+ 'description' => 'Create, view, edit, delete, and change forum entries and verify its consistency in the database.',
+ 'group' => 'Forum',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('taxonomy', 'comment', 'forum');
+ // Create users.
+ $this->admin_user = $this->drupalCreateUser(array(
+ 'access administration pages',
+ 'administer modules',
+ 'administer blocks',
+ 'administer forums',
+ 'administer menu',
+ 'administer taxonomy',
+ 'create forum content',
+ ));
+ $this->edit_any_topics_user = $this->drupalCreateUser(array(
+ 'access administration pages',
+ 'create forum content',
+ 'edit any forum content',
+ 'delete any forum content',
+ ));
+ $this->edit_own_topics_user = $this->drupalCreateUser(array(
+ 'create forum content',
+ 'edit own forum content',
+ 'delete own forum content',
+ ));
+ $this->web_user = $this->drupalCreateUser(array());
+ }
+
+ /**
+ * Tests disabling and re-enabling the Forum module.
+ */
+ function testEnableForumField() {
+ $this->drupalLogin($this->admin_user);
+
+ // Disable the Forum module.
+ $edit = array();
+ $edit['modules[Core][forum][enable]'] = FALSE;
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.');
+ module_list(TRUE);
+ $this->assertFalse(module_exists('forum'), 'Forum module is not enabled.');
+
+ // Attempt to re-enable the Forum module and ensure it does not try to
+ // recreate the taxonomy_forums field.
+ $edit = array();
+ $edit['modules[Core][forum][enable]'] = 'forum';
+ $this->drupalPost('admin/modules', $edit, t('Save configuration'));
+ $this->assertText(t('The configuration options have been saved.'), 'Modules status has been updated.');
+ module_list(TRUE);
+ $this->assertTrue(module_exists('forum'), 'Forum module is enabled.');
+ }
+
+ /**
+ * Tests forum functionality through the admin and user interfaces.
+ */
+ function testForum() {
+ //Check that the basic forum install creates a default forum topic
+ $this->drupalGet("/forum");
+ // Look for the "General discussion" default forum
+ $this->assertText(t("General discussion"), "Found the default forum at the /forum listing");
+
+ // Do the admin tests.
+ $this->doAdminTests($this->admin_user);
+ // Generate topics to populate the active forum block.
+ $this->generateForumTopics($this->forum);
+
+ // Login an unprivileged user to view the forum topics and generate an
+ // active forum topics list.
+ $this->drupalLogin($this->web_user);
+ // Verify that this user is shown a message that they may not post content.
+ $this->drupalGet('forum/' . $this->forum['tid']);
+ $this->assertText(t('You are not allowed to post new content in the forum'), "Authenticated user without permission to post forum content is shown message in local tasks to that effect.");
+
+ $this->viewForumTopics($this->nids);
+
+ // Log in, and do basic tests for a user with permission to edit any forum
+ // content.
+ $this->doBasicTests($this->edit_any_topics_user, TRUE);
+ // Create a forum node authored by this user.
+ $any_topics_user_node = $this->createForumTopic($this->forum, FALSE);
+
+ // Log in, and do basic tests for a user with permission to edit only its
+ // own forum content.
+ $this->doBasicTests($this->edit_own_topics_user, FALSE);
+ // Create a forum node authored by this user.
+ $own_topics_user_node = $this->createForumTopic($this->forum, FALSE);
+ // Verify that this user cannot edit forum content authored by another user.
+ $this->verifyForums($this->edit_any_topics_user, $any_topics_user_node, FALSE, 403);
+
+ // Verify that this user is shown a local task to add new forum content.
+ $this->drupalGet('forum');
+ $this->assertLink(t('Add new Forum topic'));
+ $this->drupalGet('forum/' . $this->forum['tid']);
+ $this->assertLink(t('Add new Forum topic'));
+
+ // Login a user with permission to edit any forum content.
+ $this->drupalLogin($this->edit_any_topics_user);
+ // Verify that this user can edit forum content authored by another user.
+ $this->verifyForums($this->edit_own_topics_user, $own_topics_user_node, TRUE);
+
+ // Verify the topic and post counts on the forum page.
+ $this->drupalGet('forum');
+
+ // Verify row for testing forum.
+ $forum_arg = array(':forum' => 'forum-list-' . $this->forum['tid']);
+
+ // Topics cell contains number of topics and number of unread topics.
+ $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]', $forum_arg);
+ $topics = $this->xpath($xpath);
+ $topics = trim($topics[0]);
+ $this->assertEqual($topics, '6', 'Number of topics found.');
+
+ // Verify the number of unread topics.
+ $unread_topics = _forum_topics_unread($this->forum['tid'], $this->edit_any_topics_user->uid);
+ $unread_topics = format_plural($unread_topics, '1 new', '@count new');
+ $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="topics"]//a', $forum_arg);
+ $this->assertFieldByXPath($xpath, $unread_topics, 'Number of unread topics found.');
+
+ // Verify total number of posts in forum.
+ $xpath = $this->buildXPathQuery('//tr[@id=:forum]//td[@class="posts"]', $forum_arg);
+ $this->assertFieldByXPath($xpath, '6', 'Number of posts found.');
+
+ // Test loading multiple forum nodes on the front page.
+ $this->drupalLogin($this->drupalCreateUser(array('administer content types', 'create forum content')));
+ $this->drupalPost('admin/structure/types/manage/forum', array('node_options[promote]' => 'promote'), t('Save content type'));
+ $this->createForumTopic($this->forum, FALSE);
+ $this->createForumTopic($this->forum, FALSE);
+ $this->drupalGet('node');
+
+ // Test adding a comment to a forum topic.
+ $node = $this->createForumTopic($this->forum, FALSE);
+ $edit = array();
+ $edit['comment_body[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName();
+ $this->drupalPost("node/$node->nid", $edit, t('Save'));
+ $this->assertResponse(200);
+
+ // Test editing a forum topic that has a comment.
+ $this->drupalLogin($this->edit_any_topics_user);
+ $this->drupalGet('forum/' . $this->forum['tid']);
+ $this->drupalPost("node/$node->nid/edit", array(), t('Save'));
+ $this->assertResponse(200);
+
+ // Make sure constructing a forum node programmatically produces no notices.
+ $node = new stdClass;
+ $node->type = 'forum';
+ $node->title = 'Test forum notices';
+ $node->uid = 1;
+ $node->taxonomy_forums[LANGUAGE_NONE][0]['tid'] = $this->root_forum['tid'];
+ node_save($node);
+ }
+
+ /**
+ * Tests that forum nodes can't be added without a parent.
+ *
+ * Verifies that forum nodes are not created without choosing "forum" from the
+ * select list.
+ */
+ function testAddOrphanTopic() {
+ // Must remove forum topics to test creating orphan topics.
+ $vid = variable_get('forum_nav_vocabulary');
+ $tree = taxonomy_get_tree($vid);
+ foreach ($tree as $term) {
+ taxonomy_term_delete($term->tid);
+ }
+
+ // Create an orphan forum item.
+ $this->drupalLogin($this->admin_user);
+ $this->drupalPost('node/add/forum', array('title' => $this->randomName(10), 'body[' . LANGUAGE_NONE .'][0][value]' => $this->randomName(120)), t('Save'));
+
+ $nid_count = db_query('SELECT COUNT(nid) FROM {node}')->fetchField();
+ $this->assertEqual(0, $nid_count, 'A forum node was not created when missing a forum vocabulary.');
+
+ // Reset the defaults for future tests.
+ module_enable(array('forum'));
+ }
+
+ /**
+ * Runs admin tests on the admin user.
+ *
+ * @param object $user
+ * The logged in user.
+ */
+ private function doAdminTests($user) {
+ // Login the user.
+ $this->drupalLogin($user);
+
+ // Enable the active forum block.
+ $edit = array();
+ $edit['blocks[forum_active][region]'] = 'sidebar_second';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ $this->assertResponse(200);
+ $this->assertText(t('The block settings have been updated.'), 'Active forum topics forum block was enabled');
+
+ // Enable the new forum block.
+ $edit = array();
+ $edit['blocks[forum_new][region]'] = 'sidebar_second';
+ $this->drupalPost('admin/structure/block', $edit, t('Save blocks'));
+ $this->assertResponse(200);
+ $this->assertText(t('The block settings have been updated.'), '[New forum topics] Forum block was enabled');
+
+ // Retrieve forum menu id.
+ $mlid = db_query_range("SELECT mlid FROM {menu_links} WHERE link_path = 'forum' AND menu_name = 'navigation' AND module = 'system' ORDER BY mlid ASC", 0, 1)->fetchField();
+
+ // Add forum to navigation menu.
+ $edit = array();
+ $this->drupalPost('admin/structure/menu/manage/navigation', $edit, t('Save configuration'));
+ $this->assertResponse(200);
+
+ // Edit forum taxonomy.
+ // Restoration of the settings fails and causes subsequent tests to fail.
+ $this->container = $this->editForumTaxonomy();
+ // Create forum container.
+ $this->container = $this->createForum('container');
+ // Verify "edit container" link exists and functions correctly.
+ $this->drupalGet('admin/structure/forum');
+ $this->clickLink('edit container');
+ $this->assertRaw('Edit container', 'Followed the link to edit the container');
+ // Create forum inside the forum container.
+ $this->forum = $this->createForum('forum', $this->container['tid']);
+ // Verify the "edit forum" link exists and functions correctly.
+ $this->drupalGet('admin/structure/forum');
+ $this->clickLink('edit forum');
+ $this->assertRaw('Edit forum', 'Followed the link to edit the forum');
+ // Navigate back to forum structure page.
+ $this->drupalGet('admin/structure/forum');
+ // Create second forum in container.
+ $this->delete_forum = $this->createForum('forum', $this->container['tid']);
+ // Save forum overview.
+ $this->drupalPost('admin/structure/forum/', array(), t('Save'));
+ $this->assertRaw(t('The configuration options have been saved.'));
+ // Delete this second forum.
+ $this->deleteForum($this->delete_forum['tid']);
+ // Create forum at the top (root) level.
+ $this->root_forum = $this->createForum('forum');
+
+ // Test vocabulary form alterations.
+ $this->drupalGet('admin/structure/taxonomy/forums/edit');
+ $this->assertFieldByName('op', t('Save'), 'Save button found.');
+ $this->assertNoFieldByName('op', t('Delete'), 'Delete button not found.');
+
+ // Test term edit form alterations.
+ $this->drupalGet('taxonomy/term/' . $this->container['tid'] . '/edit');
+ // Test parent field been hidden by forum module.
+ $this->assertNoField('parent[]', 'Parent field not found.');
+
+ // Test tags vocabulary form is not affected.
+ $this->drupalGet('admin/structure/taxonomy/tags/edit');
+ $this->assertFieldByName('op', t('Save'), 'Save button found.');
+ $this->assertFieldByName('op', t('Delete'), 'Delete button found.');
+ // Test tags vocabulary term form is not affected.
+ $this->drupalGet('admin/structure/taxonomy/tags/add');
+ $this->assertField('parent[]', 'Parent field found.');
+ // Test relations fieldset exists.
+ $relations_fieldset = $this->xpath("//fieldset[@id='edit-relations']");
+ $this->assertTrue(isset($relations_fieldset[0]), 'Relations fieldset element found.');
+ }
+
+ /**
+ * Edits the forum taxonomy.
+ */
+ function editForumTaxonomy() {
+ // Backup forum taxonomy.
+ $vid = variable_get('forum_nav_vocabulary', '');
+ $original_settings = taxonomy_vocabulary_load($vid);
+
+ // Generate a random name/description.
+ $title = $this->randomName(10);
+ $description = $this->randomName(100);
+
+ $edit = array(
+ 'name' => $title,
+ 'description' => $description,
+ 'machine_name' => drupal_strtolower(drupal_substr($this->randomName(), 3, 9)),
+ );
+
+ // Edit the vocabulary.
+ $this->drupalPost('admin/structure/taxonomy/' . $original_settings->machine_name . '/edit', $edit, t('Save'));
+ $this->assertResponse(200);
+ $this->assertRaw(t('Updated vocabulary %name.', array('%name' => $title)), 'Vocabulary was edited');
+
+ // Grab the newly edited vocabulary.
+ entity_get_controller('taxonomy_vocabulary')->resetCache();
+ $current_settings = taxonomy_vocabulary_load($vid);
+
+ // Make sure we actually edited the vocabulary properly.
+ $this->assertEqual($current_settings->name, $title, 'The name was updated');
+ $this->assertEqual($current_settings->description, $description, 'The description was updated');
+
+ // Restore the original vocabulary.
+ taxonomy_vocabulary_save($original_settings);
+ drupal_static_reset('taxonomy_vocabulary_load');
+ $current_settings = taxonomy_vocabulary_load($vid);
+ $this->assertEqual($current_settings->name, $original_settings->name, 'The original vocabulary settings were restored');
+ }
+
+ /**
+ * Creates a forum container or a forum.
+ *
+ * @param $type
+ * The forum type (forum container or forum).
+ * @param $parent
+ * The forum parent. This defaults to 0, indicating a root forum.
+ * another forum).
+ *
+ * @return
+ * The created taxonomy term data.
+ */
+ function createForum($type, $parent = 0) {
+ // Generate a random name/description.
+ $name = $this->randomName(10);
+ $description = $this->randomName(100);
+
+ $edit = array(
+ 'name' => $name,
+ 'description' => $description,
+ 'parent[0]' => $parent,
+ 'weight' => '0',
+ );
+
+ // Create forum.
+ $this->drupalPost('admin/structure/forum/add/' . $type, $edit, t('Save'));
+ $this->assertResponse(200);
+ $type = ($type == 'container') ? 'forum container' : 'forum';
+ $this->assertRaw(t('Created new @type %term.', array('%term' => $name, '@type' => t($type))), format_string('@type was created', array('@type' => ucfirst($type))));
+
+ // Verify forum.
+ $term = db_query("SELECT * FROM {taxonomy_term_data} t WHERE t.vid = :vid AND t.name = :name AND t.description = :desc", array(':vid' => variable_get('forum_nav_vocabulary', ''), ':name' => $name, ':desc' => $description))->fetchAssoc();
+ $this->assertTrue(!empty($term), 'The ' . $type . ' exists in the database');
+
+ // Verify forum hierarchy.
+ $tid = $term['tid'];
+ $parent_tid = db_query("SELECT t.parent FROM {taxonomy_term_hierarchy} t WHERE t.tid = :tid", array(':tid' => $tid))->fetchField();
+ $this->assertTrue($parent == $parent_tid, 'The ' . $type . ' is linked to its container');
+
+ return $term;
+ }
+
+ /**
+ * Deletes a forum.
+ *
+ * @param $tid
+ * The forum ID.
+ */
+ function deleteForum($tid) {
+ // Delete the forum.
+ $this->drupalPost('admin/structure/forum/edit/forum/' . $tid, array(), t('Delete'));
+ $this->drupalPost(NULL, array(), t('Delete'));
+
+ // Assert that the forum no longer exists.
+ $this->drupalGet('forum/' . $tid);
+ $this->assertResponse(404, 'The forum was not found');
+
+ // Assert that the associated term has been removed from the
+ // forum_containers variable.
+ $containers = variable_get('forum_containers', array());
+ $this->assertFalse(in_array($tid, $containers), 'The forum_containers variable has been updated.');
+ }
+
+ /**
+ * Runs basic tests on the indicated user.
+ *
+ * @param $user
+ * The logged in user.
+ * @param $admin
+ * User has 'access administration pages' privilege.
+ */
+ private function doBasicTests($user, $admin) {
+ // Login the user.
+ $this->drupalLogin($user);
+ // Attempt to create forum topic under a container.
+ $this->createForumTopic($this->container, TRUE);
+ // Create forum node.
+ $node = $this->createForumTopic($this->forum, FALSE);
+ // Verify the user has access to all the forum nodes.
+ $this->verifyForums($user, $node, $admin);
+ }
+
+ /**
+ * Creates forum topic.
+ *
+ * @param array $forum
+ * A forum array.
+ * @param boolean $container
+ * TRUE if $forum is a container; FALSE otherwise.
+ *
+ * @return object
+ * The created topic node.
+ */
+ function createForumTopic($forum, $container = FALSE) {
+ // Generate a random subject/body.
+ $title = $this->randomName(20);
+ $body = $this->randomName(200);
+
+ $langcode = LANGUAGE_NONE;
+ $edit = array(
+ "title" => $title,
+ "body[$langcode][0][value]" => $body,
+ );
+ $tid = $forum['tid'];
+
+ // Create the forum topic, preselecting the forum ID via a URL parameter.
+ $this->drupalPost('node/add/forum/' . $tid, $edit, t('Save'));
+
+ $type = t('Forum topic');
+ if ($container) {
+ $this->assertNoRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), 'Forum topic was not created');
+ $this->assertRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), 'Error message was shown');
+ return;
+ }
+ else {
+ $this->assertRaw(t('@type %title has been created.', array('@type' => $type, '%title' => $title)), 'Forum topic was created');
+ $this->assertNoRaw(t('The item %title is a forum container, not a forum.', array('%title' => $forum['name'])), 'No error message was shown');
+ }
+
+ // Retrieve node object, ensure that the topic was created and in the proper forum.
+ $node = $this->drupalGetNodeByTitle($title);
+ $this->assertTrue($node != NULL, format_string('Node @title was loaded', array('@title' => $title)));
+ $this->assertEqual($node->taxonomy_forums[LANGUAGE_NONE][0]['tid'], $tid, 'Saved forum topic was in the expected forum');
+
+ // View forum topic.
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertRaw($title, 'Subject was found');
+ $this->assertRaw($body, 'Body was found');
+
+ return $node;
+ }
+
+ /**
+ * Verifies that the logged in user has access to a forum nodes.
+ *
+ * @param $node_user
+ * The user who creates the node.
+ * @param $node
+ * The node being checked.
+ * @param $admin
+ * Boolean to indicate whether the user can 'access administration pages'.
+ * @param $response
+ * The exptected HTTP response code.
+ */
+ private function verifyForums($node_user, $node, $admin, $response = 200) {
+ $response2 = ($admin) ? 200 : 403;
+
+ // View forum help node.
+ $this->drupalGet('admin/help/forum');
+ $this->assertResponse($response2);
+ if ($response2 == 200) {
+ $this->assertTitle(t('Forum | Drupal'), 'Forum help title was displayed');
+ $this->assertText(t('Forum'), 'Forum help node was displayed');
+ }
+
+ // Verify the forum blocks were displayed.
+ $this->drupalGet('');
+ $this->assertResponse(200);
+ $this->assertText(t('New forum topics'), '[New forum topics] Forum block was displayed');
+
+ // View forum container page.
+ $this->verifyForumView($this->container);
+ // View forum page.
+ $this->verifyForumView($this->forum, $this->container);
+ // View root forum page.
+ $this->verifyForumView($this->root_forum);
+
+ // View forum node.
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertResponse(200);
+ $this->assertTitle($node->title . ' | Drupal', 'Forum node was displayed');
+ $breadcrumb = array(
+ l(t('Home'), NULL),
+ l(t('Forums'), 'forum'),
+ l($this->container['name'], 'forum/' . $this->container['tid']),
+ l($this->forum['name'], 'forum/' . $this->forum['tid']),
+ );
+ $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), 'Breadcrumbs were displayed');
+
+ // View forum edit node.
+ $this->drupalGet('node/' . $node->nid . '/edit');
+ $this->assertResponse($response);
+ if ($response == 200) {
+ $this->assertTitle('Edit Forum topic ' . $node->title . ' | Drupal', 'Forum edit node was displayed');
+ }
+
+ if ($response == 200) {
+ // Edit forum node (including moving it to another forum).
+ $edit = array();
+ $langcode = LANGUAGE_NONE;
+ $edit["title"] = 'node/' . $node->nid;
+ $edit["body[$langcode][0][value]"] = $this->randomName(256);
+ // Assume the topic is initially associated with $forum.
+ $edit["taxonomy_forums[$langcode]"] = $this->root_forum['tid'];
+ $edit['shadow'] = TRUE;
+ $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
+ $this->assertRaw(t('Forum topic %title has been updated.', array('%title' => $edit["title"])), 'Forum node was edited');
+
+ // Verify topic was moved to a different forum.
+ $forum_tid = db_query("SELECT tid FROM {forum} WHERE nid = :nid AND vid = :vid", array(
+ ':nid' => $node->nid,
+ ':vid' => $node->vid,
+ ))->fetchField();
+ $this->assertTrue($forum_tid == $this->root_forum['tid'], 'The forum topic is linked to a different forum');
+
+ // Delete forum node.
+ $this->drupalPost('node/' . $node->nid . '/delete', array(), t('Delete'));
+ $this->assertResponse($response);
+ $this->assertRaw(t('Forum topic %title has been deleted.', array('%title' => $edit['title'])), 'Forum node was deleted');
+ }
+ }
+
+ /**
+ * Verifies display of forum page.
+ *
+ * @param $forum
+ * A row from the taxonomy_term_data table in an array.
+ * @param $parent
+ * (optional) An array representing the forum's parent.
+ */
+ private function verifyForumView($forum, $parent = NULL) {
+ // View forum page.
+ $this->drupalGet('forum/' . $forum['tid']);
+ $this->assertResponse(200);
+ $this->assertTitle($forum['name'] . ' | Drupal', 'Forum name was displayed');
+
+ $breadcrumb = array(
+ l(t('Home'), NULL),
+ l(t('Forums'), 'forum'),
+ );
+ if (isset($parent)) {
+ $breadcrumb[] = l($parent['name'], 'forum/' . $parent['tid']);
+ }
+
+ $this->assertRaw(theme('breadcrumb', array('breadcrumb' => $breadcrumb)), 'Breadcrumbs were displayed');
+ }
+
+ /**
+ * Generates forum topics to test the display of an active forum block.
+ *
+ * @param array $forum
+ * The foorum array (a row from taxonomy_term_data table).
+ */
+ private function generateForumTopics($forum) {
+ $this->nids = array();
+ for ($i = 0; $i < 5; $i++) {
+ $node = $this->createForumTopic($this->forum, FALSE);
+ $this->nids[] = $node->nid;
+ }
+ }
+
+ /**
+ * Views forum topics to test the display of an active forum block.
+ *
+ * @todo The logic here is completely incorrect, since the active forum topics
+ * block is determined by comments on the node, not by views.
+ * @todo DIE
+ *
+ * @param $nids
+ * An array of forum node IDs.
+ */
+ private function viewForumTopics($nids) {
+ for ($i = 0; $i < 2; $i++) {
+ foreach ($nids as $nid) {
+ $this->drupalGet('node/' . $nid);
+ $this->drupalGet('node/' . $nid);
+ $this->drupalGet('node/' . $nid);
+ }
+ }
+ }
+}
+
+/**
+ * Tests the forum index listing.
+ */
+class ForumIndexTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Forum index',
+ 'description' => 'Tests the forum index listing.',
+ 'group' => 'Forum',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('taxonomy', 'comment', 'forum');
+
+ // Create a test user.
+ $web_user = $this->drupalCreateUser(array('create forum content', 'edit own forum content', 'edit any forum content', 'administer nodes'));
+ $this->drupalLogin($web_user);
+ }
+
+ /**
+ * Tests the forum index for published and unpublished nodes.
+ */
+ function testForumIndexStatus() {
+
+ $langcode = LANGUAGE_NONE;
+
+ // The forum ID to use.
+ $tid = 1;
+
+ // Create a test node.
+ $title = $this->randomName(20);
+ $edit = array(
+ "title" => $title,
+ "body[$langcode][0][value]" => $this->randomName(200),
+ );
+
+ // Create the forum topic, preselecting the forum ID via a URL parameter.
+ $this->drupalPost('node/add/forum/' . $tid, $edit, t('Save'));
+
+ // Check that the node exists in the database.
+ $node = $this->drupalGetNodeByTitle($title);
+ $this->assertTrue(!empty($node), 'New forum node found in database.');
+
+ // Verify that the node appears on the index.
+ $this->drupalGet('forum/' . $tid);
+ $this->assertText($title, 'Published forum topic appears on index.');
+
+ // Unpublish the node.
+ $edit = array(
+ 'status' => FALSE,
+ );
+ $this->drupalPost("node/{$node->nid}/edit", $edit, t('Save'));
+ $this->drupalGet("node/{$node->nid}");
+ $this->assertText(t('Access denied'), 'Unpublished node is no longer accessible.');
+
+ // Verify that the node no longer appears on the index.
+ $this->drupalGet('forum/' . $tid);
+ $this->assertNoText($title, 'Unpublished forum topic no longer appears on index.');
+ }
+}
diff --git a/drupal-dev/modules/forum/forums.tpl.php b/drupal-dev/modules/forum/forums.tpl.php
new file mode 100644
index 0000000..6a0e02e
--- /dev/null
+++ b/drupal-dev/modules/forum/forums.tpl.php
@@ -0,0 +1,24 @@
+
+
+
';
+
+ return $output;
+}
+
diff --git a/drupal-dev/modules/help/help.api.php b/drupal-dev/modules/help/help.api.php
new file mode 100644
index 0000000..f7d9c08
--- /dev/null
+++ b/drupal-dev/modules/help/help.api.php
@@ -0,0 +1,63 @@
+' . t('Blocks are boxes of content rendered into an area, or region, of a web page. The default theme Bartik, for example, implements the regions "Sidebar first", "Sidebar second", "Featured", "Content", "Header", "Footer", etc., and a block may appear in any one of these areas. The blocks administration page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions.', array('@blocks' => url('admin/structure/block'))) . '';
+
+ // Help for another path in the block module
+ case 'admin/structure/block':
+ return '
' . t('This page provides a drag-and-drop interface for assigning a block to a region, and for controlling the order of blocks within regions. Since not all themes implement the same regions, or display regions in the same way, blocks are positioned on a per-theme basis. Remember that your changes will not be saved until you click the Save blocks button at the bottom of the page.') . '
' . t('Follow these steps to set up and start using your website:') . '
';
+ $output .= '';
+ $output .= '
' . t('Configure your website Once logged in, visit the administration section, where you can customize and configure all aspects of your website.', array('@admin' => url('admin'), '@config' => url('admin/config'))) . '
';
+ $output .= '
' . t('Enable additional functionality Next, visit the module list and enable features which suit your specific needs. You can find additional modules in the Drupal modules download section.', array('@modules' => url('admin/modules'), '@download_modules' => 'http://drupal.org/project/modules')) . '
';
+ $output .= '
' . t('Customize your website design To change the "look and feel" of your website, visit the themes section. You may choose from one of the included themes or download additional themes from the Drupal themes download section.', array('@themes' => url('admin/appearance'), '@download_themes' => 'http://drupal.org/project/themes')) . '
';
+ $output .= '
' . t('Start posting content Finally, you can add new content for your website.', array('@content' => url('node/add'))) . '
';
+ $output .= '';
+ $output .= '
' . t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks. You may also post at the Drupal forum or view the wide range of other support options available.', array('@help' => url('admin/help'), '@handbook' => 'http://drupal.org/documentation', '@forum' => 'http://drupal.org/forum', '@support' => 'http://drupal.org/support')) . '
' . t('The Help module provides Help reference pages and context-sensitive advice to guide you through the use and configuration of modules. It is a starting point for the online Drupal handbooks. The handbooks contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online handbook entry for the Help module.', array('@help' => 'http://drupal.org/documentation/modules/help/', '@handbook' => 'http://drupal.org/documentation', '@help-page' => url('admin/help'))) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Providing a help reference') . '
';
+ $output .= '
' . t('The Help module displays explanations for using each module listed on the main Help reference page.', array('@help' => url('admin/help'))) . '
';
+ $output .= '
' . t('Providing context-sensitive help') . '
';
+ $output .= '
' . t('The Help module displays context-sensitive advice and explanations on various pages.') . '
';
+ $output .= '
';
+ return $output;
+ }
+}
diff --git a/drupal-dev/modules/help/help.test b/drupal-dev/modules/help/help.test
new file mode 100644
index 0000000..da12ccc
--- /dev/null
+++ b/drupal-dev/modules/help/help.test
@@ -0,0 +1,137 @@
+ 'Help functionality',
+ 'description' => 'Verify help display and user access to help based on permissions.',
+ 'group' => 'Help',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('blog', 'poll');
+
+ $this->getModuleList();
+
+ // Create users.
+ $this->big_user = $this->drupalCreateUser(array('access administration pages', 'view the administration theme', 'administer permissions'));
+ $this->any_user = $this->drupalCreateUser(array());
+ }
+
+ /**
+ * Logs in users, creates dblog events, and tests dblog functionality.
+ */
+ function testHelp() {
+ // Login the admin user.
+ $this->drupalLogin($this->big_user);
+ $this->verifyHelp();
+
+ // Login the regular user.
+ $this->drupalLogin($this->any_user);
+ $this->verifyHelp(403);
+
+ // Check for css on admin/help.
+ $this->drupalLogin($this->big_user);
+ $this->drupalGet('admin/help');
+ $this->assertRaw(drupal_get_path('module', 'help') . '/help.css', 'The help.css file is present in the HTML.');
+
+ // Verify that introductory help text exists, goes for 100% module coverage.
+ $this->assertRaw(t('For more information, refer to the specific topics listed in the next section or to the online Drupal handbooks.', array('@drupal' => 'http://drupal.org/documentation')), 'Help intro text correctly appears.');
+
+ // Verify that help topics text appears.
+ $this->assertRaw('
' . t('Help topics') . '
' . t('Help is available on the following items:') . '
', 'Help topics text correctly appears.');
+
+ // Make sure links are properly added for modules implementing hook_help().
+ foreach ($this->modules as $module => $name) {
+ $this->assertLink($name, 0, format_string('Link properly added to @name (admin/help/@module)', array('@module' => $module, '@name' => $name)));
+ }
+ }
+
+ /**
+ * Verifies the logged in user has access to the various help nodes.
+ *
+ * @param integer $response
+ * An HTTP response code.
+ */
+ protected function verifyHelp($response = 200) {
+ foreach ($this->modules as $module => $name) {
+ // View module help node.
+ $this->drupalGet('admin/help/' . $module);
+ $this->assertResponse($response);
+ if ($response == 200) {
+ $this->assertTitle($name . ' | Drupal', format_string('%module title was displayed', array('%module' => $module)));
+ $this->assertRaw('
' . t($name) . '
', format_string('%module heading was displayed', array('%module' => $module)));
+ }
+ }
+ }
+
+ /**
+ * Gets the list of enabled modules that implement hook_help().
+ *
+ * @return array
+ * A list of enabled modules.
+ */
+ protected function getModuleList() {
+ $this->modules = array();
+ $result = db_query("SELECT name, filename, info FROM {system} WHERE type = 'module' AND status = 1 ORDER BY weight ASC, filename ASC");
+ foreach ($result as $module) {
+ if (file_exists($module->filename) && function_exists($module->name . '_help')) {
+ $fullname = unserialize($module->info);
+ $this->modules[$module->name] = $fullname['name'];
+ }
+ }
+ }
+}
+
+/**
+ * Tests a module without help to verify it is not listed in the help page.
+ */
+class NoHelpTestCase extends DrupalWebTestCase {
+ /**
+ * The user who will be created.
+ */
+ protected $big_user;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'No help',
+ 'description' => 'Verify no help is displayed for modules not providing any help.',
+ 'group' => 'Help',
+ );
+ }
+
+ function setUp() {
+ // Use one of the test modules that do not implement hook_help().
+ parent::setUp('menu_test');
+ $this->big_user = $this->drupalCreateUser(array('access administration pages'));
+ }
+
+ /**
+ * Ensures modules not implementing help do not appear on admin/help.
+ */
+ function testMainPageNoHelp() {
+ $this->drupalLogin($this->big_user);
+
+ $this->drupalGet('admin/help');
+ $this->assertNoText('Hook menu tests', 'Making sure the test module menu_test does not display a help link in admin/help');
+ }
+}
diff --git a/drupal-dev/modules/image/image-rtl.css b/drupal-dev/modules/image/image-rtl.css
new file mode 100644
index 0000000..2a7a855
--- /dev/null
+++ b/drupal-dev/modules/image/image-rtl.css
@@ -0,0 +1,11 @@
+
+/**
+ * Image upload widget.
+ */
+div.image-preview {
+ float: right;
+ padding: 0 0 10px 10px;
+}
+div.image-widget-data {
+ float: right;
+}
diff --git a/drupal-dev/modules/image/image.admin.css b/drupal-dev/modules/image/image.admin.css
new file mode 100644
index 0000000..3115c8d
--- /dev/null
+++ b/drupal-dev/modules/image/image.admin.css
@@ -0,0 +1,60 @@
+
+/**
+ * Image style configuration pages.
+ */
+div.image-style-new,
+div.image-style-new div {
+ display: inline;
+}
+div.image-style-preview div.preview-image-wrapper {
+ float: left;
+ padding-bottom: 2em;
+ text-align: center;
+ top: 50%;
+ width: 48%;
+}
+div.image-style-preview div.preview-image {
+ margin: auto;
+ position: relative;
+}
+div.image-style-preview div.preview-image div.width {
+ border: 1px solid #666;
+ border-top: none;
+ height: 2px;
+ left: -1px;
+ bottom: -6px;
+ position: absolute;
+}
+div.image-style-preview div.preview-image div.width span {
+ position: relative;
+ top: 4px;
+}
+div.image-style-preview div.preview-image div.height {
+ border: 1px solid #666;
+ border-left: none;
+ position: absolute;
+ right: -6px;
+ top: -1px;
+ width: 2px;
+}
+div.image-style-preview div.preview-image div.height span {
+ height: 2em;
+ left: 10px;
+ margin-top: -1em;
+ position: absolute;
+ top: 50%;
+}
+
+/**
+ * Image anchor element.
+ */
+table.image-anchor {
+ width: auto;
+}
+table.image-anchor tr.even,
+table.image-anchor tr.odd {
+ background: none;
+}
+table.image-anchor td {
+ border: 1px solid #CCC;
+}
diff --git a/drupal-dev/modules/image/image.admin.inc b/drupal-dev/modules/image/image.admin.inc
new file mode 100644
index 0000000..7e62621
--- /dev/null
+++ b/drupal-dev/modules/image/image.admin.inc
@@ -0,0 +1,925 @@
+ theme('image_style_list', array('styles' => $styles)),
+ '#attached' => array(
+ 'css' => array(drupal_get_path('module', 'image') . '/image.admin.css' => array()),
+ ),
+ );
+
+ return $page;
+
+}
+
+/**
+ * Form builder; Edit an image style name and effects order.
+ *
+ * @param $form_state
+ * An associative array containing the current state of the form.
+ * @param $style
+ * An image style array.
+ * @ingroup forms
+ * @see image_style_form_submit()
+ */
+function image_style_form($form, &$form_state, $style) {
+ $title = t('Edit %name style', array('%name' => $style['label']));
+ drupal_set_title($title, PASS_THROUGH);
+
+ // Adjust this form for styles that must be overridden to edit.
+ $editable = (bool) ($style['storage'] & IMAGE_STORAGE_EDITABLE);
+
+ if (!$editable && empty($form_state['input'])) {
+ drupal_set_message(t('This image style is currently being provided by a module. Click the "Override defaults" button to change its settings.'), 'warning');
+ }
+
+ $form_state['image_style'] = $style;
+ $form['#tree'] = TRUE;
+ $form['#attached']['css'][drupal_get_path('module', 'image') . '/image.admin.css'] = array();
+
+ // Show the thumbnail preview.
+ $form['preview'] = array(
+ '#type' => 'item',
+ '#title' => t('Preview'),
+ '#markup' => theme('image_style_preview', array('style' => $style)),
+ );
+
+ // Show the Image Style label.
+ $form['label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Image style name'),
+ '#default_value' => $style['label'],
+ '#disabled' => !$editable,
+ '#required' => TRUE,
+ );
+
+ // Allow the name of the style to be changed, unless this style is
+ // provided by a module's hook_default_image_styles().
+ $form['name'] = array(
+ '#type' => 'machine_name',
+ '#size' => '64',
+ '#default_value' => $style['name'],
+ '#disabled' => !$editable,
+ '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'),
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'image_style_load',
+ 'source' => array('label'),
+ 'replace_pattern' => '[^0-9a-z_\-]',
+ 'error' => t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'),
+ ),
+ );
+
+ // Build the list of existing image effects for this image style.
+ $form['effects'] = array(
+ '#theme' => 'image_style_effects',
+ );
+ foreach ($style['effects'] as $key => $effect) {
+ $form['effects'][$key]['#weight'] = isset($form_state['input']['effects']) ? $form_state['input']['effects'][$key]['weight'] : NULL;
+ $form['effects'][$key]['label'] = array(
+ '#markup' => $effect['label'],
+ );
+ $form['effects'][$key]['summary'] = array(
+ '#markup' => isset($effect['summary theme']) ? theme($effect['summary theme'], array('data' => $effect['data'])) : '',
+ );
+ $form['effects'][$key]['weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight for @title', array('@title' => $effect['label'])),
+ '#title_display' => 'invisible',
+ '#default_value' => $effect['weight'],
+ '#access' => $editable,
+ );
+
+ // Only attempt to display these fields for editable styles as the 'ieid'
+ // key is not set for styles defined in code.
+ if ($editable) {
+ $form['effects'][$key]['configure'] = array(
+ '#type' => 'link',
+ '#title' => t('edit'),
+ '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'],
+ '#access' => $editable && isset($effect['form callback']),
+ );
+ $form['effects'][$key]['remove'] = array(
+ '#type' => 'link',
+ '#title' => t('delete'),
+ '#href' => 'admin/config/media/image-styles/edit/' . $style['name'] . '/effects/' . $effect['ieid'] . '/delete',
+ '#access' => $editable,
+ );
+ }
+ }
+
+ // Build the new image effect addition form and add it to the effect list.
+ $new_effect_options = array();
+ foreach (image_effect_definitions() as $effect => $definition) {
+ $new_effect_options[$effect] = check_plain($definition['label']);
+ }
+ $form['effects']['new'] = array(
+ '#tree' => FALSE,
+ '#weight' => isset($form_state['input']['weight']) ? $form_state['input']['weight'] : NULL,
+ '#access' => $editable,
+ );
+ $form['effects']['new']['new'] = array(
+ '#type' => 'select',
+ '#title' => t('Effect'),
+ '#title_display' => 'invisible',
+ '#options' => $new_effect_options,
+ '#empty_option' => t('Select a new effect'),
+ );
+ $form['effects']['new']['weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight for new effect'),
+ '#title_display' => 'invisible',
+ '#default_value' => count($form['effects']) - 1,
+ );
+ $form['effects']['new']['add'] = array(
+ '#type' => 'submit',
+ '#value' => t('Add'),
+ '#validate' => array('image_style_form_add_validate'),
+ '#submit' => array('image_style_form_submit', 'image_style_form_add_submit'),
+ );
+
+ // Show the Override or Submit button for this style.
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['override'] = array(
+ '#type' => 'submit',
+ '#value' => t('Override defaults'),
+ '#validate' => array(),
+ '#submit' => array('image_style_form_override_submit'),
+ '#access' => !$editable,
+ );
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Update style'),
+ '#access' => $editable,
+ );
+
+ return $form;
+}
+
+/**
+ * Validate handler for adding a new image effect to an image style.
+ */
+function image_style_form_add_validate($form, &$form_state) {
+ if (!$form_state['values']['new']) {
+ form_error($form['effects']['new']['new'], t('Select an effect to add.'));
+ }
+}
+
+/**
+ * Submit handler for adding a new image effect to an image style.
+ */
+function image_style_form_add_submit($form, &$form_state) {
+ $style = $form_state['image_style'];
+ // Check if this field has any configuration options.
+ $effect = image_effect_definition_load($form_state['values']['new']);
+
+ // Load the configuration form for this option.
+ if (isset($effect['form callback'])) {
+ $path = 'admin/config/media/image-styles/edit/' . $form_state['image_style']['name'] . '/add/' . $form_state['values']['new'];
+ $form_state['redirect'] = array($path, array('query' => array('weight' => $form_state['values']['weight'])));
+ }
+ // If there's no form, immediately add the image effect.
+ else {
+ $effect['isid'] = $style['isid'];
+ $effect['weight'] = $form_state['values']['weight'];
+ image_effect_save($effect);
+ drupal_set_message(t('The image effect was successfully applied.'));
+ }
+}
+
+/**
+ * Submit handler for overriding a module-defined style.
+ */
+function image_style_form_override_submit($form, &$form_state) {
+ drupal_set_message(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $form_state['image_style']['label'])));
+ image_default_style_save($form_state['image_style']);
+}
+
+/**
+ * Submit handler for saving an image style.
+ */
+function image_style_form_submit($form, &$form_state) {
+ // Update the image style.
+ $style = $form_state['image_style'];
+ $style['name'] = $form_state['values']['name'];
+ $style['label'] = $form_state['values']['label'];
+
+ // Update image effect weights.
+ if (!empty($form_state['values']['effects'])) {
+ foreach ($form_state['values']['effects'] as $ieid => $effect_data) {
+ if (isset($style['effects'][$ieid])) {
+ $effect = $style['effects'][$ieid];
+ $effect['weight'] = $effect_data['weight'];
+ image_effect_save($effect);
+ }
+ }
+ }
+
+ image_style_save($style);
+ if ($form_state['values']['op'] == t('Update style')) {
+ drupal_set_message(t('Changes to the style have been saved.'));
+ }
+ $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name'];
+}
+
+/**
+ * Form builder; Form for adding a new image style.
+ *
+ * @ingroup forms
+ * @see image_style_add_form_submit()
+ */
+function image_style_add_form($form, &$form_state) {
+ $form['label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Style name'),
+ '#default_value' => '',
+ '#required' => TRUE,
+ );
+ $form['name'] = array(
+ '#type' => 'machine_name',
+ '#description' => t('The name is used in URLs for generated images. Use only lowercase alphanumeric characters, underscores (_), and hyphens (-).'),
+ '#size' => '64',
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'image_style_load',
+ 'source' => array('label'),
+ 'replace_pattern' => '[^0-9a-z_\-]',
+ 'error' => t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'),
+ ),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Create new style'),
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for adding a new image style.
+ */
+function image_style_add_form_submit($form, &$form_state) {
+ $style = array(
+ 'name' => $form_state['values']['name'],
+ 'label' => $form_state['values']['label'],
+ );
+ $style = image_style_save($style);
+ drupal_set_message(t('Style %name was created.', array('%name' => $style['label'])));
+ $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name'];
+}
+
+/**
+ * Element validate function to ensure unique, URL safe style names.
+ *
+ * This function is no longer used in Drupal core since image style names are
+ * now validated using #machine_name functionality. It is kept for backwards
+ * compatibility (since non-core modules may be using it) and will be removed
+ * in Drupal 8.
+ */
+function image_style_name_validate($element, $form_state) {
+ // Check for duplicates.
+ $styles = image_styles();
+ if (isset($styles[$element['#value']]) && (!isset($form_state['image_style']['isid']) || $styles[$element['#value']]['isid'] != $form_state['image_style']['isid'])) {
+ form_set_error($element['#name'], t('The image style name %name is already in use.', array('%name' => $element['#value'])));
+ }
+
+ // Check for illegal characters in image style names.
+ if (preg_match('/[^0-9a-z_\-]/', $element['#value'])) {
+ form_set_error($element['#name'], t('Please only use lowercase alphanumeric characters, underscores (_), and hyphens (-) for style names.'));
+ }
+}
+
+/**
+ * Form builder; Form for deleting an image style.
+ *
+ * @param $style
+ * An image style array.
+ *
+ * @ingroup forms
+ * @see image_style_delete_form_submit()
+ */
+function image_style_delete_form($form, &$form_state, $style) {
+ $form_state['image_style'] = $style;
+
+ $replacement_styles = array_diff_key(image_style_options(TRUE, PASS_THROUGH), array($style['name'] => ''));
+ $form['replacement'] = array(
+ '#title' => t('Replacement style'),
+ '#type' => 'select',
+ '#options' => $replacement_styles,
+ '#empty_option' => t('No replacement, just delete'),
+ );
+
+ return confirm_form(
+ $form,
+ t('Optionally select a style before deleting %style', array('%style' => $style['label'])),
+ 'admin/config/media/image-styles',
+ t('If this style is in use on the site, you may select another style to replace it. All images that have been generated for this style will be permanently deleted.'),
+ t('Delete'), t('Cancel')
+ );
+}
+
+/**
+ * Submit handler to delete an image style.
+ */
+function image_style_delete_form_submit($form, &$form_state) {
+ $style = $form_state['image_style'];
+
+ image_style_delete($style, $form_state['values']['replacement']);
+ drupal_set_message(t('Style %name was deleted.', array('%name' => $style['label'])));
+ $form_state['redirect'] = 'admin/config/media/image-styles';
+}
+
+/**
+ * Confirmation form to revert a database style to its default.
+ */
+function image_style_revert_form($form, &$form_state, $style) {
+ $form_state['image_style'] = $style;
+
+ return confirm_form(
+ $form,
+ t('Revert the %style style?', array('%style' => $style['label'])),
+ 'admin/config/media/image-styles',
+ t('Reverting this style will delete the customized settings and restore the defaults provided by the @module module.', array('@module' => $style['module'])),
+ t('Revert'), t('Cancel')
+ );
+}
+
+/**
+ * Submit handler to convert an overridden style to its default.
+ */
+function image_style_revert_form_submit($form, &$form_state) {
+ drupal_set_message(t('The %style style has been reverted to its defaults.', array('%style' => $form_state['image_style']['label'])));
+ image_default_style_revert($form_state['image_style']);
+ $form_state['redirect'] = 'admin/config/media/image-styles';
+}
+
+/**
+ * Form builder; Form for adding and editing image effects.
+ *
+ * This form is used universally for editing all image effects. Each effect adds
+ * its own custom section to the form by calling the form function specified in
+ * hook_image_effects().
+ *
+ * @param $form_state
+ * An associative array containing the current state of the form.
+ * @param $style
+ * An image style array.
+ * @param $effect
+ * An image effect array.
+ *
+ * @ingroup forms
+ * @see hook_image_effects()
+ * @see image_effects()
+ * @see image_resize_form()
+ * @see image_scale_form()
+ * @see image_rotate_form()
+ * @see image_crop_form()
+ * @see image_effect_form_submit()
+ */
+function image_effect_form($form, &$form_state, $style, $effect) {
+ if (!empty($effect['data'])) {
+ $title = t('Edit %label effect', array('%label' => $effect['label']));
+ }
+ else{
+ $title = t('Add %label effect', array('%label' => $effect['label']));
+ }
+ drupal_set_title($title, PASS_THROUGH);
+
+ $form_state['image_style'] = $style;
+ $form_state['image_effect'] = $effect;
+
+ // If no configuration for this image effect, return to the image style page.
+ if (!isset($effect['form callback'])) {
+ drupal_goto('admin/config/media/image-styles/edit/' . $style['name']);
+ }
+
+ $form['#tree'] = TRUE;
+ $form['#attached']['css'][drupal_get_path('module', 'image') . '/image.admin.css'] = array();
+ if (function_exists($effect['form callback'])) {
+ $form['data'] = call_user_func($effect['form callback'], $effect['data']);
+ }
+
+ // Check the URL for a weight, then the image effect, otherwise use default.
+ $form['weight'] = array(
+ '#type' => 'hidden',
+ '#value' => isset($_GET['weight']) ? intval($_GET['weight']) : (isset($effect['weight']) ? $effect['weight'] : count($style['effects'])),
+ );
+
+ $form['actions'] = array('#tree' => FALSE, '#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => isset($effect['ieid']) ? t('Update effect') : t('Add effect'),
+ );
+ $form['actions']['cancel'] = array(
+ '#type' => 'link',
+ '#title' => t('Cancel'),
+ '#href' => 'admin/config/media/image-styles/edit/' . $style['name'],
+ );
+
+ return $form;
+}
+
+/**
+ * Submit handler for updating an image effect.
+ */
+function image_effect_form_submit($form, &$form_state) {
+ $style = $form_state['image_style'];
+ $effect = array_merge($form_state['image_effect'], $form_state['values']);
+ $effect['isid'] = $style['isid'];
+ image_effect_save($effect);
+ drupal_set_message(t('The image effect was successfully applied.'));
+ $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name'];
+}
+
+/**
+ * Form builder; Form for deleting an image effect.
+ *
+ * @param $style
+ * Name of the image style from which the image effect will be removed.
+ * @param $effect
+ * Name of the image effect to remove.
+ * @ingroup forms
+ * @see image_effect_delete_form_submit()
+ */
+function image_effect_delete_form($form, &$form_state, $style, $effect) {
+ $form_state['image_style'] = $style;
+ $form_state['image_effect'] = $effect;
+
+ $question = t('Are you sure you want to delete the @effect effect from the %style style?', array('%style' => $style['label'], '@effect' => $effect['label']));
+ return confirm_form($form, $question, 'admin/config/media/image-styles/edit/' . $style['name'], '', t('Delete'));
+}
+
+/**
+ * Submit handler to delete an image effect.
+ */
+function image_effect_delete_form_submit($form, &$form_state) {
+ $style = $form_state['image_style'];
+ $effect = $form_state['image_effect'];
+
+ image_effect_delete($effect);
+ drupal_set_message(t('The image effect %name has been deleted.', array('%name' => $effect['label'])));
+ $form_state['redirect'] = 'admin/config/media/image-styles/edit/' . $style['name'];
+}
+
+/**
+ * Element validate handler to ensure an integer pixel value.
+ *
+ * The property #allow_negative = TRUE may be set to allow negative integers.
+ */
+function image_effect_integer_validate($element, &$form_state) {
+ $value = empty($element['#allow_negative']) ? $element['#value'] : preg_replace('/^-/', '', $element['#value']);
+ if ($element['#value'] != '' && (!is_numeric($value) || intval($value) <= 0)) {
+ if (empty($element['#allow_negative'])) {
+ form_error($element, t('!name must be an integer.', array('!name' => $element['#title'])));
+ }
+ else {
+ form_error($element, t('!name must be a positive integer.', array('!name' => $element['#title'])));
+ }
+ }
+}
+
+/**
+ * Element validate handler to ensure a hexadecimal color value.
+ */
+function image_effect_color_validate($element, &$form_state) {
+ if ($element['#value'] != '') {
+ $hex_value = preg_replace('/^#/', '', $element['#value']);
+ if (!preg_match('/^#[0-9A-F]{3}([0-9A-F]{3})?$/', $element['#value'])) {
+ form_error($element, t('!name must be a hexadecimal color value.', array('!name' => $element['#title'])));
+ }
+ }
+}
+
+/**
+ * Element validate handler to ensure that either a height or a width is
+ * specified.
+ */
+function image_effect_scale_validate($element, &$form_state) {
+ if (empty($element['width']['#value']) && empty($element['height']['#value'])) {
+ form_error($element, t('Width and height can not both be blank.'));
+ }
+}
+
+/**
+ * Form structure for the image resize form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the resize options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param $data
+ * The current configuration for this resize effect.
+ */
+function image_resize_form($data) {
+ $form['width'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Width'),
+ '#default_value' => isset($data['width']) ? $data['width'] : '',
+ '#field_suffix' => ' ' . t('pixels'),
+ '#required' => TRUE,
+ '#size' => 10,
+ '#element_validate' => array('image_effect_integer_validate'),
+ '#allow_negative' => FALSE,
+ );
+ $form['height'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Height'),
+ '#default_value' => isset($data['height']) ? $data['height'] : '',
+ '#field_suffix' => ' ' . t('pixels'),
+ '#required' => TRUE,
+ '#size' => 10,
+ '#element_validate' => array('image_effect_integer_validate'),
+ '#allow_negative' => FALSE,
+ );
+ return $form;
+}
+
+/**
+ * Form structure for the image scale form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the scale options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param $data
+ * The current configuration for this scale effect.
+ */
+function image_scale_form($data) {
+ $form = image_resize_form($data);
+ $form['#element_validate'] = array('image_effect_scale_validate');
+ $form['width']['#required'] = FALSE;
+ $form['height']['#required'] = FALSE;
+ $form['upscale'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => (isset($data['upscale'])) ? $data['upscale'] : 0,
+ '#title' => t('Allow Upscaling'),
+ '#description' => t('Let scale make images larger than their original size'),
+ );
+ return $form;
+}
+
+/**
+ * Form structure for the image crop form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the crop options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param $data
+ * The current configuration for this crop effect.
+ */
+function image_crop_form($data) {
+ $data += array(
+ 'width' => '',
+ 'height' => '',
+ 'anchor' => 'center-center',
+ );
+
+ $form = image_resize_form($data);
+ $form['anchor'] = array(
+ '#type' => 'radios',
+ '#title' => t('Anchor'),
+ '#options' => array(
+ 'left-top' => t('Top') . ' ' . t('Left'),
+ 'center-top' => t('Top') . ' ' . t('Center'),
+ 'right-top' => t('Top') . ' ' . t('Right'),
+ 'left-center' => t('Center') . ' ' . t('Left'),
+ 'center-center' => t('Center'),
+ 'right-center' => t('Center') . ' ' . t('Right'),
+ 'left-bottom' => t('Bottom') . ' ' . t('Left'),
+ 'center-bottom' => t('Bottom') . ' ' . t('Center'),
+ 'right-bottom' => t('Bottom') . ' ' . t('Right'),
+ ),
+ '#theme' => 'image_anchor',
+ '#default_value' => $data['anchor'],
+ '#description' => t('The part of the image that will be retained during the crop.'),
+ );
+
+ return $form;
+}
+
+/**
+ * Form structure for the image rotate form.
+ *
+ * Note that this is not a complete form, it only contains the portion of the
+ * form for configuring the rotate options. Therefore it does not not need to
+ * include metadata about the effect, nor a submit button.
+ *
+ * @param $data
+ * The current configuration for this rotate effect.
+ */
+function image_rotate_form($data) {
+ $form['degrees'] = array(
+ '#type' => 'textfield',
+ '#default_value' => (isset($data['degrees'])) ? $data['degrees'] : 0,
+ '#title' => t('Rotation angle'),
+ '#description' => t('The number of degrees the image should be rotated. Positive numbers are clockwise, negative are counter-clockwise.'),
+ '#field_suffix' => '°',
+ '#required' => TRUE,
+ '#size' => 6,
+ '#maxlength' => 4,
+ '#element_validate' => array('image_effect_integer_validate'),
+ '#allow_negative' => TRUE,
+ );
+ $form['bgcolor'] = array(
+ '#type' => 'textfield',
+ '#default_value' => (isset($data['bgcolor'])) ? $data['bgcolor'] : '#FFFFFF',
+ '#title' => t('Background color'),
+ '#description' => t('The background color to use for exposed areas of the image. Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave blank for transparency on image types that support it.'),
+ '#size' => 7,
+ '#maxlength' => 7,
+ '#element_validate' => array('image_effect_color_validate'),
+ );
+ $form['random'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => (isset($data['random'])) ? $data['random'] : 0,
+ '#title' => t('Randomize'),
+ '#description' => t('Randomize the rotation angle for each image. The angle specified above is used as a maximum.'),
+ );
+ return $form;
+}
+
+/**
+ * Returns HTML for the page containing the list of image styles.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - styles: An array of all the image styles returned by image_get_styles().
+ *
+ * @see image_get_styles()
+ * @ingroup themeable
+ */
+function theme_image_style_list($variables) {
+ $styles = $variables['styles'];
+
+ $header = array(t('Style name'), t('Settings'), array('data' => t('Operations'), 'colspan' => 3));
+ $rows = array();
+ foreach ($styles as $style) {
+ $row = array();
+ $row[] = l($style['label'], 'admin/config/media/image-styles/edit/' . $style['name']);
+ $link_attributes = array(
+ 'attributes' => array(
+ 'class' => array('image-style-link'),
+ ),
+ );
+ if ($style['storage'] == IMAGE_STORAGE_NORMAL) {
+ $row[] = t('Custom');
+ $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes);
+ $row[] = l(t('delete'), 'admin/config/media/image-styles/delete/' . $style['name'], $link_attributes);
+ }
+ elseif ($style['storage'] == IMAGE_STORAGE_OVERRIDE) {
+ $row[] = t('Overridden');
+ $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes);
+ $row[] = l(t('revert'), 'admin/config/media/image-styles/revert/' . $style['name'], $link_attributes);
+ }
+ else {
+ $row[] = t('Default');
+ $row[] = l(t('edit'), 'admin/config/media/image-styles/edit/' . $style['name'], $link_attributes);
+ $row[] = '';
+ }
+ $rows[] = $row;
+ }
+
+ if (empty($rows)) {
+ $rows[] = array(array(
+ 'colspan' => 4,
+ 'data' => t('There are currently no styles. Add a new one.', array('!url' => url('admin/config/media/image-styles/add'))),
+ ));
+ }
+
+ return theme('table', array('header' => $header, 'rows' => $rows));
+}
+
+/**
+ * Returns HTML for a listing of the effects within a specific image style.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - form: A render element representing the form.
+ *
+ * @ingroup themeable
+ */
+function theme_image_style_effects($variables) {
+ $form = $variables['form'];
+
+ $rows = array();
+
+ foreach (element_children($form) as $key) {
+ $row = array();
+ $form[$key]['weight']['#attributes']['class'] = array('image-effect-order-weight');
+ if (is_numeric($key)) {
+ $summary = drupal_render($form[$key]['summary']);
+ $row[] = drupal_render($form[$key]['label']) . (empty($summary) ? '' : ' ' . $summary);
+ $row[] = drupal_render($form[$key]['weight']);
+ $row[] = drupal_render($form[$key]['configure']);
+ $row[] = drupal_render($form[$key]['remove']);
+ }
+ else {
+ // Add the row for adding a new image effect.
+ $row[] = '
'; // End image-style-preview.
+
+ return $output;
+}
+
+/**
+ * Returns HTML for a 3x3 grid of checkboxes for image anchors.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element containing radio buttons.
+ *
+ * @ingroup themeable
+ */
+function theme_image_anchor($variables) {
+ $element = $variables['element'];
+
+ $rows = array();
+ $row = array();
+ foreach (element_children($element) as $n => $key) {
+ $element[$key]['#attributes']['title'] = $element[$key]['#title'];
+ unset($element[$key]['#title']);
+ $row[] = drupal_render($element[$key]);
+ if ($n % 3 == 3 - 1) {
+ $rows[] = $row;
+ $row = array();
+ }
+ }
+
+ return theme('table', array('header' => array(), 'rows' => $rows, 'attributes' => array('class' => array('image-anchor'))));
+}
+
+/**
+ * Returns HTML for a summary of an image resize effect.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - data: The current configuration for this resize effect.
+ *
+ * @ingroup themeable
+ */
+function theme_image_resize_summary($variables) {
+ $data = $variables['data'];
+
+ if ($data['width'] && $data['height']) {
+ return check_plain($data['width']) . 'x' . check_plain($data['height']);
+ }
+ else {
+ return ($data['width']) ? t('width @width', array('@width' => $data['width'])) : t('height @height', array('@height' => $data['height']));
+ }
+}
+
+/**
+ * Returns HTML for a summary of an image scale effect.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - data: The current configuration for this scale effect.
+ *
+ * @ingroup themeable
+ */
+function theme_image_scale_summary($variables) {
+ $data = $variables['data'];
+ return theme('image_resize_summary', array('data' => $data)) . ' ' . ($data['upscale'] ? '(' . t('upscaling allowed') . ')' : '');
+}
+
+/**
+ * Returns HTML for a summary of an image crop effect.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - data: The current configuration for this crop effect.
+ *
+ * @ingroup themeable
+ */
+function theme_image_crop_summary($variables) {
+ return theme('image_resize_summary', $variables);
+}
+
+/**
+ * Returns HTML for a summary of an image rotate effect.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - data: The current configuration for this rotate effect.
+ *
+ * @ingroup themeable
+ */
+function theme_image_rotate_summary($variables) {
+ $data = $variables['data'];
+ return ($data['random']) ? t('random between -@degrees° and @degrees°', array('@degrees' => str_replace('-', '', $data['degrees']))) : t('@degrees°', array('@degrees' => $data['degrees']));
+}
diff --git a/drupal-dev/modules/image/image.api.php b/drupal-dev/modules/image/image.api.php
new file mode 100644
index 0000000..8115116
--- /dev/null
+++ b/drupal-dev/modules/image/image.api.php
@@ -0,0 +1,200 @@
+ t('Resize'),
+ 'help' => t('Resize an image to an exact set of dimensions, ignoring aspect ratio.'),
+ 'effect callback' => 'mymodule_resize_effect',
+ 'dimensions callback' => 'mymodule_resize_dimensions',
+ 'form callback' => 'mymodule_resize_form',
+ 'summary theme' => 'mymodule_resize_summary',
+ );
+
+ return $effects;
+}
+
+/**
+ * Alter the information provided in hook_image_effect_info().
+ *
+ * @param $effects
+ * The array of image effects, keyed on the machine-readable effect name.
+ *
+ * @see hook_image_effect_info()
+ */
+function hook_image_effect_info_alter(&$effects) {
+ // Override the Image module's crop effect with more options.
+ $effects['image_crop']['effect callback'] = 'mymodule_crop_effect';
+ $effects['image_crop']['dimensions callback'] = 'mymodule_crop_dimensions';
+ $effects['image_crop']['form callback'] = 'mymodule_crop_form';
+}
+
+/**
+ * Respond to image style updating.
+ *
+ * This hook enables modules to update settings that might be affected by
+ * changes to an image. For example, updating a module specific variable to
+ * reflect a change in the image style's name.
+ *
+ * @param $style
+ * The image style array that is being updated.
+ */
+function hook_image_style_save($style) {
+ // If a module defines an image style and that style is renamed by the user
+ // the module should update any references to that style.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('mymodule_image_style', '')) {
+ variable_set('mymodule_image_style', $style['name']);
+ }
+}
+
+/**
+ * Respond to image style deletion.
+ *
+ * This hook enables modules to update settings when a image style is being
+ * deleted. If a style is deleted, a replacement name may be specified in
+ * $style['name'] and the style being deleted will be specified in
+ * $style['old_name'].
+ *
+ * @param $style
+ * The image style array that being deleted.
+ */
+function hook_image_style_delete($style) {
+ // Administrators can choose an optional replacement style when deleting.
+ // Update the modules style variable accordingly.
+ if (isset($style['old_name']) && $style['old_name'] == variable_get('mymodule_image_style', '')) {
+ variable_set('mymodule_image_style', $style['name']);
+ }
+}
+
+/**
+ * Respond to image style flushing.
+ *
+ * This hook enables modules to take effect when a style is being flushed (all
+ * images are being deleted from the server and regenerated). Any
+ * module-specific caches that contain information related to the style should
+ * be cleared using this hook. This hook is called whenever a style is updated,
+ * deleted, or any effect associated with the style is update or deleted.
+ *
+ * @param $style
+ * The image style array that is being flushed.
+ */
+function hook_image_style_flush($style) {
+ // Empty cached data that contains information about the style.
+ cache_clear_all('*', 'cache_mymodule', TRUE);
+}
+
+/**
+ * Modify any image styles provided by other modules or the user.
+ *
+ * This hook allows modules to modify, add, or remove image styles. This may
+ * be useful to modify default styles provided by other modules or enforce
+ * that a specific effect is always enabled on a style. Note that modifications
+ * to these styles may negatively affect the user experience, such as if an
+ * effect is added to a style through this hook, the user may attempt to remove
+ * the effect but it will be immediately be re-added.
+ *
+ * The best use of this hook is usually to modify default styles, which are not
+ * editable by the user until they are overridden, so such interface
+ * contradictions will not occur. This hook can target default (or user) styles
+ * by checking the $style['storage'] property.
+ *
+ * If your module needs to provide a new style (rather than modify an existing
+ * one) use hook_image_default_styles() instead.
+ *
+ * @see hook_image_default_styles()
+ */
+function hook_image_styles_alter(&$styles) {
+ // Check that we only affect a default style.
+ if ($styles['thumbnail']['storage'] == IMAGE_STORAGE_DEFAULT) {
+ // Add an additional effect to the thumbnail style.
+ $styles['thumbnail']['effects'][] = array(
+ 'name' => 'image_desaturate',
+ 'data' => array(),
+ 'weight' => 1,
+ 'effect callback' => 'image_desaturate_effect',
+ );
+ }
+}
+
+/**
+ * Provide module-based image styles for reuse throughout Drupal.
+ *
+ * This hook allows your module to provide image styles. This may be useful if
+ * you require images to fit within exact dimensions. Note that you should
+ * attempt to re-use the default styles provided by Image module whenever
+ * possible, rather than creating image styles that are specific to your module.
+ * Image provides the styles "thumbnail", "medium", and "large".
+ *
+ * You may use this hook to more easily manage your site's changes by moving
+ * existing image styles from the database to a custom module. Note however that
+ * moving image styles to code instead storing them in the database has a
+ * negligible effect on performance, since custom image styles are loaded
+ * from the database all at once. Even if all styles are pulled from modules,
+ * Image module will still perform the same queries to check the database for
+ * any custom styles.
+ *
+ * @return
+ * An array of image styles, keyed by the style name.
+ * @see image_image_default_styles()
+ */
+function hook_image_default_styles() {
+ $styles = array();
+
+ $styles['mymodule_preview'] = array(
+ 'label' => 'My module preview',
+ 'effects' => array(
+ array(
+ 'name' => 'image_scale',
+ 'data' => array('width' => 400, 'height' => 400, 'upscale' => 1),
+ 'weight' => 0,
+ ),
+ array(
+ 'name' => 'image_desaturate',
+ 'data' => array(),
+ 'weight' => 1,
+ ),
+ ),
+ );
+
+ return $styles;
+}
+
+ /**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/drupal-dev/modules/image/image.css b/drupal-dev/modules/image/image.css
new file mode 100644
index 0000000..7db307b
--- /dev/null
+++ b/drupal-dev/modules/image/image.css
@@ -0,0 +1,14 @@
+
+/**
+ * Image upload widget.
+ */
+div.image-preview {
+ float: left; /* LTR */
+ padding: 0 10px 10px 0; /* LTR */
+}
+div.image-widget-data {
+ float: left; /* LTR */
+}
+div.image-widget-data input.text-field {
+ width: auto;
+}
diff --git a/drupal-dev/modules/image/image.effects.inc b/drupal-dev/modules/image/image.effects.inc
new file mode 100644
index 0000000..35a6a74
--- /dev/null
+++ b/drupal-dev/modules/image/image.effects.inc
@@ -0,0 +1,314 @@
+ array(
+ 'label' => t('Resize'),
+ 'help' => t('Resizing will make images an exact set of dimensions. This may cause images to be stretched or shrunk disproportionately.'),
+ 'effect callback' => 'image_resize_effect',
+ 'dimensions callback' => 'image_resize_dimensions',
+ 'form callback' => 'image_resize_form',
+ 'summary theme' => 'image_resize_summary',
+ ),
+ 'image_scale' => array(
+ 'label' => t('Scale'),
+ 'help' => t('Scaling will maintain the aspect-ratio of the original image. If only a single dimension is specified, the other dimension will be calculated.'),
+ 'effect callback' => 'image_scale_effect',
+ 'dimensions callback' => 'image_scale_dimensions',
+ 'form callback' => 'image_scale_form',
+ 'summary theme' => 'image_scale_summary',
+ ),
+ 'image_scale_and_crop' => array(
+ 'label' => t('Scale and crop'),
+ 'help' => t('Scale and crop will maintain the aspect-ratio of the original image, then crop the larger dimension. This is most useful for creating perfectly square thumbnails without stretching the image.'),
+ 'effect callback' => 'image_scale_and_crop_effect',
+ 'dimensions callback' => 'image_resize_dimensions',
+ 'form callback' => 'image_resize_form',
+ 'summary theme' => 'image_resize_summary',
+ ),
+ 'image_crop' => array(
+ 'label' => t('Crop'),
+ 'help' => t('Cropping will remove portions of an image to make it the specified dimensions.'),
+ 'effect callback' => 'image_crop_effect',
+ 'dimensions callback' => 'image_resize_dimensions',
+ 'form callback' => 'image_crop_form',
+ 'summary theme' => 'image_crop_summary',
+ ),
+ 'image_desaturate' => array(
+ 'label' => t('Desaturate'),
+ 'help' => t('Desaturate converts an image to grayscale.'),
+ 'effect callback' => 'image_desaturate_effect',
+ 'dimensions passthrough' => TRUE,
+ ),
+ 'image_rotate' => array(
+ 'label' => t('Rotate'),
+ 'help' => t('Rotating an image may cause the dimensions of an image to increase to fit the diagonal.'),
+ 'effect callback' => 'image_rotate_effect',
+ 'dimensions callback' => 'image_rotate_dimensions',
+ 'form callback' => 'image_rotate_form',
+ 'summary theme' => 'image_rotate_summary',
+ ),
+ );
+
+ return $effects;
+}
+
+/**
+ * Image effect callback; Resize an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the resize effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ *
+ * @return
+ * TRUE on success. FALSE on failure to resize image.
+ *
+ * @see image_resize()
+ */
+function image_resize_effect(&$image, $data) {
+ if (!image_resize($image, $data['width'], $data['height'])) {
+ watchdog('image', 'Image resize failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image dimensions callback; Resize.
+ *
+ * @param $dimensions
+ * Dimensions to be modified - an array with components width and height, in
+ * pixels.
+ * @param $data
+ * An array of attributes to use when performing the resize effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ */
+function image_resize_dimensions(array &$dimensions, array $data) {
+ // The new image will have the exact dimensions defined for the effect.
+ $dimensions['width'] = $data['width'];
+ $dimensions['height'] = $data['height'];
+}
+
+/**
+ * Image effect callback; Scale an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the scale effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * - "upscale": A boolean indicating that the image should be upscaled if the
+ * dimensions are larger than the original image.
+ *
+ * @return
+ * TRUE on success. FALSE on failure to scale image.
+ *
+ * @see image_scale()
+ */
+function image_scale_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'width' => NULL,
+ 'height' => NULL,
+ 'upscale' => FALSE,
+ );
+
+ if (!image_scale($image, $data['width'], $data['height'], $data['upscale'])) {
+ watchdog('image', 'Image scale failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image dimensions callback; Scale.
+ *
+ * @param $dimensions
+ * Dimensions to be modified - an array with components width and height, in
+ * pixels.
+ * @param $data
+ * An array of attributes to use when performing the scale effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * - "upscale": A boolean indicating that the image should be upscaled if the
+ * dimensions are larger than the original image.
+ */
+function image_scale_dimensions(array &$dimensions, array $data) {
+ if ($dimensions['width'] && $dimensions['height']) {
+ image_dimensions_scale($dimensions, $data['width'], $data['height'], $data['upscale']);
+ }
+}
+
+/**
+ * Image effect callback; Crop an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the crop effect with the
+ * following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * - "anchor": A string describing where the crop should originate in the form
+ * of "XOFFSET-YOFFSET". XOFFSET is either a number of pixels or
+ * "left", "center", "right" and YOFFSET is either a number of pixels or
+ * "top", "center", "bottom".
+ * @return
+ * TRUE on success. FALSE on failure to crop image.
+ * @see image_crop()
+ */
+function image_crop_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'anchor' => 'center-center',
+ );
+
+ list($x, $y) = explode('-', $data['anchor']);
+ $x = image_filter_keyword($x, $image->info['width'], $data['width']);
+ $y = image_filter_keyword($y, $image->info['height'], $data['height']);
+ if (!image_crop($image, $x, $y, $data['width'], $data['height'])) {
+ watchdog('image', 'Image crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Scale and crop an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the scale and crop effect
+ * with the following items:
+ * - "width": An integer representing the desired width in pixels.
+ * - "height": An integer representing the desired height in pixels.
+ * @return
+ * TRUE on success. FALSE on failure to scale and crop image.
+ * @see image_scale_and_crop()
+ */
+function image_scale_and_crop_effect(&$image, $data) {
+ if (!image_scale_and_crop($image, $data['width'], $data['height'])) {
+ watchdog('image', 'Image scale and crop failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Desaturate (grayscale) an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the desaturate effect.
+ * @return
+ * TRUE on success. FALSE on failure to desaturate image.
+ * @see image_desaturate()
+ */
+function image_desaturate_effect(&$image, $data) {
+ if (!image_desaturate($image)) {
+ watchdog('image', 'Image desaturate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image effect callback; Rotate an image resource.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $data
+ * An array of attributes to use when performing the rotate effect containing
+ * the following items:
+ * - "degrees": The number of (clockwise) degrees to rotate the image.
+ * - "random": A boolean indicating that a random rotation angle should be
+ * used for this image. The angle specified in "degrees" is used as a
+ * positive and negative maximum.
+ * - "bgcolor": The background color to use for exposed areas of the image.
+ * Use web-style hex colors (#FFFFFF for white, #000000 for black). Leave
+ * blank for transparency on image types that support it.
+ * @return
+ * TRUE on success. FALSE on failure to rotate image.
+ * @see image_rotate().
+ */
+function image_rotate_effect(&$image, $data) {
+ // Set sane default values.
+ $data += array(
+ 'degrees' => 0,
+ 'bgcolor' => NULL,
+ 'random' => FALSE,
+ );
+
+ // Convert short #FFF syntax to full #FFFFFF syntax.
+ if (strlen($data['bgcolor']) == 4) {
+ $c = $data['bgcolor'];
+ $data['bgcolor'] = $c[0] . $c[1] . $c[1] . $c[2] . $c[2] . $c[3] . $c[3];
+ }
+
+ // Convert #FFFFFF syntax to hexadecimal colors.
+ if ($data['bgcolor'] != '') {
+ $data['bgcolor'] = hexdec(str_replace('#', '0x', $data['bgcolor']));
+ }
+ else {
+ $data['bgcolor'] = NULL;
+ }
+
+ if (!empty($data['random'])) {
+ $degrees = abs((float) $data['degrees']);
+ $data['degrees'] = rand(-1 * $degrees, $degrees);
+ }
+
+ if (!image_rotate($image, $data['degrees'], $data['bgcolor'])) {
+ watchdog('image', 'Image rotate failed using the %toolkit toolkit on %path (%mimetype, %dimensions)', array('%toolkit' => $image->toolkit, '%path' => $image->source, '%mimetype' => $image->info['mime_type'], '%dimensions' => $image->info['width'] . 'x' . $image->info['height']), WATCHDOG_ERROR);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * Image dimensions callback; Rotate.
+ *
+ * @param $dimensions
+ * Dimensions to be modified - an array with components width and height, in
+ * pixels.
+ * @param $data
+ * An array of attributes to use when performing the rotate effect containing
+ * the following items:
+ * - "degrees": The number of (clockwise) degrees to rotate the image.
+ * - "random": A boolean indicating that a random rotation angle should be
+ * used for this image. The angle specified in "degrees" is used as a
+ * positive and negative maximum.
+ */
+function image_rotate_dimensions(array &$dimensions, array $data) {
+ // If the rotate is not random and the angle is a multiple of 90 degrees,
+ // then the new dimensions can be determined.
+ if (!$data['random'] && ((int) ($data['degrees']) == $data['degrees']) && ($data['degrees'] % 90 == 0)) {
+ if ($data['degrees'] % 180 != 0) {
+ $temp = $dimensions['width'];
+ $dimensions['width'] = $dimensions['height'];
+ $dimensions['height'] = $temp;
+ }
+ }
+ else {
+ $dimensions['width'] = $dimensions['height'] = NULL;
+ }
+}
diff --git a/drupal-dev/modules/image/image.field.inc b/drupal-dev/modules/image/image.field.inc
new file mode 100644
index 0000000..6d1867c
--- /dev/null
+++ b/drupal-dev/modules/image/image.field.inc
@@ -0,0 +1,642 @@
+ array(
+ 'label' => t('Image'),
+ 'description' => t('This field stores the ID of an image file as an integer value.'),
+ 'settings' => array(
+ 'uri_scheme' => variable_get('file_default_scheme', 'public'),
+ 'default_image' => 0,
+ ),
+ 'instance_settings' => array(
+ 'file_extensions' => 'png gif jpg jpeg',
+ 'file_directory' => '',
+ 'max_filesize' => '',
+ 'alt_field' => 0,
+ 'title_field' => 0,
+ 'max_resolution' => '',
+ 'min_resolution' => '',
+ 'default_image' => 0,
+ ),
+ 'default_widget' => 'image_image',
+ 'default_formatter' => 'image',
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_settings_form().
+ */
+function image_field_settings_form($field, $instance) {
+ $defaults = field_info_field_settings($field['type']);
+ $settings = array_merge($defaults, $field['settings']);
+
+ $scheme_options = array();
+ foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) {
+ $scheme_options[$scheme] = $stream_wrapper['name'];
+ }
+ $form['uri_scheme'] = array(
+ '#type' => 'radios',
+ '#title' => t('Upload destination'),
+ '#options' => $scheme_options,
+ '#default_value' => $settings['uri_scheme'],
+ '#description' => t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
+ );
+
+ // When the user sets the scheme on the UI, even for the first time, it's
+ // updating a field because fields are created on the "Manage fields"
+ // page. So image_field_update_field() can handle this change.
+ $form['default_image'] = array(
+ '#title' => t('Default image'),
+ '#type' => 'managed_file',
+ '#description' => t('If no image is uploaded, this image will be shown on display.'),
+ '#default_value' => $field['settings']['default_image'],
+ '#upload_location' => $settings['uri_scheme'] . '://default_images/',
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_instance_settings_form().
+ */
+function image_field_instance_settings_form($field, $instance) {
+ $settings = $instance['settings'];
+
+ // Use the file field instance settings form as a basis.
+ $form = file_field_instance_settings_form($field, $instance);
+
+ // Add maximum and minimum resolution settings.
+ $max_resolution = explode('x', $settings['max_resolution']) + array('', '');
+ $form['max_resolution'] = array(
+ '#type' => 'item',
+ '#title' => t('Maximum image resolution'),
+ '#element_validate' => array('_image_field_resolution_validate'),
+ '#weight' => 4.1,
+ '#field_prefix' => '
',
+ '#field_suffix' => '
',
+ '#description' => t('The maximum allowed image size expressed as WIDTHxHEIGHT (e.g. 640x480). Leave blank for no restriction. If a larger image is uploaded, it will be resized to reflect the given width and height. Resizing images on upload will cause the loss of EXIF data in the image.'),
+ );
+ $form['max_resolution']['x'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum width'),
+ '#title_display' => 'invisible',
+ '#default_value' => $max_resolution[0],
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => ' x ',
+ );
+ $form['max_resolution']['y'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum height'),
+ '#title_display' => 'invisible',
+ '#default_value' => $max_resolution[1],
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => ' ' . t('pixels'),
+ );
+
+ $min_resolution = explode('x', $settings['min_resolution']) + array('', '');
+ $form['min_resolution'] = array(
+ '#type' => 'item',
+ '#title' => t('Minimum image resolution'),
+ '#element_validate' => array('_image_field_resolution_validate'),
+ '#weight' => 4.2,
+ '#field_prefix' => '
',
+ '#field_suffix' => '
',
+ '#description' => t('The minimum allowed image size expressed as WIDTHxHEIGHT (e.g. 640x480). Leave blank for no restriction. If a smaller image is uploaded, it will be rejected.'),
+ );
+ $form['min_resolution']['x'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Minimum width'),
+ '#title_display' => 'invisible',
+ '#default_value' => $min_resolution[0],
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => ' x ',
+ );
+ $form['min_resolution']['y'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Minimum height'),
+ '#title_display' => 'invisible',
+ '#default_value' => $min_resolution[1],
+ '#size' => 5,
+ '#maxlength' => 5,
+ '#field_suffix' => ' ' . t('pixels'),
+ );
+
+ // Remove the description option.
+ unset($form['description_field']);
+
+ // Add title and alt configuration options.
+ $form['alt_field'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable Alt field'),
+ '#default_value' => $settings['alt_field'],
+ '#description' => t('The alt attribute may be used by search engines, screen readers, and when the image cannot be loaded.'),
+ '#weight' => 10,
+ );
+ $form['title_field'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable Title field'),
+ '#default_value' => $settings['title_field'],
+ '#description' => t('The title attribute is used as a tooltip when the mouse hovers over the image.'),
+ '#weight' => 11,
+ );
+
+ // Add the default image to the instance.
+ $form['default_image'] = array(
+ '#title' => t('Default image'),
+ '#type' => 'managed_file',
+ '#description' => t("If no image is uploaded, this image will be shown on display and will override the field's default image."),
+ '#default_value' => $settings['default_image'],
+ '#upload_location' => $field['settings']['uri_scheme'] . '://default_images/',
+ );
+
+ return $form;
+}
+
+/**
+ * Element validate function for resolution fields.
+ */
+function _image_field_resolution_validate($element, &$form_state) {
+ if (!empty($element['x']['#value']) || !empty($element['y']['#value'])) {
+ foreach (array('x', 'y') as $dimension) {
+ $value = $element[$dimension]['#value'];
+ if (!is_numeric($value)) {
+ form_error($element[$dimension], t('Height and width values must be numeric.'));
+ return;
+ }
+ if (intval($value) == 0) {
+ form_error($element[$dimension], t('Both a height and width value must be specified in the !name field.', array('!name' => $element['#title'])));
+ return;
+ }
+ }
+ form_set_value($element, intval($element['x']['#value']) . 'x' . intval($element['y']['#value']), $form_state);
+ }
+ else {
+ form_set_value($element, '', $form_state);
+ }
+}
+
+/**
+ * Implements hook_field_load().
+ */
+function image_field_load($entity_type, $entities, $field, $instances, $langcode, &$items, $age) {
+ file_field_load($entity_type, $entities, $field, $instances, $langcode, $items, $age);
+}
+
+/**
+ * Implements hook_field_prepare_view().
+ */
+function image_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) {
+ // If there are no files specified at all, use the default.
+ foreach ($entities as $id => $entity) {
+ if (empty($items[$id])) {
+ $fid = 0;
+ // Use the default for the instance if one is available.
+ if (!empty($instances[$id]['settings']['default_image'])) {
+ $fid = $instances[$id]['settings']['default_image'];
+ }
+ // Otherwise, use the default for the field.
+ elseif (!empty($field['settings']['default_image'])) {
+ $fid = $field['settings']['default_image'];
+ }
+
+ // Add the default image if one is found.
+ if ($fid && ($file = file_load($fid))) {
+ $items[$id][0] = (array) $file + array(
+ 'is_default' => TRUE,
+ 'alt' => '',
+ 'title' => '',
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_presave().
+ */
+function image_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ file_field_presave($entity_type, $entity, $field, $instance, $langcode, $items);
+
+ // Determine the dimensions if necessary.
+ foreach ($items as &$item) {
+ if (!isset($item['width']) || !isset($item['height'])) {
+ $info = image_get_info(file_load($item['fid'])->uri);
+
+ if (is_array($info)) {
+ $item['width'] = $info['width'];
+ $item['height'] = $info['height'];
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_field_insert().
+ */
+function image_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ file_field_insert($entity_type, $entity, $field, $instance, $langcode, $items);
+}
+
+/**
+ * Implements hook_field_update().
+ */
+function image_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ file_field_update($entity_type, $entity, $field, $instance, $langcode, $items);
+}
+
+/**
+ * Implements hook_field_delete().
+ */
+function image_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ file_field_delete($entity_type, $entity, $field, $instance, $langcode, $items);
+}
+
+/**
+ * Implements hook_field_delete_revision().
+ */
+function image_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) {
+ file_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, $items);
+}
+
+/**
+ * Implements hook_field_is_empty().
+ */
+function image_field_is_empty($item, $field) {
+ return file_field_is_empty($item, $field);
+}
+
+/**
+ * Implements hook_field_widget_info().
+ */
+function image_field_widget_info() {
+ return array(
+ 'image_image' => array(
+ 'label' => t('Image'),
+ 'field types' => array('image'),
+ 'settings' => array(
+ 'progress_indicator' => 'throbber',
+ 'preview_image_style' => 'thumbnail',
+ ),
+ 'behaviors' => array(
+ 'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+ 'default value' => FIELD_BEHAVIOR_NONE,
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_field_widget_settings_form().
+ */
+function image_field_widget_settings_form($field, $instance) {
+ $widget = $instance['widget'];
+ $settings = $widget['settings'];
+
+ // Use the file widget settings form.
+ $form = file_field_widget_settings_form($field, $instance);
+
+ $form['preview_image_style'] = array(
+ '#title' => t('Preview image style'),
+ '#type' => 'select',
+ '#options' => image_style_options(FALSE, PASS_THROUGH),
+ '#empty_option' => '<' . t('no preview') . '>',
+ '#default_value' => $settings['preview_image_style'],
+ '#description' => t('The preview image will be shown while editing the content.'),
+ '#weight' => 15,
+ );
+
+ return $form;
+}
+
+/**
+ * Implements hook_field_widget_form().
+ */
+function image_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+
+ // Add display_field setting to field because file_field_widget_form() assumes it is set.
+ $field['settings']['display_field'] = 0;
+
+ $elements = file_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
+ $settings = $instance['settings'];
+
+ foreach (element_children($elements) as $delta) {
+ // Add upload resolution validation.
+ if ($settings['max_resolution'] || $settings['min_resolution']) {
+ $elements[$delta]['#upload_validators']['file_validate_image_resolution'] = array($settings['max_resolution'], $settings['min_resolution']);
+ }
+
+ // If not using custom extension validation, ensure this is an image.
+ $supported_extensions = array('png', 'gif', 'jpg', 'jpeg');
+ $extensions = isset($elements[$delta]['#upload_validators']['file_validate_extensions'][0]) ? $elements[$delta]['#upload_validators']['file_validate_extensions'][0] : implode(' ', $supported_extensions);
+ $extensions = array_intersect(explode(' ', $extensions), $supported_extensions);
+ $elements[$delta]['#upload_validators']['file_validate_extensions'][0] = implode(' ', $extensions);
+
+ // Add all extra functionality provided by the image widget.
+ $elements[$delta]['#process'][] = 'image_field_widget_process';
+ }
+
+ if ($field['cardinality'] == 1) {
+ // If there's only one field, return it as delta 0.
+ if (empty($elements[0]['#default_value']['fid'])) {
+ $elements[0]['#description'] = theme('file_upload_help', array('description' => field_filter_xss($instance['description']), 'upload_validators' => $elements[0]['#upload_validators']));
+ }
+ }
+ else {
+ $elements['#file_upload_description'] = theme('file_upload_help', array('upload_validators' => $elements[0]['#upload_validators']));
+ }
+ return $elements;
+}
+
+/**
+ * An element #process callback for the image_image field type.
+ *
+ * Expands the image_image type to include the alt and title fields.
+ */
+function image_field_widget_process($element, &$form_state, $form) {
+ $item = $element['#value'];
+ $item['fid'] = $element['fid']['#value'];
+
+ $instance = field_widget_instance($element, $form_state);
+
+ $settings = $instance['settings'];
+ $widget_settings = $instance['widget']['settings'];
+
+ $element['#theme'] = 'image_widget';
+ $element['#attached']['css'][] = drupal_get_path('module', 'image') . '/image.css';
+
+ // Add the image preview.
+ if ($element['#file'] && $widget_settings['preview_image_style']) {
+ $variables = array(
+ 'style_name' => $widget_settings['preview_image_style'],
+ 'path' => $element['#file']->uri,
+ );
+
+ // Determine image dimensions.
+ if (isset($element['#value']['width']) && isset($element['#value']['height'])) {
+ $variables['width'] = $element['#value']['width'];
+ $variables['height'] = $element['#value']['height'];
+ }
+ else {
+ $info = image_get_info($element['#file']->uri);
+
+ if (is_array($info)) {
+ $variables['width'] = $info['width'];
+ $variables['height'] = $info['height'];
+ }
+ else {
+ $variables['width'] = $variables['height'] = NULL;
+ }
+ }
+
+ $element['preview'] = array(
+ '#type' => 'markup',
+ '#markup' => theme('image_style', $variables),
+ );
+
+ // Store the dimensions in the form so the file doesn't have to be accessed
+ // again. This is important for remote files.
+ $element['width'] = array(
+ '#type' => 'hidden',
+ '#value' => $variables['width'],
+ );
+ $element['height'] = array(
+ '#type' => 'hidden',
+ '#value' => $variables['height'],
+ );
+ }
+
+ // Add the additional alt and title fields.
+ $element['alt'] = array(
+ '#title' => t('Alternate text'),
+ '#type' => 'textfield',
+ '#default_value' => isset($item['alt']) ? $item['alt'] : '',
+ '#description' => t('This text will be used by screen readers, search engines, or when the image cannot be loaded.'),
+ // @see http://www.gawds.org/show.php?contentid=28
+ '#maxlength' => 512,
+ '#weight' => -2,
+ '#access' => (bool) $item['fid'] && $settings['alt_field'],
+ );
+ $element['title'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Title'),
+ '#default_value' => isset($item['title']) ? $item['title'] : '',
+ '#description' => t('The title is used as a tool tip when the user hovers the mouse over the image.'),
+ '#maxlength' => 1024,
+ '#weight' => -1,
+ '#access' => (bool) $item['fid'] && $settings['title_field'],
+ );
+
+ return $element;
+}
+
+/**
+ * Returns HTML for an image field widget.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - element: A render element representing the image field widget.
+ *
+ * @ingroup themeable
+ */
+function theme_image_widget($variables) {
+ $element = $variables['element'];
+ $output = '';
+ $output .= '
';
+
+ if (isset($element['preview'])) {
+ $output .= '
';
+
+ return $output;
+}
+
+/**
+ * Implements hook_field_formatter_info().
+ */
+function image_field_formatter_info() {
+ $formatters = array(
+ 'image' => array(
+ 'label' => t('Image'),
+ 'field types' => array('image'),
+ 'settings' => array('image_style' => '', 'image_link' => ''),
+ ),
+ );
+
+ return $formatters;
+}
+
+/**
+ * Implements hook_field_formatter_settings_form().
+ */
+function image_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $image_styles = image_style_options(FALSE, PASS_THROUGH);
+ $element['image_style'] = array(
+ '#title' => t('Image style'),
+ '#type' => 'select',
+ '#default_value' => $settings['image_style'],
+ '#empty_option' => t('None (original image)'),
+ '#options' => $image_styles,
+ );
+
+ $link_types = array(
+ 'content' => t('Content'),
+ 'file' => t('File'),
+ );
+ $element['image_link'] = array(
+ '#title' => t('Link image to'),
+ '#type' => 'select',
+ '#default_value' => $settings['image_link'],
+ '#empty_option' => t('Nothing'),
+ '#options' => $link_types,
+ );
+
+ return $element;
+}
+
+/**
+ * Implements hook_field_formatter_settings_summary().
+ */
+function image_field_formatter_settings_summary($field, $instance, $view_mode) {
+ $display = $instance['display'][$view_mode];
+ $settings = $display['settings'];
+
+ $summary = array();
+
+ $image_styles = image_style_options(FALSE, PASS_THROUGH);
+ // Unset possible 'No defined styles' option.
+ unset($image_styles['']);
+ // Styles could be lost because of enabled/disabled modules that defines
+ // their styles in code.
+ if (isset($image_styles[$settings['image_style']])) {
+ $summary[] = t('Image style: @style', array('@style' => $image_styles[$settings['image_style']]));
+ }
+ else {
+ $summary[] = t('Original image');
+ }
+
+ $link_types = array(
+ 'content' => t('Linked to content'),
+ 'file' => t('Linked to file'),
+ );
+ // Display this setting only if image is linked.
+ if (isset($link_types[$settings['image_link']])) {
+ $summary[] = $link_types[$settings['image_link']];
+ }
+
+ return implode(' ', $summary);
+}
+
+/**
+ * Implements hook_field_formatter_view().
+ */
+function image_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
+ $element = array();
+
+ // Check if the formatter involves a link.
+ if ($display['settings']['image_link'] == 'content') {
+ $uri = entity_uri($entity_type, $entity);
+ }
+ elseif ($display['settings']['image_link'] == 'file') {
+ $link_file = TRUE;
+ }
+
+ foreach ($items as $delta => $item) {
+ if (isset($link_file)) {
+ $uri = array(
+ 'path' => file_create_url($item['uri']),
+ 'options' => array(),
+ );
+ }
+ $element[$delta] = array(
+ '#theme' => 'image_formatter',
+ '#item' => $item,
+ '#image_style' => $display['settings']['image_style'],
+ '#path' => isset($uri) ? $uri : '',
+ );
+ }
+
+ return $element;
+}
+
+/**
+ * Returns HTML for an image field formatter.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - item: Associative array of image data, which may include "uri", "alt",
+ * "width", "height", "title" and "attributes".
+ * - image_style: An optional image style.
+ * - path: An array containing the link 'path' and link 'options'.
+ *
+ * @ingroup themeable
+ */
+function theme_image_formatter($variables) {
+ $item = $variables['item'];
+ $image = array(
+ 'path' => $item['uri'],
+ );
+
+ if (array_key_exists('alt', $item)) {
+ $image['alt'] = $item['alt'];
+ }
+
+ if (isset($item['attributes'])) {
+ $image['attributes'] = $item['attributes'];
+ }
+
+ if (isset($item['width']) && isset($item['height'])) {
+ $image['width'] = $item['width'];
+ $image['height'] = $item['height'];
+ }
+
+ // Do not output an empty 'title' attribute.
+ if (isset($item['title']) && drupal_strlen($item['title']) > 0) {
+ $image['title'] = $item['title'];
+ }
+
+ if ($variables['image_style']) {
+ $image['style_name'] = $variables['image_style'];
+ $output = theme('image_style', $image);
+ }
+ else {
+ $output = theme('image', $image);
+ }
+
+ // The link path and link options are both optional, but for the options to be
+ // processed, the link path must at least be an empty string.
+ if (isset($variables['path']['path'])) {
+ $path = $variables['path']['path'];
+ $options = isset($variables['path']['options']) ? $variables['path']['options'] : array();
+ // When displaying an image inside a link, the html option must be TRUE.
+ $options['html'] = TRUE;
+ $output = l($output, $path, $options);
+ }
+
+ return $output;
+}
diff --git a/drupal-dev/modules/image/image.info b/drupal-dev/modules/image/image.info
new file mode 100644
index 0000000..a601da7
--- /dev/null
+++ b/drupal-dev/modules/image/image.info
@@ -0,0 +1,14 @@
+name = Image
+description = Provides image manipulation tools.
+package = Core
+version = VERSION
+core = 7.x
+dependencies[] = file
+files[] = image.test
+configure = admin/config/media/image-styles
+
+; Information added by Drupal.org packaging script on 2014-01-15
+version = "7.26"
+project = "drupal"
+datestamp = "1389815930"
+
diff --git a/drupal-dev/modules/image/image.install b/drupal-dev/modules/image/image.install
new file mode 100644
index 0000000..45bcbbb
--- /dev/null
+++ b/drupal-dev/modules/image/image.install
@@ -0,0 +1,522 @@
+ 'Stores configuration options for image styles.',
+ 'fields' => array(
+ 'isid' => array(
+ 'description' => 'The primary identifier for an image style.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'The style machine name.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'label' => array(
+ 'description' => 'The style administrative name.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ ),
+ ),
+ 'primary key' => array('isid'),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ );
+
+ $schema['image_effects'] = array(
+ 'description' => 'Stores configuration options for image effects.',
+ 'fields' => array(
+ 'ieid' => array(
+ 'description' => 'The primary identifier for an image effect.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'isid' => array(
+ 'description' => 'The {image_styles}.isid for an image style.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'weight' => array(
+ 'description' => 'The weight of the effect in the style.',
+ 'type' => 'int',
+ 'unsigned' => FALSE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'name' => array(
+ 'description' => 'The unique name of the effect to be executed.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'The configuration data for the effect.',
+ 'type' => 'blob',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('ieid'),
+ 'indexes' => array(
+ 'isid' => array('isid'),
+ 'weight' => array('weight'),
+ ),
+ 'foreign keys' => array(
+ 'image_style' => array(
+ 'table' => 'image_styles',
+ 'columns' => array('isid' => 'isid'),
+ ),
+ ),
+ );
+
+ return $schema;
+}
+
+/**
+ * Implements hook_field_schema().
+ */
+function image_field_schema($field) {
+ return array(
+ 'columns' => array(
+ 'fid' => array(
+ 'description' => 'The {file_managed}.fid being referenced in this field.',
+ 'type' => 'int',
+ 'not null' => FALSE,
+ 'unsigned' => TRUE,
+ ),
+ 'alt' => array(
+ 'description' => "Alternative image text, for the image's 'alt' attribute.",
+ 'type' => 'varchar',
+ 'length' => 512,
+ 'not null' => FALSE,
+ ),
+ 'title' => array(
+ 'description' => "Image title text, for the image's 'title' attribute.",
+ 'type' => 'varchar',
+ 'length' => 1024,
+ 'not null' => FALSE,
+ ),
+ 'width' => array(
+ 'description' => 'The width of the image in pixels.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ ),
+ 'height' => array(
+ 'description' => 'The height of the image in pixels.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ ),
+ ),
+ 'indexes' => array(
+ 'fid' => array('fid'),
+ ),
+ 'foreign keys' => array(
+ 'fid' => array(
+ 'table' => 'file_managed',
+ 'columns' => array('fid' => 'fid'),
+ ),
+ ),
+ );
+}
+
+/**
+ * Implements hook_update_dependencies().
+ */
+function image_update_dependencies() {
+ $dependencies['image'][7002] = array(
+ // Image update 7002 uses field API functions, so must run after
+ // Field API has been enabled.
+ 'system' => 7020,
+ );
+ return $dependencies;
+}
+
+/**
+ * Install the schema for users upgrading from the contributed module.
+ */
+function image_update_7000() {
+ if (!db_table_exists('image_styles')) {
+ $schema = array();
+
+ $schema['cache_image'] = system_schema_cache_7054();
+ $schema['cache_image']['description'] = 'Cache table used to store information about image manipulations that are in-progress.';
+
+ $schema['image_styles'] = array(
+ 'description' => 'Stores configuration options for image styles.',
+ 'fields' => array(
+ 'isid' => array(
+ 'description' => 'The primary identifier for an image style.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'name' => array(
+ 'description' => 'The style name.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ ),
+ 'primary key' => array('isid'),
+ 'unique keys' => array(
+ 'name' => array('name'),
+ ),
+ );
+
+ $schema['image_effects'] = array(
+ 'description' => 'Stores configuration options for image effects.',
+ 'fields' => array(
+ 'ieid' => array(
+ 'description' => 'The primary identifier for an image effect.',
+ 'type' => 'serial',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ ),
+ 'isid' => array(
+ 'description' => 'The {image_styles}.isid for an image style.',
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'weight' => array(
+ 'description' => 'The weight of the effect in the style.',
+ 'type' => 'int',
+ 'unsigned' => FALSE,
+ 'not null' => TRUE,
+ 'default' => 0,
+ ),
+ 'name' => array(
+ 'description' => 'The unique name of the effect to be executed.',
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ ),
+ 'data' => array(
+ 'description' => 'The configuration data for the effect.',
+ 'type' => 'blob',
+ 'not null' => TRUE,
+ 'size' => 'big',
+ 'serialize' => TRUE,
+ ),
+ ),
+ 'primary key' => array('ieid'),
+ 'indexes' => array(
+ 'isid' => array('isid'),
+ 'weight' => array('weight'),
+ ),
+ 'foreign keys' => array(
+ 'image_style' => array(
+ 'table' => 'image_styles',
+ 'columns' => array('isid' => 'isid'),
+ ),
+ ),
+ );
+
+ db_create_table('cache_image', $schema['cache_image']);
+ db_create_table('image_styles', $schema['image_styles']);
+ db_create_table('image_effects', $schema['image_effects']);
+ }
+}
+
+/**
+ * @addtogroup updates-7.x-extra
+ * @{
+ */
+
+/**
+ * Rename possibly misnamed {image_effect} table to {image_effects}.
+ */
+function image_update_7001() {
+ // Due to a bug in earlier versions of image_update_7000() it is possible
+ // to end up with an {image_effect} table where there should be an
+ // {image_effects} table.
+ if (!db_table_exists('image_effects') && db_table_exists('image_effect')) {
+ db_rename_table('image_effect', 'image_effects');
+ }
+}
+
+/**
+ * Add width and height columns to a specific table.
+ *
+ * @param $table
+ * The name of the database table to be updated.
+ * @param $columns
+ * Keyed array of columns this table is supposed to have.
+ */
+function _image_update_7002_add_columns($table, $field_name) {
+ $spec = array(
+ 'type' => 'int',
+ 'unsigned' => TRUE,
+ );
+
+ $spec['description'] = 'The width of the image in pixels.';
+ db_add_field($table, $field_name . '_width', $spec);
+
+ $spec['description'] = 'The height of the image in pixels.';
+ db_add_field($table, $field_name . '_height', $spec);
+}
+
+/**
+ * Populate image dimensions in a specific table.
+ *
+ * @param $table
+ * The name of the database table to be updated.
+ * @param $columns
+ * Keyed array of columns this table is supposed to have.
+ * @param $last_fid
+ * The fid of the last image to have been processed.
+ *
+ * @return
+ * The number of images that were processed.
+ */
+function _image_update_7002_populate_dimensions($table, $field_name, &$last_fid) {
+ // Define how many images to process per pass.
+ $images_per_pass = 100;
+
+ // Query the database for fid / URI pairs.
+ $query = db_select($table, NULL, array('fetch' => PDO::FETCH_ASSOC));
+ $query->join('file_managed', NULL, $table . '.' . $field_name . '_fid = file_managed.fid');
+
+ if ($last_fid) {
+ $query->condition('file_managed.fid', $last_fid, '>');
+ }
+
+ $result = $query->fields('file_managed', array('fid', 'uri'))
+ ->orderBy('file_managed.fid')
+ ->range(0, $images_per_pass)
+ ->execute();
+
+ $count = 0;
+ foreach ($result as $file) {
+ $count++;
+ $info = image_get_info($file['uri']);
+
+ if (is_array($info)) {
+ db_update($table)
+ ->fields(array(
+ $field_name . '_width' => $info['width'],
+ $field_name . '_height' => $info['height'],
+ ))
+ ->condition($field_name . '_fid', $file['fid'])
+ ->execute();
+ }
+ }
+
+ // If less than the requested number of rows were returned then this table
+ // has been fully processed.
+ $last_fid = ($count < $images_per_pass) ? NULL : $file['fid'];
+ return $count;
+}
+
+/**
+ * Add width and height columns to image field schema and populate.
+ */
+function image_update_7002(array &$sandbox) {
+ if (empty($sandbox)) {
+ // Setup the sandbox.
+ $sandbox = array(
+ 'tables' => array(),
+ 'total' => 0,
+ 'processed' => 0,
+ 'last_fid' => NULL,
+ );
+
+ $fields = _update_7000_field_read_fields(array(
+ 'module' => 'image',
+ 'storage_type' => 'field_sql_storage',
+ 'deleted' => 0,
+ ));
+
+ foreach ($fields as $field) {
+ $tables = array(
+ _field_sql_storage_tablename($field),
+ _field_sql_storage_revision_tablename($field),
+ );
+ foreach ($tables as $table) {
+ // Add the width and height columns to the table.
+ _image_update_7002_add_columns($table, $field['field_name']);
+
+ // How many rows need dimensions populated?
+ $count = db_select($table)->countQuery()->execute()->fetchField();
+
+ if (!$count) {
+ continue;
+ }
+
+ $sandbox['total'] += $count;
+ $sandbox['tables'][$table] = $field['field_name'];
+ }
+ }
+
+ // If no tables need rows populated with dimensions then we are done.
+ if (empty($sandbox['tables'])) {
+ $sandbox = array();
+ return;
+ }
+ }
+
+ // Process the table at the top of the list.
+ $keys = array_keys($sandbox['tables']);
+ $table = reset($keys);
+ $sandbox['processed'] += _image_update_7002_populate_dimensions($table, $sandbox['tables'][$table], $sandbox['last_fid']);
+
+ // Has the table been fully processed?
+ if (!$sandbox['last_fid']) {
+ unset($sandbox['tables'][$table]);
+ }
+
+ $sandbox['#finished'] = count($sandbox['tables']) ? ($sandbox['processed'] / $sandbox['total']) : 1;
+}
+
+/**
+ * Remove the variables that set alt and title length since they were not
+ * used for database column size and could cause PDO exceptions.
+ */
+function image_update_7003() {
+ variable_del('image_alt_length');
+ variable_del('image_title_length');
+}
+
+/**
+ * Use a large setting (512 and 1024 characters) for the length of the image alt
+ * and title fields.
+ */
+function image_update_7004() {
+ $alt_spec = array(
+ 'type' => 'varchar',
+ 'length' => 512,
+ 'not null' => FALSE,
+ );
+
+ $title_spec = array(
+ 'type' => 'varchar',
+ 'length' => 1024,
+ 'not null' => FALSE,
+ );
+
+ $fields = _update_7000_field_read_fields(array(
+ 'module' => 'image',
+ 'storage_type' => 'field_sql_storage',
+ ));
+
+ foreach ($fields as $field_name => $field) {
+ $tables = array(
+ _field_sql_storage_tablename($field),
+ _field_sql_storage_revision_tablename($field),
+ );
+ $alt_column = $field['field_name'] . '_alt';
+ $title_column = $field['field_name'] . '_title';
+ foreach ($tables as $table) {
+ db_change_field($table, $alt_column, $alt_column, $alt_spec);
+ db_change_field($table, $title_column, $title_column, $title_spec);
+ }
+ }
+}
+
+/**
+ * Add a column to the 'image_style' table to store administrative labels.
+ */
+function image_update_7005() {
+ $field = array(
+ 'type' => 'varchar',
+ 'length' => 255,
+ 'not null' => TRUE,
+ 'default' => '',
+ 'description' => 'The style administrative name.',
+ );
+ db_add_field('image_styles', 'label', $field);
+
+ // Do a direct query here, rather than calling image_styles(),
+ // in case Image module is disabled.
+ $styles = db_query('SELECT name FROM {image_styles}')->fetchCol();
+ foreach ($styles as $style) {
+ db_update('image_styles')
+ ->fields(array('label' => $style))
+ ->condition('name', $style)
+ ->execute();
+ }
+}
+
+/**
+ * @} End of "addtogroup updates-7.x-extra".
+ */
+
+/**
+ * Implements hook_requirements() to check the PHP GD Library.
+ *
+ * @param $phase
+ */
+function image_requirements($phase) {
+ $requirements = array();
+
+ if ($phase == 'runtime') {
+ // Check for the PHP GD library.
+ if (function_exists('imagegd2')) {
+ $info = gd_info();
+ $requirements['image_gd'] = array(
+ 'value' => $info['GD Version'],
+ );
+
+ // Check for filter and rotate support.
+ if (function_exists('imagefilter') && function_exists('imagerotate')) {
+ $requirements['image_gd']['severity'] = REQUIREMENT_OK;
+ }
+ else {
+ $requirements['image_gd']['severity'] = REQUIREMENT_WARNING;
+ $requirements['image_gd']['description'] = t('The GD Library for PHP is enabled, but was compiled without support for functions used by the rotate and desaturate effects. It was probably compiled using the official GD libraries from http://www.libgd.org instead of the GD library bundled with PHP. You should recompile PHP --with-gd using the bundled GD library. See the PHP manual.');
+ }
+ }
+ else {
+ $requirements['image_gd'] = array(
+ 'value' => t('Not installed'),
+ 'severity' => REQUIREMENT_ERROR,
+ 'description' => t('The GD library for PHP is missing or outdated. Check the PHP image documentation for information on how to correct this.', array('@url' => 'http://www.php.net/manual/book.image.php')),
+ );
+ }
+ $requirements['image_gd']['title'] = t('GD library rotate and desaturate effects');
+ }
+
+ return $requirements;
+}
diff --git a/drupal-dev/modules/image/image.module b/drupal-dev/modules/image/image.module
new file mode 100644
index 0000000..c6a23f2
--- /dev/null
+++ b/drupal-dev/modules/image/image.module
@@ -0,0 +1,1393 @@
+' . t('About') . '';
+ $output .= '
' . t('The Image module allows you to manipulate images on your website. It exposes a setting for using the Image toolkit, allows you to configure Image styles that can be used for resizing or adjusting images on display, and provides an Image field for attaching images to content. For more information, see the online handbook entry for Image module.', array('@image' => 'http://drupal.org/documentation/modules/image')) . '
';
+ $output .= '
' . t('Uses') . '
';
+ $output .= '
';
+ $output .= '
' . t('Manipulating images') . '
';
+ $output .= '
' . t('With the Image module you can scale, crop, resize, rotate and desaturate images without affecting the original image using image styles. When you change an image style, the module automatically refreshes all created images. Every image style must have a name, which will be used in the URL of the generated images. There are two common approaches to naming image styles (which you use will depend on how the image style is being applied):',array('@image' => url('admin/config/media/image-styles')));
+ $output .= '
' . t('Based on where it will be used: eg. profile-picture') . '
';
+ $output .= '
' . t('Describing its appearance: eg. square-85x85') . '
';
+ $output .= t('After you create an image style, you can add effects: crop, scale, resize, rotate, and desaturate (other contributed modules provide additional effects). For example, by combining effects as crop, scale, and desaturate, you can create square, grayscale thumbnails.') . '
';
+ $output .= '
' . t('Attaching images to content as fields') . '
';
+ $output .= '
' . t("Image module also allows you to attach images to content as fields. To add an image field to a content type, go to the content type's manage fields page, and add a new field of type Image. Attaching images to content this way allows image styles to be applied and maintained, and also allows you more flexibility when theming.", array('@content-type' => url('admin/structure/types'))) . '
';
+ $output .= '
';
+ return $output;
+ case 'admin/config/media/image-styles':
+ return '
' . t('Image styles commonly provide thumbnail sizes by scaling and cropping images, but can also add various effects before an image is displayed. When an image is displayed with a style, a new file is created and the original image is left unchanged.') . '
') : NULL;
+ }
+}
+
+/**
+ * Implements hook_menu().
+ */
+function image_menu() {
+ $items = array();
+
+ // Generate image derivatives of publicly available files.
+ // If clean URLs are disabled, image derivatives will always be served
+ // through the menu system.
+ // If clean URLs are enabled and the image derivative already exists,
+ // PHP will be bypassed.
+ $directory_path = file_stream_wrapper_get_instance_by_scheme('public')->getDirectoryPath();
+ $items[$directory_path . '/styles/%image_style'] = array(
+ 'title' => 'Generate image style',
+ 'page callback' => 'image_style_deliver',
+ 'page arguments' => array(count(explode('/', $directory_path)) + 1),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ // Generate and deliver image derivatives of private files.
+ // These image derivatives are always delivered through the menu system.
+ $items['system/files/styles/%image_style'] = array(
+ 'title' => 'Generate image style',
+ 'page callback' => 'image_style_deliver',
+ 'page arguments' => array(3),
+ 'access callback' => TRUE,
+ 'type' => MENU_CALLBACK,
+ );
+ $items['admin/config/media/image-styles'] = array(
+ 'title' => 'Image styles',
+ 'description' => 'Configure styles that can be used for resizing or adjusting images on display.',
+ 'page callback' => 'image_style_list',
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/list'] = array(
+ 'title' => 'List',
+ 'description' => 'List the current image styles on the site.',
+ 'page callback' => 'image_style_list',
+ 'access arguments' => array('administer image styles'),
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => 1,
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/add'] = array(
+ 'title' => 'Add style',
+ 'description' => 'Add a new image style.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_style_add_form'),
+ 'access arguments' => array('administer image styles'),
+ 'type' => MENU_LOCAL_ACTION,
+ 'weight' => 2,
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/edit/%image_style'] = array(
+ 'title' => 'Edit style',
+ 'description' => 'Configure an image style.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_style_form', 5),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/delete/%image_style'] = array(
+ 'title' => 'Delete style',
+ 'description' => 'Delete an image style.',
+ 'load arguments' => array(NULL, (string) IMAGE_STORAGE_NORMAL),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_style_delete_form', 5),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/revert/%image_style'] = array(
+ 'title' => 'Revert style',
+ 'description' => 'Revert an image style.',
+ 'load arguments' => array(NULL, (string) IMAGE_STORAGE_OVERRIDE),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_style_revert_form', 5),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect'] = array(
+ 'title' => 'Edit image effect',
+ 'description' => 'Edit an existing effect within a style.',
+ 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_effect_form', 5, 7),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/edit/%image_style/effects/%image_effect/delete'] = array(
+ 'title' => 'Delete image effect',
+ 'description' => 'Delete an existing effect from a style.',
+ 'load arguments' => array(5, (string) IMAGE_STORAGE_EDITABLE),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_effect_delete_form', 5, 7),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+ $items['admin/config/media/image-styles/edit/%image_style/add/%image_effect_definition'] = array(
+ 'title' => 'Add image effect',
+ 'description' => 'Add a new effect to a style.',
+ 'load arguments' => array(5),
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('image_effect_form', 5, 7),
+ 'access arguments' => array('administer image styles'),
+ 'file' => 'image.admin.inc',
+ );
+
+ return $items;
+}
+
+/**
+ * Implements hook_theme().
+ */
+function image_theme() {
+ return array(
+ // Theme functions in image.module.
+ 'image_style' => array(
+ 'variables' => array(
+ 'style_name' => NULL,
+ 'path' => NULL,
+ 'width' => NULL,
+ 'height' => NULL,
+ 'alt' => '',
+ 'title' => NULL,
+ 'attributes' => array(),
+ ),
+ ),
+
+ // Theme functions in image.admin.inc.
+ 'image_style_list' => array(
+ 'variables' => array('styles' => NULL),
+ ),
+ 'image_style_effects' => array(
+ 'render element' => 'form',
+ ),
+ 'image_style_preview' => array(
+ 'variables' => array('style' => NULL),
+ ),
+ 'image_anchor' => array(
+ 'render element' => 'element',
+ ),
+ 'image_resize_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+ 'image_scale_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+ 'image_crop_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+ 'image_rotate_summary' => array(
+ 'variables' => array('data' => NULL),
+ ),
+
+ // Theme functions in image.field.inc.
+ 'image_widget' => array(
+ 'render element' => 'element',
+ ),
+ 'image_formatter' => array(
+ 'variables' => array('item' => NULL, 'path' => NULL, 'image_style' => NULL),
+ ),
+ );
+}
+
+/**
+ * Implements hook_permission().
+ */
+function image_permission() {
+ return array(
+ 'administer image styles' => array(
+ 'title' => t('Administer image styles'),
+ 'description' => t('Create and modify styles for generating image modifications such as thumbnails.'),
+ ),
+ );
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function image_form_system_file_system_settings_alter(&$form, &$form_state) {
+ $form['#submit'][] = 'image_system_file_system_settings_submit';
+}
+
+/**
+ * Form submission handler for system_file_system_settings().
+ *
+ * Adds a menu rebuild after the public file path has been changed, so that the
+ * menu router item depending on that file path will be regenerated.
+ */
+function image_system_file_system_settings_submit($form, &$form_state) {
+ if ($form['file_public_path']['#default_value'] !== $form_state['values']['file_public_path']) {
+ variable_set('menu_rebuild_needed', TRUE);
+ }
+}
+
+/**
+ * Implements hook_flush_caches().
+ */
+function image_flush_caches() {
+ return array('cache_image');
+}
+
+/**
+ * Implements hook_file_download().
+ *
+ * Control the access to files underneath the styles directory.
+ */
+function image_file_download($uri) {
+ $path = file_uri_target($uri);
+
+ // Private file access for image style derivatives.
+ if (strpos($path, 'styles/') === 0) {
+ $args = explode('/', $path);
+ // Discard the first part of the path (styles).
+ array_shift($args);
+ // Get the style name from the second part.
+ $style_name = array_shift($args);
+ // Remove the scheme from the path.
+ array_shift($args);
+
+ // Then the remaining parts are the path to the image.
+ $original_uri = file_uri_scheme($uri) . '://' . implode('/', $args);
+
+ // Check that the file exists and is an image.
+ if ($info = image_get_info($uri)) {
+ // Check the permissions of the original to grant access to this image.
+ $headers = module_invoke_all('file_download', $original_uri);
+ // Confirm there's at least one module granting access and none denying access.
+ if (!empty($headers) && !in_array(-1, $headers)) {
+ return array(
+ // Send headers describing the image's size, and MIME-type...
+ 'Content-Type' => $info['mime_type'],
+ 'Content-Length' => $info['file_size'],
+ // By not explicitly setting them here, this uses normal Drupal
+ // Expires, Cache-Control and ETag headers to prevent proxy or
+ // browser caching of private images.
+ );
+ }
+ }
+ return -1;
+ }
+
+ // Private file access for the original files. Note that we only check access
+ // for non-temporary images, since file.module will grant access for all
+ // temporary files.
+ $files = file_load_multiple(array(), array('uri' => $uri));
+ if (count($files)) {
+ $file = reset($files);
+ if ($file->status) {
+ return file_file_download($uri, 'image');
+ }
+ }
+}
+
+/**
+ * Implements hook_file_move().
+ */
+function image_file_move($file, $source) {
+ // Delete any image derivatives at the original image path.
+ image_path_flush($source->uri);
+}
+
+/**
+ * Implements hook_file_delete().
+ */
+function image_file_delete($file) {
+ // Delete any image derivatives of this image.
+ image_path_flush($file->uri);
+}
+
+/**
+ * Implements hook_image_default_styles().
+ */
+function image_image_default_styles() {
+ $styles = array();
+
+ $styles['thumbnail'] = array(
+ 'label' => 'Thumbnail (100x100)',
+ 'effects' => array(
+ array(
+ 'name' => 'image_scale',
+ 'data' => array('width' => 100, 'height' => 100, 'upscale' => 1),
+ 'weight' => 0,
+ ),
+ )
+ );
+
+ $styles['medium'] = array(
+ 'label' => 'Medium (220x220)',
+ 'effects' => array(
+ array(
+ 'name' => 'image_scale',
+ 'data' => array('width' => 220, 'height' => 220, 'upscale' => 1),
+ 'weight' => 0,
+ ),
+ )
+ );
+
+ $styles['large'] = array(
+ 'label' => 'Large (480x480)',
+ 'effects' => array(
+ array(
+ 'name' => 'image_scale',
+ 'data' => array('width' => 480, 'height' => 480, 'upscale' => 0),
+ 'weight' => 0,
+ ),
+ )
+ );
+
+ return $styles;
+}
+
+/**
+ * Implements hook_image_style_save().
+ */
+function image_image_style_save($style) {
+ if (isset($style['old_name']) && $style['old_name'] != $style['name']) {
+ $instances = field_read_instances();
+ // Loop through all fields searching for image fields.
+ foreach ($instances as $instance) {
+ if ($instance['widget']['module'] == 'image') {
+ $instance_changed = FALSE;
+ foreach ($instance['display'] as $view_mode => $display) {
+ // Check if the formatter involves an image style.
+ if ($display['type'] == 'image' && $display['settings']['image_style'] == $style['old_name']) {
+ // Update display information for any instance using the image
+ // style that was just deleted.
+ $instance['display'][$view_mode]['settings']['image_style'] = $style['name'];
+ $instance_changed = TRUE;
+ }
+ }
+ if ($instance['widget']['settings']['preview_image_style'] == $style['old_name']) {
+ $instance['widget']['settings']['preview_image_style'] = $style['name'];
+ $instance_changed = TRUE;
+ }
+ if ($instance_changed) {
+ field_update_instance($instance);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_image_style_delete().
+ */
+function image_image_style_delete($style) {
+ image_image_style_save($style);
+}
+
+/**
+ * Implements hook_field_delete_field().
+ */
+function image_field_delete_field($field) {
+ if ($field['type'] != 'image') {
+ return;
+ }
+
+ // The value of a managed_file element can be an array if #extended == TRUE.
+ $fid = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']);
+ if ($fid && ($file = file_load($fid))) {
+ file_usage_delete($file, 'image', 'default_image', $field['id']);
+ }
+}
+
+/**
+ * Implements hook_field_update_field().
+ */
+function image_field_update_field($field, $prior_field, $has_data) {
+ if ($field['type'] != 'image') {
+ return;
+ }
+
+ // The value of a managed_file element can be an array if #extended == TRUE.
+ $fid_new = (is_array($field['settings']['default_image']) ? $field['settings']['default_image']['fid'] : $field['settings']['default_image']);
+ $fid_old = (is_array($prior_field['settings']['default_image']) ? $prior_field['settings']['default_image']['fid'] : $prior_field['settings']['default_image']);
+
+ $file_new = $fid_new ? file_load($fid_new) : FALSE;
+
+ if ($fid_new != $fid_old) {
+
+ // Is there a new file?
+ if ($file_new) {
+ $file_new->status = FILE_STATUS_PERMANENT;
+ file_save($file_new);
+ file_usage_add($file_new, 'image', 'default_image', $field['id']);
+ }
+
+ // Is there an old file?
+ if ($fid_old && ($file_old = file_load($fid_old))) {
+ file_usage_delete($file_old, 'image', 'default_image', $field['id']);
+ }
+ }
+
+ // If the upload destination changed, then move the file.
+ if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) {
+ $directory = $field['settings']['uri_scheme'] . '://default_images/';
+ file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
+ file_move($file_new, $directory . $file_new->filename);
+ }
+}
+
+/**
+ * Implements hook_field_delete_instance().
+ */
+function image_field_delete_instance($instance) {
+ // Only act on image fields.
+ $field = field_read_field($instance['field_name']);
+ if ($field['type'] != 'image') {
+ return;
+ }
+
+ // The value of a managed_file element can be an array if the #extended
+ // property is set to TRUE.
+ $fid = $instance['settings']['default_image'];
+ if (is_array($fid)) {
+ $fid = $fid['fid'];
+ }
+
+ // Remove the default image when the instance is deleted.
+ if ($fid && ($file = file_load($fid))) {
+ file_usage_delete($file, 'image', 'default_image', $instance['id']);
+ }
+}
+
+/**
+ * Implements hook_field_update_instance().
+ */
+function image_field_update_instance($instance, $prior_instance) {
+ // Only act on image fields.
+ $field = field_read_field($instance['field_name']);
+ if ($field['type'] != 'image') {
+ return;
+ }
+
+ // The value of a managed_file element can be an array if the #extended
+ // property is set to TRUE.
+ $fid_new = $instance['settings']['default_image'];
+ if (is_array($fid_new)) {
+ $fid_new = $fid_new['fid'];
+ }
+ $fid_old = $prior_instance['settings']['default_image'];
+ if (is_array($fid_old)) {
+ $fid_old = $fid_old['fid'];
+ }
+
+ // If the old and new files do not match, update the default accordingly.
+ $file_new = $fid_new ? file_load($fid_new) : FALSE;
+ if ($fid_new != $fid_old) {
+ // Save the new file, if present.
+ if ($file_new) {
+ $file_new->status = FILE_STATUS_PERMANENT;
+ file_save($file_new);
+ file_usage_add($file_new, 'image', 'default_image', $instance['id']);
+ }
+ // Delete the old file, if present.
+ if ($fid_old && ($file_old = file_load($fid_old))) {
+ file_usage_delete($file_old, 'image', 'default_image', $instance['id']);
+ }
+ }
+
+ // If the upload destination changed, then move the file.
+ if ($file_new && (file_uri_scheme($file_new->uri) != $field['settings']['uri_scheme'])) {
+ $directory = $field['settings']['uri_scheme'] . '://default_images/';
+ file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
+ file_move($file_new, $directory . $file_new->filename);
+ }
+}
+
+/**
+ * Clears cached versions of a specific file in all styles.
+ *
+ * @param $path
+ * The Drupal file path to the original image.
+ */
+function image_path_flush($path) {
+ $styles = image_styles();
+ foreach ($styles as $style) {
+ $image_path = image_style_path($style['name'], $path);
+ if (file_exists($image_path)) {
+ file_unmanaged_delete($image_path);
+ }
+ }
+}
+
+/**
+ * Gets an array of all styles and their settings.
+ *
+ * @return
+ * An array of styles keyed by the image style ID (isid).
+ * @see image_style_load()
+ */
+function image_styles() {
+ $styles = &drupal_static(__FUNCTION__);
+
+ // Grab from cache or build the array.
+ if (!isset($styles)) {
+ if ($cache = cache_get('image_styles', 'cache')) {
+ $styles = $cache->data;
+ }
+ else {
+ $styles = array();
+
+ // Select the module-defined styles.
+ foreach (module_implements('image_default_styles') as $module) {
+ $module_styles = module_invoke($module, 'image_default_styles');
+ foreach ($module_styles as $style_name => $style) {
+ $style['name'] = $style_name;
+ $style['label'] = empty($style['label']) ? $style_name : $style['label'];
+ $style['module'] = $module;
+ $style['storage'] = IMAGE_STORAGE_DEFAULT;
+ foreach ($style['effects'] as $key => $effect) {
+ $definition = image_effect_definition_load($effect['name']);
+ $effect = array_merge($definition, $effect);
+ $style['effects'][$key] = $effect;
+ }
+ $styles[$style_name] = $style;
+ }
+ }
+
+ // Select all the user-defined styles.
+ $user_styles = db_select('image_styles', NULL, array('fetch' => PDO::FETCH_ASSOC))
+ ->fields('image_styles')
+ ->orderBy('name')
+ ->execute()
+ ->fetchAllAssoc('name', PDO::FETCH_ASSOC);
+
+ // Allow the user styles to override the module styles.
+ foreach ($user_styles as $style_name => $style) {
+ $style['module'] = NULL;
+ $style['storage'] = IMAGE_STORAGE_NORMAL;
+ $style['effects'] = image_style_effects($style);
+ if (isset($styles[$style_name]['module'])) {
+ $style['module'] = $styles[$style_name]['module'];
+ $style['storage'] = IMAGE_STORAGE_OVERRIDE;
+ }
+ $styles[$style_name] = $style;
+ }
+
+ drupal_alter('image_styles', $styles);
+ cache_set('image_styles', $styles);
+ }
+ }
+
+ return $styles;
+}
+
+/**
+ * Loads a style by style name or ID.
+ *
+ * May be used as a loader for menu items.
+ *
+ * @param $name
+ * The name of the style.
+ * @param $isid
+ * Optional. The numeric id of a style if the name is not known.
+ * @param $include
+ * If set, this loader will restrict to a specific type of image style, may be
+ * one of the defined Image style storage constants.
+ *
+ * @return
+ * An image style array containing the following keys:
+ * - "isid": The unique image style ID.
+ * - "name": The unique image style name.
+ * - "effects": An array of image effects within this image style.
+ * If the image style name or ID is not valid, an empty array is returned.
+ * @see image_effect_load()
+ */
+function image_style_load($name = NULL, $isid = NULL, $include = NULL) {
+ $styles = image_styles();
+
+ // If retrieving by name.
+ if (isset($name) && isset($styles[$name])) {
+ $style = $styles[$name];
+ }
+
+ // If retrieving by image style id.
+ if (!isset($name) && isset($isid)) {
+ foreach ($styles as $name => $database_style) {
+ if (isset($database_style['isid']) && $database_style['isid'] == $isid) {
+ $style = $database_style;
+ break;
+ }
+ }
+ }
+
+ // Restrict to the specific type of flag. This bitwise operation basically
+ // states "if the storage is X, then allow".
+ if (isset($style) && (!isset($include) || ($style['storage'] & (int) $include))) {
+ return $style;
+ }
+
+ // Otherwise the style was not found.
+ return FALSE;
+}
+
+/**
+ * Saves an image style.
+ *
+ * @param array $style
+ * An image style array containing:
+ * - name: A unique name for the style.
+ * - isid: (optional) An image style ID.
+ *
+ * @return array
+ * An image style array containing:
+ * - name: An unique name for the style.
+ * - old_name: The original name for the style.
+ * - isid: An image style ID.
+ * - is_new: TRUE if this is a new style, and FALSE if it is an existing
+ * style.
+ */
+function image_style_save($style) {
+ if (isset($style['isid']) && is_numeric($style['isid'])) {
+ // Load the existing style to make sure we account for renamed styles.
+ $old_style = image_style_load(NULL, $style['isid']);
+ image_style_flush($old_style);
+ drupal_write_record('image_styles', $style, 'isid');
+ if ($old_style['name'] != $style['name']) {
+ $style['old_name'] = $old_style['name'];
+ }
+ }
+ else {
+ // Add a default label when not given.
+ if (empty($style['label'])) {
+ $style['label'] = $style['name'];
+ }
+ drupal_write_record('image_styles', $style);
+ $style['is_new'] = TRUE;
+ }
+
+ // Let other modules update as necessary on save.
+ module_invoke_all('image_style_save', $style);
+
+ // Clear all caches and flush.
+ image_style_flush($style);
+
+ return $style;
+}
+
+/**
+ * Deletes an image style.
+ *
+ * @param $style
+ * An image style array.
+ * @param $replacement_style_name
+ * (optional) When deleting a style, specify a replacement style name so
+ * that existing settings (if any) may be converted to a new style.
+ *
+ * @return
+ * TRUE on success.
+ */
+function image_style_delete($style, $replacement_style_name = '') {
+ image_style_flush($style);
+
+ db_delete('image_effects')->condition('isid', $style['isid'])->execute();
+ db_delete('image_styles')->condition('isid', $style['isid'])->execute();
+
+ // Let other modules update as necessary on save.
+ $style['old_name'] = $style['name'];
+ $style['name'] = $replacement_style_name;
+ module_invoke_all('image_style_delete', $style);
+
+ return TRUE;
+}
+
+/**
+ * Loads all the effects for an image style.
+ *
+ * @param array $style
+ * An image style array containing:
+ * - isid: The unique image style ID that contains this image effect.
+ *
+ * @return array
+ * An array of image effects associated with specified image style in the
+ * format array('isid' => array()), or an empty array if the specified style
+ * has no effects.
+ * @see image_effects()
+ */
+function image_style_effects($style) {
+ $effects = image_effects();
+ $style_effects = array();
+ foreach ($effects as $effect) {
+ if ($style['isid'] == $effect['isid']) {
+ $style_effects[$effect['ieid']] = $effect;
+ }
+ }
+
+ return $style_effects;
+}
+
+/**
+ * Gets an array of image styles suitable for using as select list options.
+ *
+ * @param $include_empty
+ * If TRUE a option will be inserted in the options array.
+ * @param $output
+ * Optional flag determining how the options will be sanitized on output.
+ * Leave this at the default (CHECK_PLAIN) if you are using the output of
+ * this function directly in an HTML context, such as for checkbox or radio
+ * button labels, and do not plan to sanitize it on your own. If using the
+ * output of this function as select list options (its primary use case), you
+ * should instead set this flag to PASS_THROUGH to avoid double-escaping of
+ * the output (the form API sanitizes select list options by default).
+ *
+ * @return
+ * Array of image styles with the machine name as key and the label as value.
+ */
+function image_style_options($include_empty = TRUE, $output = CHECK_PLAIN) {
+ $styles = image_styles();
+ $options = array();
+ if ($include_empty && !empty($styles)) {
+ $options[''] = t('');
+ }
+ foreach ($styles as $name => $style) {
+ $options[$name] = ($output == PASS_THROUGH) ? $style['label'] : check_plain($style['label']);
+ }
+
+ if (empty($options)) {
+ $options[''] = t('No defined styles');
+ }
+ return $options;
+}
+
+/**
+ * Page callback: Generates a derivative, given a style and image path.
+ *
+ * After generating an image, transfer it to the requesting agent.
+ *
+ * @param $style
+ * The image style
+ */
+function image_style_deliver($style, $scheme) {
+ $args = func_get_args();
+ array_shift($args);
+ array_shift($args);
+ $target = implode('/', $args);
+
+ // Check that the style is defined, the scheme is valid, and the image
+ // derivative token is valid. (Sites which require image derivatives to be
+ // generated without a token can set the 'image_allow_insecure_derivatives'
+ // variable to TRUE to bypass the latter check, but this will increase the
+ // site's vulnerability to denial-of-service attacks. To prevent this
+ // variable from leaving the site vulnerable to the most serious attacks, a
+ // token is always required when a derivative of a derivative is requested.)
+ $valid = !empty($style) && file_stream_wrapper_valid_scheme($scheme);
+ if (!variable_get('image_allow_insecure_derivatives', FALSE) || strpos(ltrim($target, '\/'), 'styles/') === 0) {
+ $valid = $valid && isset($_GET[IMAGE_DERIVATIVE_TOKEN]) && $_GET[IMAGE_DERIVATIVE_TOKEN] === image_style_path_token($style['name'], $scheme . '://' . $target);
+ }
+ if (!$valid) {
+ return MENU_ACCESS_DENIED;
+ }
+
+ $image_uri = $scheme . '://' . $target;
+ $derivative_uri = image_style_path($style['name'], $image_uri);
+
+ // If using the private scheme, let other modules provide headers and
+ // control access to the file.
+ if ($scheme == 'private') {
+ if (file_exists($derivative_uri)) {
+ file_download($scheme, file_uri_target($derivative_uri));
+ }
+ else {
+ $headers = module_invoke_all('file_download', $image_uri);
+ if (in_array(-1, $headers) || empty($headers)) {
+ return MENU_ACCESS_DENIED;
+ }
+ if (count($headers)) {
+ foreach ($headers as $name => $value) {
+ drupal_add_http_header($name, $value);
+ }
+ }
+ }
+ }
+
+ // Don't start generating the image if the derivative already exists or if
+ // generation is in progress in another thread.
+ $lock_name = 'image_style_deliver:' . $style['name'] . ':' . drupal_hash_base64($image_uri);
+ if (!file_exists($derivative_uri)) {
+ $lock_acquired = lock_acquire($lock_name);
+ if (!$lock_acquired) {
+ // Tell client to retry again in 3 seconds. Currently no browsers are known
+ // to support Retry-After.
+ drupal_add_http_header('Status', '503 Service Unavailable');
+ drupal_add_http_header('Retry-After', 3);
+ print t('Image generation in progress. Try again shortly.');
+ drupal_exit();
+ }
+ }
+
+ // Try to generate the image, unless another thread just did it while we were
+ // acquiring the lock.
+ $success = file_exists($derivative_uri) || image_style_create_derivative($style, $image_uri, $derivative_uri);
+
+ if (!empty($lock_acquired)) {
+ lock_release($lock_name);
+ }
+
+ if ($success) {
+ $image = image_load($derivative_uri);
+ file_transfer($image->source, array('Content-Type' => $image->info['mime_type'], 'Content-Length' => $image->info['file_size']));
+ }
+ else {
+ watchdog('image', 'Unable to generate the derived image located at %path.', array('%path' => $derivative_uri));
+ drupal_add_http_header('Status', '500 Internal Server Error');
+ print t('Error generating image.');
+ drupal_exit();
+ }
+}
+
+/**
+ * Creates a new image derivative based on an image style.
+ *
+ * Generates an image derivative by creating the destination folder (if it does
+ * not already exist), applying all image effects defined in $style['effects'],
+ * and saving a cached version of the resulting image.
+ *
+ * @param $style
+ * An image style array.
+ * @param $source
+ * Path of the source file.
+ * @param $destination
+ * Path or URI of the destination file.
+ *
+ * @return
+ * TRUE if an image derivative was generated, or FALSE if the image derivative
+ * could not be generated.
+ *
+ * @see image_style_load()
+ */
+function image_style_create_derivative($style, $source, $destination) {
+ // If the source file doesn't exist, return FALSE without creating folders.
+ if (!$image = image_load($source)) {
+ return FALSE;
+ }
+
+ // Get the folder for the final location of this style.
+ $directory = drupal_dirname($destination);
+
+ // Build the destination folder tree if it doesn't already exist.
+ if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
+ watchdog('image', 'Failed to create style directory: %directory', array('%directory' => $directory), WATCHDOG_ERROR);
+ return FALSE;
+ }
+
+ foreach ($style['effects'] as $effect) {
+ image_effect_apply($image, $effect);
+ }
+
+ if (!image_save($image, $destination)) {
+ if (file_exists($destination)) {
+ watchdog('image', 'Cached image file %destination already exists. There may be an issue with your rewrite configuration.', array('%destination' => $destination), WATCHDOG_ERROR);
+ }
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/**
+ * Determines the dimensions of the styled image.
+ *
+ * Applies all of an image style's effects to $dimensions.
+ *
+ * @param $style_name
+ * The name of the style to be applied.
+ * @param $dimensions
+ * Dimensions to be modified - an array with components width and height, in
+ * pixels.
+ */
+function image_style_transform_dimensions($style_name, array &$dimensions) {
+ module_load_include('inc', 'image', 'image.effects');
+ $style = image_style_load($style_name);
+
+ if (!is_array($style)) {
+ return;
+ }
+
+ foreach ($style['effects'] as $effect) {
+ if (isset($effect['dimensions passthrough'])) {
+ continue;
+ }
+
+ if (isset($effect['dimensions callback'])) {
+ $effect['dimensions callback']($dimensions, $effect['data']);
+ }
+ else {
+ $dimensions['width'] = $dimensions['height'] = NULL;
+ }
+ }
+}
+
+/**
+ * Flushes cached media for a style.
+ *
+ * @param $style
+ * An image style array.
+ */
+function image_style_flush($style) {
+ // Delete the style directory in each registered wrapper.
+ $wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
+ foreach ($wrappers as $wrapper => $wrapper_data) {
+ if (file_exists($directory = $wrapper . '://styles/' . $style['name'])) {
+ file_unmanaged_delete_recursive($directory);
+ }
+ }
+
+ // Let other modules update as necessary on flush.
+ module_invoke_all('image_style_flush', $style);
+
+ // Clear image style and effect caches.
+ cache_clear_all('image_styles', 'cache');
+ cache_clear_all('image_effects:', 'cache', TRUE);
+ drupal_static_reset('image_styles');
+ drupal_static_reset('image_effects');
+
+ // Clear field caches so that formatters may be added for this style.
+ field_info_cache_clear();
+ drupal_theme_rebuild();
+
+ // Clear page caches when flushing.
+ if (module_exists('block')) {
+ cache_clear_all('*', 'cache_block', TRUE);
+ }
+ cache_clear_all('*', 'cache_page', TRUE);
+}
+
+/**
+ * Returns the URL for an image derivative given a style and image path.
+ *
+ * @param $style_name
+ * The name of the style to be used with this image.
+ * @param $path
+ * The path to the image.
+ *
+ * @return
+ * The absolute URL where a style image can be downloaded, suitable for use
+ * in an tag. Requesting the URL will cause the image to be created.
+ * @see image_style_deliver()
+ */
+function image_style_url($style_name, $path) {
+ $uri = image_style_path($style_name, $path);
+
+ // The passed-in $path variable can be either a relative path or a full URI.
+ $original_uri = file_uri_scheme($path) ? file_stream_wrapper_uri_normalize($path) : file_build_uri($path);
+
+ // The token query is added even if the 'image_allow_insecure_derivatives'
+ // variable is TRUE, so that the emitted links remain valid if it is changed
+ // back to the default FALSE.
+ $token_query = array(IMAGE_DERIVATIVE_TOKEN => image_style_path_token($style_name, $original_uri));
+
+ // If not using clean URLs, the image derivative callback is only available
+ // with the query string. If the file does not exist, use url() to ensure
+ // that it is included. Once the file exists it's fine to fall back to the
+ // actual file path, this avoids bootstrapping PHP once the files are built.
+ if (!variable_get('clean_url') && file_uri_scheme($uri) == 'public' && !file_exists($uri)) {
+ $directory_path = file_stream_wrapper_get_instance_by_uri($uri)->getDirectoryPath();
+ return url($directory_path . '/' . file_uri_target($uri), array('absolute' => TRUE, 'query' => $token_query));
+ }
+
+ $file_url = file_create_url($uri);
+ // Append the query string with the token.
+ return $file_url . (strpos($file_url, '?') !== FALSE ? '&' : '?') . drupal_http_build_query($token_query);
+}
+
+/**
+ * Generates a token to protect an image style derivative.
+ *
+ * This prevents unauthorized generation of an image style derivative,
+ * which can be costly both in CPU time and disk space.
+ *
+ * @param $style_name
+ * The name of the image style.
+ * @param $uri
+ * The URI of the image for this style, for example as returned by
+ * image_style_path().
+ *
+ * @return
+ * An eight-character token which can be used to protect image style
+ * derivatives against denial-of-service attacks.
+ */
+function image_style_path_token($style_name, $uri) {
+ // Return the first eight characters.
+ return substr(drupal_hmac_base64($style_name . ':' . $uri, drupal_get_private_key() . drupal_get_hash_salt()), 0, 8);
+}
+
+/**
+ * Returns the URI of an image when using a style.
+ *
+ * The path returned by this function may not exist. The default generation
+ * method only creates images when they are requested by a user's browser.
+ *
+ * @param $style_name
+ * The name of the style to be used with this image.
+ * @param $uri
+ * The URI or path to the image.
+ *
+ * @return
+ * The URI to an image style image.
+ * @see image_style_url()
+ */
+function image_style_path($style_name, $uri) {
+ $scheme = file_uri_scheme($uri);
+ if ($scheme) {
+ $path = file_uri_target($uri);
+ }
+ else {
+ $path = $uri;
+ $scheme = file_default_scheme();
+ }
+ return $scheme . '://styles/' . $style_name . '/' . $scheme . '/' . $path;
+}
+
+/**
+ * Saves a default image style to the database.
+ *
+ * @param style
+ * An image style array provided by a module.
+ *
+ * @return
+ * An image style array. The returned style array will include the new 'isid'
+ * assigned to the style.
+ */
+function image_default_style_save($style) {
+ $style = image_style_save($style);
+ $effects = array();
+ foreach ($style['effects'] as $effect) {
+ $effect['isid'] = $style['isid'];
+ $effect = image_effect_save($effect);
+ $effects[$effect['ieid']] = $effect;
+ }
+ $style['effects'] = $effects;
+ return $style;
+}
+
+/**
+ * Reverts the changes made by users to a default image style.
+ *
+ * @param style
+ * An image style array.
+ * @return
+ * Boolean TRUE if the operation succeeded.
+ */
+function image_default_style_revert($style) {
+ image_style_flush($style);
+
+ db_delete('image_effects')->condition('isid', $style['isid'])->execute();
+ db_delete('image_styles')->condition('isid', $style['isid'])->execute();
+
+ return TRUE;
+}
+
+/**
+ * Returns a set of image effects.
+ *
+ * These image effects are exposed by modules implementing
+ * hook_image_effect_info().
+ *
+ * @return
+ * An array of image effects to be used when transforming images.
+ * @see hook_image_effect_info()
+ * @see image_effect_definition_load()
+ */
+function image_effect_definitions() {
+ global $language;
+
+ // hook_image_effect_info() includes translated strings, so each language is
+ // cached separately.
+ $langcode = $language->language;
+
+ $effects = &drupal_static(__FUNCTION__);
+
+ if (!isset($effects)) {
+ if ($cache = cache_get("image_effects:$langcode")) {
+ $effects = $cache->data;
+ }
+ else {
+ $effects = array();
+ include_once DRUPAL_ROOT . '/modules/image/image.effects.inc';
+ foreach (module_implements('image_effect_info') as $module) {
+ foreach (module_invoke($module, 'image_effect_info') as $name => $effect) {
+ // Ensure the current toolkit supports the effect.
+ $effect['module'] = $module;
+ $effect['name'] = $name;
+ $effect['data'] = isset($effect['data']) ? $effect['data'] : array();
+ $effects[$name] = $effect;
+ }
+ }
+ uasort($effects, '_image_effect_definitions_sort');
+ drupal_alter('image_effect_info', $effects);
+ cache_set("image_effects:$langcode", $effects);
+ }
+ }
+
+ return $effects;
+}
+
+/**
+ * Loads the definition for an image effect.
+ *
+ * The effect definition is a set of core properties for an image effect, not
+ * containing any user-settings. The definition defines various functions to
+ * call when configuring or executing an image effect. This loader is mostly for
+ * internal use within image.module. Use image_effect_load() or
+ * image_style_load() to get image effects that contain configuration.
+ *
+ * @param $effect
+ * The name of the effect definition to load.
+ * @param $style
+ * An image style array to which this effect will be added.
+ *
+ * @return
+ * An array containing the image effect definition with the following keys:
+ * - "effect": The unique name for the effect being performed. Usually prefixed
+ * with the name of the module providing the effect.
+ * - "module": The module providing the effect.
+ * - "help": A description of the effect.
+ * - "function": The name of the function that will execute the effect.
+ * - "form": (optional) The name of a function to configure the effect.
+ * - "summary": (optional) The name of a theme function that will display a
+ * one-line summary of the effect. Does not include the "theme_" prefix.
+ */
+function image_effect_definition_load($effect, $style_name = NULL) {
+ $definitions = image_effect_definitions();
+
+ // If a style is specified, do not allow loading of default style
+ // effects.
+ if (isset($style_name)) {
+ $style = image_style_load($style_name, NULL);
+ if ($style['storage'] == IMAGE_STORAGE_DEFAULT) {
+ return FALSE;
+ }
+ }
+
+ return isset($definitions[$effect]) ? $definitions[$effect] : FALSE;
+}
+
+/**
+ * Loads all image effects from the database.
+ *
+ * @return
+ * An array of all image effects.
+ * @see image_effect_load()
+ */
+function image_effects() {
+ $effects = &drupal_static(__FUNCTION__);
+
+ if (!isset($effects)) {
+ $effects = array();
+
+ // Add database image effects.
+ $result = db_select('image_effects', NULL, array('fetch' => PDO::FETCH_ASSOC))
+ ->fields('image_effects')
+ ->orderBy('image_effects.weight', 'ASC')
+ ->execute();
+ foreach ($result as $effect) {
+ $effect['data'] = unserialize($effect['data']);
+ $definition = image_effect_definition_load($effect['name']);
+ // Do not load image effects whose definition cannot be found.
+ if ($definition) {
+ $effect = array_merge($definition, $effect);
+ $effects[$effect['ieid']] = $effect;
+ }
+ }
+ }
+
+ return $effects;
+}
+
+/**
+ * Loads a single image effect.
+ *
+ * @param $ieid
+ * The image effect ID.
+ * @param $style_name
+ * The image style name.
+ * @param $include
+ * If set, this loader will restrict to a specific type of image style, may be
+ * one of the defined Image style storage constants.
+ *
+ * @return
+ * An image effect array, consisting of the following keys:
+ * - "ieid": The unique image effect ID.
+ * - "isid": The unique image style ID that contains this image effect.
+ * - "weight": The weight of this image effect within the image style.
+ * - "name": The name of the effect definition that powers this image effect.
+ * - "data": An array of configuration options for this image effect.
+ * Besides these keys, the entirety of the image definition is merged into
+ * the image effect array. Returns FALSE if the specified effect cannot be
+ * found.
+ * @see image_style_load()
+ * @see image_effect_definition_load()
+ */
+function image_effect_load($ieid, $style_name, $include = NULL) {
+ if (($style = image_style_load($style_name, NULL, $include)) && isset($style['effects'][$ieid])) {
+ return $style['effects'][$ieid];
+ }
+ return FALSE;
+}
+
+/**
+ * Saves an image effect.
+ *
+ * @param $effect
+ * An image effect array.
+ *
+ * @return
+ * An image effect array. In the case of a new effect, 'ieid' will be set.
+ */
+function image_effect_save($effect) {
+ if (!empty($effect['ieid'])) {
+ drupal_write_record('image_effects', $effect, 'ieid');
+ }
+ else {
+ drupal_write_record('image_effects', $effect);
+ }
+ $style = image_style_load(NULL, $effect['isid']);
+ image_style_flush($style);
+ return $effect;
+}
+
+/**
+ * Deletes an image effect.
+ *
+ * @param $effect
+ * An image effect array.
+ */
+function image_effect_delete($effect) {
+ db_delete('image_effects')->condition('ieid', $effect['ieid'])->execute();
+ $style = image_style_load(NULL, $effect['isid']);
+ image_style_flush($style);
+}
+
+/**
+ * Applies an image effect to the image object.
+ *
+ * @param $image
+ * An image object returned by image_load().
+ * @param $effect
+ * An image effect array.
+ *
+ * @return
+ * TRUE on success. FALSE if unable to perform the image effect on the image.
+ */
+function image_effect_apply($image, $effect) {
+ module_load_include('inc', 'image', 'image.effects');
+ $function = $effect['effect callback'];
+ if (function_exists($function)) {
+ return $function($image, $effect['data']);
+ }
+ return FALSE;
+}
+
+/**
+ * Returns HTML for an image using a specific image style.
+ *
+ * @param $variables
+ * An associative array containing:
+ * - style_name: The name of the style to be used to alter the original image.
+ * - path: The path of the image file relative to the Drupal files directory.
+ * This function does not work with images outside the files directory nor
+ * with remotely hosted images. This should be in a format such as
+ * 'images/image.jpg', or using a stream wrapper such as
+ * 'public://images/image.jpg'.
+ * - width: The width of the source image (if known).
+ * - height: The height of the source image (if known).
+ * - alt: The alternative text for text-based browsers.
+ * - title: The title text is displayed when the image is hovered in some
+ * popular browsers.
+ * - attributes: Associative array of attributes to be placed in the img tag.
+ *
+ * @ingroup themeable
+ */
+function theme_image_style($variables) {
+ // Determine the dimensions of the styled image.
+ $dimensions = array(
+ 'width' => $variables['width'],
+ 'height' => $variables['height'],
+ );
+
+ image_style_transform_dimensions($variables['style_name'], $dimensions);
+
+ $variables['width'] = $dimensions['width'];
+ $variables['height'] = $dimensions['height'];
+
+ // Determine the URL for the styled image.
+ $variables['path'] = image_style_url($variables['style_name'], $variables['path']);
+ return theme('image', $variables);
+}
+
+/**
+ * Accepts a keyword (center, top, left, etc) and returns it as a pixel offset.
+ *
+ * @param $value
+ * @param $current_pixels
+ * @param $new_pixels
+ */
+function image_filter_keyword($value, $current_pixels, $new_pixels) {
+ switch ($value) {
+ case 'top':
+ case 'left':
+ return 0;
+
+ case 'bottom':
+ case 'right':
+ return $current_pixels - $new_pixels;
+
+ case 'center':
+ return $current_pixels / 2 - $new_pixels / 2;
+ }
+ return $value;
+}
+
+/**
+ * Internal function for sorting image effect definitions through uasort().
+ *
+ * @see image_effect_definitions()
+ */
+function _image_effect_definitions_sort($a, $b) {
+ return strcasecmp($a['name'], $b['name']);
+}
diff --git a/drupal-dev/modules/image/image.test b/drupal-dev/modules/image/image.test
new file mode 100644
index 0000000..4a4aab0
--- /dev/null
+++ b/drupal-dev/modules/image/image.test
@@ -0,0 +1,1865 @@
+admin_user = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'create article content', 'edit any article content', 'delete any article content', 'administer image styles'));
+ $this->drupalLogin($this->admin_user);
+ }
+
+ /**
+ * Create a new image field.
+ *
+ * @param $name
+ * The name of the new field (all lowercase), exclude the "field_" prefix.
+ * @param $type_name
+ * The node type that this field will be added to.
+ * @param $field_settings
+ * A list of field settings that will be added to the defaults.
+ * @param $instance_settings
+ * A list of instance settings that will be added to the instance defaults.
+ * @param $widget_settings
+ * A list of widget settings that will be added to the widget defaults.
+ */
+ function createImageField($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) {
+ $field = array(
+ 'field_name' => $name,
+ 'type' => 'image',
+ 'settings' => array(),
+ 'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1,
+ );
+ $field['settings'] = array_merge($field['settings'], $field_settings);
+ field_create_field($field);
+
+ $instance = array(
+ 'field_name' => $field['field_name'],
+ 'entity_type' => 'node',
+ 'label' => $name,
+ 'bundle' => $type_name,
+ 'required' => !empty($instance_settings['required']),
+ 'settings' => array(),
+ 'widget' => array(
+ 'type' => 'image_image',
+ 'settings' => array(),
+ ),
+ );
+ $instance['settings'] = array_merge($instance['settings'], $instance_settings);
+ $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings);
+ return field_create_instance($instance);
+ }
+
+ /**
+ * Upload an image to a node.
+ *
+ * @param $image
+ * A file object representing the image to upload.
+ * @param $field_name
+ * Name of the image field the image should be attached to.
+ * @param $type
+ * The type of node to create.
+ */
+ function uploadNodeImage($image, $field_name, $type) {
+ $edit = array(
+ 'title' => $this->randomName(),
+ );
+ $edit['files[' . $field_name . '_' . LANGUAGE_NONE . '_0]'] = drupal_realpath($image->uri);
+ $this->drupalPost('node/add/' . $type, $edit, t('Save'));
+
+ // Retrieve ID of the newly created node from the current URL.
+ $matches = array();
+ preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
+ return isset($matches[1]) ? $matches[1] : FALSE;
+ }
+}
+
+/**
+ * Tests the functions for generating paths and URLs for image styles.
+ */
+class ImageStylesPathAndUrlTestCase extends DrupalWebTestCase {
+ protected $style_name;
+ protected $image_info;
+ protected $image_filepath;
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image styles path and URL functions',
+ 'description' => 'Tests functions for generating paths and URLs to image styles.',
+ 'group' => 'Image',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('image_module_test');
+
+ $this->style_name = 'style_foo';
+ image_style_save(array('name' => $this->style_name, 'label' => $this->randomString()));
+ }
+
+ /**
+ * Test image_style_path().
+ */
+ function testImageStylePath() {
+ $scheme = 'public';
+ $actual = image_style_path($this->style_name, "$scheme://foo/bar.gif");
+ $expected = "$scheme://styles/" . $this->style_name . "/$scheme/foo/bar.gif";
+ $this->assertEqual($actual, $expected, 'Got the path for a file URI.');
+
+ $actual = image_style_path($this->style_name, 'foo/bar.gif');
+ $expected = "$scheme://styles/" . $this->style_name . "/$scheme/foo/bar.gif";
+ $this->assertEqual($actual, $expected, 'Got the path for a relative file path.');
+ }
+
+ /**
+ * Test image_style_url() with a file using the "public://" scheme.
+ */
+ function testImageStyleUrlAndPathPublic() {
+ $this->_testImageStyleUrlAndPath('public');
+ }
+
+ /**
+ * Test image_style_url() with a file using the "private://" scheme.
+ */
+ function testImageStyleUrlAndPathPrivate() {
+ $this->_testImageStyleUrlAndPath('private');
+ }
+
+ /**
+ * Test image_style_url() with the "public://" scheme and unclean URLs.
+ */
+ function testImageStylUrlAndPathPublicUnclean() {
+ $this->_testImageStyleUrlAndPath('public', FALSE);
+ }
+
+ /**
+ * Test image_style_url() with the "private://" schema and unclean URLs.
+ */
+ function testImageStyleUrlAndPathPrivateUnclean() {
+ $this->_testImageStyleUrlAndPath('private', FALSE);
+ }
+
+ /**
+ * Test image_style_url() with a file URL that has an extra slash in it.
+ */
+ function testImageStyleUrlExtraSlash() {
+ $this->_testImageStyleUrlAndPath('public', TRUE, TRUE);
+ }
+
+ /**
+ * Test image_style_url().
+ */
+ function _testImageStyleUrlAndPath($scheme, $clean_url = TRUE, $extra_slash = FALSE) {
+ // Make the default scheme neither "public" nor "private" to verify the
+ // functions work for other than the default scheme.
+ variable_set('file_default_scheme', 'temporary');
+ variable_set('clean_url', $clean_url);
+
+ // Create the directories for the styles.
+ $directory = $scheme . '://styles/' . $this->style_name;
+ $status = file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
+ $this->assertNotIdentical(FALSE, $status, 'Created the directory for the generated images for the test style.');
+
+ // Create a working copy of the file.
+ $files = $this->drupalGetTestFiles('image');
+ $file = array_shift($files);
+ $image_info = image_get_info($file->uri);
+ $original_uri = file_unmanaged_copy($file->uri, $scheme . '://', FILE_EXISTS_RENAME);
+ // Let the image_module_test module know about this file, so it can claim
+ // ownership in hook_file_download().
+ variable_set('image_module_test_file_download', $original_uri);
+ $this->assertNotIdentical(FALSE, $original_uri, 'Created the generated image file.');
+
+ // Get the URL of a file that has not been generated and try to create it.
+ $generated_uri = image_style_path($this->style_name, $original_uri);
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $generate_url = image_style_url($this->style_name, $original_uri);
+
+ // Ensure that the tests still pass when the file is generated by accessing
+ // a poorly constructed (but still valid) file URL that has an extra slash
+ // in it.
+ if ($extra_slash) {
+ $modified_uri = str_replace('://', ':///', $original_uri);
+ $this->assertNotEqual($original_uri, $modified_uri, 'An extra slash was added to the generated file URI.');
+ $generate_url = image_style_url($this->style_name, $modified_uri);
+ }
+
+ if (!$clean_url) {
+ $this->assertTrue(strpos($generate_url, '?q=') !== FALSE, 'When using non-clean URLS, the system path contains the query string.');
+ }
+ // Add some extra chars to the token.
+ $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url));
+ $this->assertResponse(403, 'Image was inaccessible at the URL with an invalid token.');
+ // Change the parameter name so the token is missing.
+ $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url));
+ $this->assertResponse(403, 'Image was inaccessible at the URL with a missing token.');
+
+ // Check that the generated URL is the same when we pass in a relative path
+ // rather than a URI. We need to temporarily switch the default scheme to
+ // match the desired scheme before testing this, then switch it back to the
+ // "temporary" scheme used throughout this test afterwards.
+ variable_set('file_default_scheme', $scheme);
+ $relative_path = file_uri_target($original_uri);
+ $generate_url_from_relative_path = image_style_url($this->style_name, $relative_path);
+ $this->assertEqual($generate_url, $generate_url_from_relative_path, 'Generated URL is the same regardless of whether it came from a relative path or a file URI.');
+ variable_set('file_default_scheme', 'temporary');
+
+ // Fetch the URL that generates the file.
+ $this->drupalGet($generate_url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $this->assertRaw(file_get_contents($generated_uri), 'URL returns expected file.');
+ $generated_image_info = image_get_info($generated_uri);
+ $this->assertEqual($this->drupalGetHeader('Content-Type'), $generated_image_info['mime_type'], 'Expected Content-Type was reported.');
+ $this->assertEqual($this->drupalGetHeader('Content-Length'), $generated_image_info['file_size'], 'Expected Content-Length was reported.');
+ if ($scheme == 'private') {
+ $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
+ $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'no-cache, must-revalidate, post-check=0, pre-check=0', 'Cache-Control header was set to prevent caching.');
+ $this->assertEqual($this->drupalGetHeader('X-Image-Owned-By'), 'image_module_test', 'Expected custom header has been added.');
+
+ // Make sure that a second request to the already existing derivate works
+ // too.
+ $this->drupalGet($generate_url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+
+ // Make sure that access is denied for existing style files if we do not
+ // have access.
+ variable_del('image_module_test_file_download');
+ $this->drupalGet($generate_url);
+ $this->assertResponse(403, 'Confirmed that access is denied for the private image style.');
+
+ // Repeat this with a different file that we do not have access to and
+ // make sure that access is denied.
+ $file_noaccess = array_shift($files);
+ $original_uri_noaccess = file_unmanaged_copy($file_noaccess->uri, $scheme . '://', FILE_EXISTS_RENAME);
+ $generated_uri_noaccess = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/'. drupal_basename($original_uri_noaccess);
+ $this->assertFalse(file_exists($generated_uri_noaccess), 'Generated file does not exist.');
+ $generate_url_noaccess = image_style_url($this->style_name, $original_uri_noaccess);
+
+ $this->drupalGet($generate_url_noaccess);
+ $this->assertResponse(403, 'Confirmed that access is denied for the private image style.');
+ // Verify that images are not appended to the response. Currently this test only uses PNG images.
+ if (strpos($generate_url, '.png') === FALSE ) {
+ $this->fail('Confirming that private image styles are not appended require PNG file.');
+ }
+ else {
+ // Check for PNG-Signature (cf. http://www.libpng.org/pub/png/book/chapter08.html#png.ch08.div.2) in the
+ // response body.
+ $this->assertNoRaw( chr(137) . chr(80) . chr(78) . chr(71) . chr(13) . chr(10) . chr(26) . chr(10), 'No PNG signature found in the response body.');
+ }
+ }
+ elseif ($clean_url) {
+ // Add some extra chars to the token.
+ $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', IMAGE_DERIVATIVE_TOKEN . '=Zo', $generate_url));
+ $this->assertResponse(200, 'Existing image was accessible at the URL with an invalid token.');
+ }
+
+ // Allow insecure image derivatives to be created for the remainder of this
+ // test.
+ variable_set('image_allow_insecure_derivatives', TRUE);
+
+ // Create another working copy of the file.
+ $files = $this->drupalGetTestFiles('image');
+ $file = array_shift($files);
+ $image_info = image_get_info($file->uri);
+ $original_uri = file_unmanaged_copy($file->uri, $scheme . '://', FILE_EXISTS_RENAME);
+ // Let the image_module_test module know about this file, so it can claim
+ // ownership in hook_file_download().
+ variable_set('image_module_test_file_download', $original_uri);
+
+ // Get the URL of a file that has not been generated and try to create it.
+ $generated_uri = image_style_path($this->style_name, $original_uri);
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $generate_url = image_style_url($this->style_name, $original_uri);
+
+ // Check that the image is accessible even without the security token.
+ $this->drupalGet(str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $generate_url));
+ $this->assertResponse(200, 'Image was accessible at the URL with a missing token.');
+
+ // Check that a security token is still required when generating a second
+ // image derivative using the first one as a source.
+ $nested_uri = image_style_path($this->style_name, $generated_uri);
+ $nested_url = image_style_url($this->style_name, $generated_uri);
+ $nested_url_with_wrong_token = str_replace(IMAGE_DERIVATIVE_TOKEN . '=', 'wrongparam=', $nested_url);
+ $this->drupalGet($nested_url_with_wrong_token);
+ $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token.');
+ // Check that this restriction cannot be bypassed by adding extra slashes
+ // to the URL.
+ $this->drupalGet(substr_replace($nested_url_with_wrong_token, '//styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/')));
+ $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra forward slash in the URL.');
+ $this->drupalGet(substr_replace($nested_url_with_wrong_token, '/\styles/', strrpos($nested_url_with_wrong_token, '/styles/'), strlen('/styles/')));
+ $this->assertResponse(403, 'Image generated from an earlier derivative was inaccessible at the URL with a missing token, even with an extra backslash in the URL.');
+ // Make sure the image can still be generated if a correct token is used.
+ $this->drupalGet($nested_url);
+ $this->assertResponse(200, 'Image was accessible when a correct token was provided in the URL.');
+
+ // Check that requesting a nonexistent image does not create any new
+ // directories in the file system.
+ $directory = $scheme . '://styles/' . $this->style_name . '/' . $scheme . '/' . $this->randomName();
+ $this->drupalGet(file_create_url($directory . '/' . $this->randomName()));
+ $this->assertFalse(file_exists($directory), 'New directory was not created in the filesystem when requesting an unauthorized image.');
+ }
+}
+
+/**
+ * Use the image_test.module's mock toolkit to ensure that the effects are
+ * properly passing parameters to the image toolkit.
+ */
+class ImageEffectsUnitTest extends ImageToolkitTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image effects',
+ 'description' => 'Test that the image effects pass parameters to the toolkit correctly.',
+ 'group' => 'Image',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('image_module_test');
+ module_load_include('inc', 'image', 'image.effects');
+ }
+
+ /**
+ * Test the image_resize_effect() function.
+ */
+ function testResizeEffect() {
+ $this->assertTrue(image_resize_effect($this->image, array('width' => 1, 'height' => 2)), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('resize'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['resize'][0][1], 1, 'Width was passed correctly');
+ $this->assertEqual($calls['resize'][0][2], 2, 'Height was passed correctly');
+ }
+
+ /**
+ * Test the image_scale_effect() function.
+ */
+ function testScaleEffect() {
+ // @todo: need to test upscaling.
+ $this->assertTrue(image_scale_effect($this->image, array('width' => 10, 'height' => 10)), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('resize'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['resize'][0][1], 10, 'Width was passed correctly');
+ $this->assertEqual($calls['resize'][0][2], 5, 'Height was based off aspect ratio and passed correctly');
+ }
+
+ /**
+ * Test the image_crop_effect() function.
+ */
+ function testCropEffect() {
+ // @todo should test the keyword offsets.
+ $this->assertTrue(image_crop_effect($this->image, array('anchor' => 'top-1', 'width' => 3, 'height' => 4)), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('crop'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['crop'][0][1], 0, 'X was passed correctly');
+ $this->assertEqual($calls['crop'][0][2], 1, 'Y was passed correctly');
+ $this->assertEqual($calls['crop'][0][3], 3, 'Width was passed correctly');
+ $this->assertEqual($calls['crop'][0][4], 4, 'Height was passed correctly');
+ }
+
+ /**
+ * Test the image_scale_and_crop_effect() function.
+ */
+ function testScaleAndCropEffect() {
+ $this->assertTrue(image_scale_and_crop_effect($this->image, array('width' => 5, 'height' => 10)), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('resize', 'crop'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['crop'][0][1], 7.5, 'X was computed and passed correctly');
+ $this->assertEqual($calls['crop'][0][2], 0, 'Y was computed and passed correctly');
+ $this->assertEqual($calls['crop'][0][3], 5, 'Width was computed and passed correctly');
+ $this->assertEqual($calls['crop'][0][4], 10, 'Height was computed and passed correctly');
+ }
+
+ /**
+ * Test the image_desaturate_effect() function.
+ */
+ function testDesaturateEffect() {
+ $this->assertTrue(image_desaturate_effect($this->image, array()), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('desaturate'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual(count($calls['desaturate'][0]), 1, 'Only the image was passed.');
+ }
+
+ /**
+ * Test the image_rotate_effect() function.
+ */
+ function testRotateEffect() {
+ // @todo: need to test with 'random' => TRUE
+ $this->assertTrue(image_rotate_effect($this->image, array('degrees' => 90, 'bgcolor' => '#fff')), 'Function returned the expected value.');
+ $this->assertToolkitOperationsCalled(array('rotate'));
+
+ // Check the parameters.
+ $calls = image_test_get_all_calls();
+ $this->assertEqual($calls['rotate'][0][1], 90, 'Degrees were passed correctly');
+ $this->assertEqual($calls['rotate'][0][2], 0xffffff, 'Background color was passed correctly');
+ }
+
+ /**
+ * Test image effect caching.
+ */
+ function testImageEffectsCaching() {
+ $image_effect_definitions_called = &drupal_static('image_module_test_image_effect_info_alter');
+
+ // First call should grab a fresh copy of the data.
+ $effects = image_effect_definitions();
+ $this->assertTrue($image_effect_definitions_called === 1, 'image_effect_definitions() generated data.');
+
+ // Second call should come from cache.
+ drupal_static_reset('image_effect_definitions');
+ drupal_static_reset('image_module_test_image_effect_info_alter');
+ $cached_effects = image_effect_definitions();
+ $this->assertTrue(is_null($image_effect_definitions_called), 'image_effect_definitions() returned data from cache.');
+
+ $this->assertTrue($effects == $cached_effects, 'Cached effects are the same as generated effects.');
+ }
+}
+
+/**
+ * Tests creation, deletion, and editing of image styles and effects.
+ */
+class ImageAdminStylesUnitTest extends ImageFieldTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image styles and effects UI configuration',
+ 'description' => 'Tests creation, deletion, and editing of image styles and effects at the UI level.',
+ 'group' => 'Image',
+ );
+ }
+
+ /**
+ * Given an image style, generate an image.
+ */
+ function createSampleImage($style) {
+ static $file_path;
+
+ // First, we need to make sure we have an image in our testing
+ // file directory. Copy over an image on the first run.
+ if (!isset($file_path)) {
+ $files = $this->drupalGetTestFiles('image');
+ $file = reset($files);
+ $file_path = file_unmanaged_copy($file->uri);
+ }
+
+ return image_style_url($style['name'], $file_path) ? $file_path : FALSE;
+ }
+
+ /**
+ * Count the number of images currently create for a style.
+ */
+ function getImageCount($style) {
+ return count(file_scan_directory('public://styles/' . $style['name'], '/.*/'));
+ }
+
+ /**
+ * Test creating an image style with a numeric name and ensuring it can be
+ * applied to an image.
+ */
+ function testNumericStyleName() {
+ $style_name = rand();
+ $style_label = $this->randomString();
+ $edit = array(
+ 'name' => $style_name,
+ 'label' => $style_label,
+ );
+ $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style'));
+ $this->assertRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.');
+ $options = image_style_options();
+ $this->assertTrue(array_key_exists($style_name, $options), format_string('Array key %key exists.', array('%key' => $style_name)));
+ }
+
+ /**
+ * General test to add a style, add/remove/edit effects to it, then delete it.
+ */
+ function testStyle() {
+ // Setup a style to be created and effects to add to it.
+ $style_name = strtolower($this->randomName(10));
+ $style_label = $this->randomString();
+ $style_path = 'admin/config/media/image-styles/edit/' . $style_name;
+ $effect_edits = array(
+ 'image_resize' => array(
+ 'data[width]' => 100,
+ 'data[height]' => 101,
+ ),
+ 'image_scale' => array(
+ 'data[width]' => 110,
+ 'data[height]' => 111,
+ 'data[upscale]' => 1,
+ ),
+ 'image_scale_and_crop' => array(
+ 'data[width]' => 120,
+ 'data[height]' => 121,
+ ),
+ 'image_crop' => array(
+ 'data[width]' => 130,
+ 'data[height]' => 131,
+ 'data[anchor]' => 'center-center',
+ ),
+ 'image_desaturate' => array(
+ // No options for desaturate.
+ ),
+ 'image_rotate' => array(
+ 'data[degrees]' => 5,
+ 'data[random]' => 1,
+ 'data[bgcolor]' => '#FFFF00',
+ ),
+ );
+
+ // Add style form.
+
+ $edit = array(
+ 'name' => $style_name,
+ 'label' => $style_label,
+ );
+ $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style'));
+ $this->assertRaw(t('Style %name was created.', array('%name' => $style_label)), 'Image style successfully created.');
+
+ // Add effect form.
+
+ // Add each sample effect to the style.
+ foreach ($effect_edits as $effect => $edit) {
+ // Add the effect.
+ $this->drupalPost($style_path, array('new' => $effect), t('Add'));
+ if (!empty($edit)) {
+ $this->drupalPost(NULL, $edit, t('Add effect'));
+ }
+ }
+
+ // Edit effect form.
+
+ // Revisit each form to make sure the effect was saved.
+ drupal_static_reset('image_styles');
+ $style = image_style_load($style_name);
+
+ foreach ($style['effects'] as $ieid => $effect) {
+ $this->drupalGet($style_path . '/effects/' . $ieid);
+ foreach ($effect_edits[$effect['name']] as $field => $value) {
+ $this->assertFieldByName($field, $value, format_string('The %field field in the %effect effect has the correct value of %value.', array('%field' => $field, '%effect' => $effect['name'], '%value' => $value)));
+ }
+ }
+
+ // Image style overview form (ordering and renaming).
+
+ // Confirm the order of effects is maintained according to the order we
+ // added the fields.
+ $effect_edits_order = array_keys($effect_edits);
+ $effects_order = array_values($style['effects']);
+ $order_correct = TRUE;
+ foreach ($effects_order as $index => $effect) {
+ if ($effect_edits_order[$index] != $effect['name']) {
+ $order_correct = FALSE;
+ }
+ }
+ $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.');
+
+ // Test the style overview form.
+ // Change the name of the style and adjust the weights of effects.
+ $style_name = strtolower($this->randomName(10));
+ $style_label = $this->randomString();
+ $weight = count($effect_edits);
+ $edit = array(
+ 'name' => $style_name,
+ 'label' => $style_label,
+ );
+ foreach ($style['effects'] as $ieid => $effect) {
+ $edit['effects[' . $ieid . '][weight]'] = $weight;
+ $weight--;
+ }
+
+ // Create an image to make sure it gets flushed after saving.
+ $image_path = $this->createSampleImage($style);
+ $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path)));
+
+ $this->drupalPost($style_path, $edit, t('Update style'));
+
+ // Note that after changing the style name, the style path is changed.
+ $style_path = 'admin/config/media/image-styles/edit/' . $style_name;
+
+ // Check that the URL was updated.
+ $this->drupalGet($style_path);
+ $this->assertResponse(200, format_string('Image style %original renamed to %new', array('%original' => $style['label'], '%new' => $style_label)));
+
+ // Check that the image was flushed after updating the style.
+ // This is especially important when renaming the style. Make sure that
+ // the old image directory has been deleted.
+ $this->assertEqual($this->getImageCount($style), 0, format_string('Image style %style was flushed after renaming the style and updating the order of effects.', array('%style' => $style['label'])));
+
+ // Load the style by the new name with the new weights.
+ drupal_static_reset('image_styles');
+ $style = image_style_load($style_name, NULL);
+
+ // Confirm the new style order was saved.
+ $effect_edits_order = array_reverse($effect_edits_order);
+ $effects_order = array_values($style['effects']);
+ $order_correct = TRUE;
+ foreach ($effects_order as $index => $effect) {
+ if ($effect_edits_order[$index] != $effect['name']) {
+ $order_correct = FALSE;
+ }
+ }
+ $this->assertTrue($order_correct, 'The order of the effects is correctly set by default.');
+
+ // Image effect deletion form.
+
+ // Create an image to make sure it gets flushed after deleting an effect.
+ $image_path = $this->createSampleImage($style);
+ $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path)));
+
+ // Test effect deletion form.
+ $effect = array_pop($style['effects']);
+ $this->drupalPost($style_path . '/effects/' . $effect['ieid'] . '/delete', array(), t('Delete'));
+ $this->assertRaw(t('The image effect %name has been deleted.', array('%name' => $effect['label'])), 'Image effect deleted.');
+
+ // Style deletion form.
+
+ // Delete the style.
+ $this->drupalPost('admin/config/media/image-styles/delete/' . $style_name, array(), t('Delete'));
+
+ // Confirm the style directory has been removed.
+ $directory = file_default_scheme() . '://styles/' . $style_name;
+ $this->assertFalse(is_dir($directory), format_string('Image style %style directory removed on style deletion.', array('%style' => $style['label'])));
+
+ drupal_static_reset('image_styles');
+ $this->assertFalse(image_style_load($style_name), format_string('Image style %style successfully deleted.', array('%style' => $style['label'])));
+
+ }
+
+ /**
+ * Test to override, edit, then revert a style.
+ */
+ function testDefaultStyle() {
+ // Setup a style to be created and effects to add to it.
+ $style_name = 'thumbnail';
+ $style_label = 'Thumbnail (100x100)';
+ $edit_path = 'admin/config/media/image-styles/edit/' . $style_name;
+ $delete_path = 'admin/config/media/image-styles/delete/' . $style_name;
+ $revert_path = 'admin/config/media/image-styles/revert/' . $style_name;
+
+ // Ensure deleting a default is not possible.
+ $this->drupalGet($delete_path);
+ $this->assertText(t('Page not found'), 'Default styles may not be deleted.');
+
+ // Ensure that editing a default is not possible (without overriding).
+ $this->drupalGet($edit_path);
+ $disabled_field = $this->xpath('//input[@id=:id and @disabled="disabled"]', array(':id' => 'edit-name'));
+ $this->assertTrue($disabled_field, 'Default styles may not be renamed.');
+ $this->assertNoField('edit-submit', 'Default styles may not be edited.');
+ $this->assertNoField('edit-add', 'Default styles may not have new effects added.');
+
+ // Create an image to make sure the default works before overriding.
+ drupal_static_reset('image_styles');
+ $style = image_style_load($style_name);
+ $image_path = $this->createSampleImage($style);
+ $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path)));
+
+ // Verify that effects attached to a default style do not have an ieid key.
+ foreach ($style['effects'] as $effect) {
+ $this->assertFalse(isset($effect['ieid']), format_string('The %effect effect does not have an ieid.', array('%effect' => $effect['name'])));
+ }
+
+ // Override the default.
+ $this->drupalPost($edit_path, array(), t('Override defaults'));
+ $this->assertRaw(t('The %style style has been overridden, allowing you to change its settings.', array('%style' => $style_label)), 'Default image style may be overridden.');
+
+ // Add sample effect to the overridden style.
+ $this->drupalPost($edit_path, array('new' => 'image_desaturate'), t('Add'));
+ drupal_static_reset('image_styles');
+ $style = image_style_load($style_name);
+
+ // Verify that effects attached to the style have an ieid now.
+ foreach ($style['effects'] as $effect) {
+ $this->assertTrue(isset($effect['ieid']), format_string('The %effect effect has an ieid.', array('%effect' => $effect['name'])));
+ }
+
+ // The style should now have 2 effect, the original scale provided by core
+ // and the desaturate effect we added in the override.
+ $effects = array_values($style['effects']);
+ $this->assertEqual($effects[0]['name'], 'image_scale', 'The default effect still exists in the overridden style.');
+ $this->assertEqual($effects[1]['name'], 'image_desaturate', 'The added effect exists in the overridden style.');
+
+ // Check that we are able to rename an overridden style.
+ $this->drupalGet($edit_path);
+ $disabled_field = $this->xpath('//input[@id=:id and @disabled="disabled"]', array(':id' => 'edit-name'));
+ $this->assertFalse($disabled_field, 'Overridden styles may be renamed.');
+
+ // Create an image to ensure the override works properly.
+ $image_path = $this->createSampleImage($style);
+ $this->assertEqual($this->getImageCount($style), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['label'], '%file' => $image_path)));
+
+ // Revert the image style.
+ $this->drupalPost($revert_path, array(), t('Revert'));
+ drupal_static_reset('image_styles');
+ $style = image_style_load($style_name);
+
+ // The style should now have the single effect for scale.
+ $effects = array_values($style['effects']);
+ $this->assertEqual($effects[0]['name'], 'image_scale', 'The default effect still exists in the reverted style.');
+ $this->assertFalse(array_key_exists(1, $effects), 'The added effect has been removed in the reverted style.');
+ }
+
+ /**
+ * Test deleting a style and choosing a replacement style.
+ */
+ function testStyleReplacement() {
+ // Create a new style.
+ $style_name = strtolower($this->randomName(10));
+ $style_label = $this->randomString();
+ image_style_save(array('name' => $style_name, 'label' => $style_label));
+ $style_path = 'admin/config/media/image-styles/edit/' . $style_name;
+
+ // Create an image field that uses the new style.
+ $field_name = strtolower($this->randomName(10));
+ $this->createImageField($field_name, 'article');
+ $instance = field_info_instance('node', $field_name, 'article');
+ $instance['display']['default']['type'] = 'image';
+ $instance['display']['default']['settings']['image_style'] = $style_name;
+ field_update_instance($instance);
+
+ // Create a new node with an image attached.
+ $test_image = current($this->drupalGetTestFiles('image'));
+ $nid = $this->uploadNodeImage($test_image, $field_name, 'article');
+ $node = node_load($nid);
+
+ // Test that image is displayed using newly created style.
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw(check_plain(image_style_url($style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style @style.', array('@style' => $style_name)));
+
+ // Rename the style and make sure the image field is updated.
+ $new_style_name = strtolower($this->randomName(10));
+ $new_style_label = $this->randomString();
+ $edit = array(
+ 'name' => $new_style_name,
+ 'label' => $new_style_label,
+ );
+ $this->drupalPost('admin/config/media/image-styles/edit/' . $style_name, $edit, t('Update style'));
+ $this->assertText(t('Changes to the style have been saved.'), format_string('Style %name was renamed to %new_name.', array('%name' => $style_name, '%new_name' => $new_style_name)));
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw(check_plain(image_style_url($new_style_name, $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style replacement style.'));
+
+ // Delete the style and choose a replacement style.
+ $edit = array(
+ 'replacement' => 'thumbnail',
+ );
+ $this->drupalPost('admin/config/media/image-styles/delete/' . $new_style_name, $edit, t('Delete'));
+ $message = t('Style %name was deleted.', array('%name' => $new_style_label));
+ $this->assertRaw($message, $message);
+
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw(check_plain(image_style_url('thumbnail', $node->{$field_name}[LANGUAGE_NONE][0]['uri'])), format_string('Image displayed using style replacement style.'));
+ }
+}
+
+/**
+ * Test class to check that formatters and display settings are working.
+ */
+class ImageFieldDisplayTestCase extends ImageFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image field display tests',
+ 'description' => 'Test the display of image fields.',
+ 'group' => 'Image',
+ );
+ }
+
+ /**
+ * Test image formatters on node display for public files.
+ */
+ function testImageFieldFormattersPublic() {
+ $this->_testImageFieldFormatters('public');
+ }
+
+ /**
+ * Test image formatters on node display for private files.
+ */
+ function testImageFieldFormattersPrivate() {
+ // Remove access content permission from anonymous users.
+ user_role_change_permissions(DRUPAL_ANONYMOUS_RID, array('access content' => FALSE));
+ $this->_testImageFieldFormatters('private');
+ }
+
+ /**
+ * Test image formatters on node display.
+ */
+ function _testImageFieldFormatters($scheme) {
+ $field_name = strtolower($this->randomName());
+ $this->createImageField($field_name, 'article', array('uri_scheme' => $scheme));
+ // Create a new node with an image attached.
+ $test_image = current($this->drupalGetTestFiles('image'));
+ $nid = $this->uploadNodeImage($test_image, $field_name, 'article');
+ $node = node_load($nid, NULL, TRUE);
+
+ // Test that the default formatter is being used.
+ $image_uri = $node->{$field_name}[LANGUAGE_NONE][0]['uri'];
+ $image_info = array(
+ 'path' => $image_uri,
+ 'width' => 40,
+ 'height' => 20,
+ );
+ $default_output = theme('image', $image_info);
+ $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.');
+
+ // Test the image linked to file formatter.
+ $instance = field_info_instance('node', $field_name, 'article');
+ $instance['display']['default']['type'] = 'image';
+ $instance['display']['default']['settings']['image_link'] = 'file';
+ field_update_instance($instance);
+ $default_output = l(theme('image', $image_info), file_create_url($image_uri), array('html' => TRUE));
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw($default_output, 'Image linked to file formatter displaying correctly on full node view.');
+ // Verify that the image can be downloaded.
+ $this->assertEqual(file_get_contents($test_image->uri), $this->drupalGet(file_create_url($image_uri)), 'File was downloaded successfully.');
+ if ($scheme == 'private') {
+ // Only verify HTTP headers when using private scheme and the headers are
+ // sent by Drupal.
+ $this->assertEqual($this->drupalGetHeader('Content-Type'), 'image/png', 'Content-Type header was sent.');
+ $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'private', 'Cache-Control header was sent.');
+
+ // Log out and try to access the file.
+ $this->drupalLogout();
+ $this->drupalGet(file_create_url($image_uri));
+ $this->assertResponse('403', 'Access denied to original image as anonymous user.');
+
+ // Log in again.
+ $this->drupalLogin($this->admin_user);
+ }
+
+ // Test the image linked to content formatter.
+ $instance['display']['default']['settings']['image_link'] = 'content';
+ field_update_instance($instance);
+ $default_output = l(theme('image', $image_info), 'node/' . $nid, array('html' => TRUE, 'attributes' => array('class' => 'active')));
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw($default_output, 'Image linked to content formatter displaying correctly on full node view.');
+
+ // Test the image style 'thumbnail' formatter.
+ $instance['display']['default']['settings']['image_link'] = '';
+ $instance['display']['default']['settings']['image_style'] = 'thumbnail';
+ field_update_instance($instance);
+ // Ensure the derivative image is generated so we do not have to deal with
+ // image style callback paths.
+ $this->drupalGet(image_style_url('thumbnail', $image_uri));
+ // Need to create the URL again since it will change if clean URLs
+ // are disabled.
+ $image_info['path'] = image_style_url('thumbnail', $image_uri);
+ $image_info['width'] = 100;
+ $image_info['height'] = 50;
+ $default_output = theme('image', $image_info);
+ $this->drupalGet('node/' . $nid);
+ $this->assertRaw($default_output, 'Image style thumbnail formatter displaying correctly on full node view.');
+
+ if ($scheme == 'private') {
+ // Log out and try to access the file.
+ $this->drupalLogout();
+ $this->drupalGet(image_style_url('thumbnail', $image_uri));
+ $this->assertResponse('403', 'Access denied to image style thumbnail as anonymous user.');
+ }
+ }
+
+ /**
+ * Tests for image field settings.
+ */
+ function testImageFieldSettings() {
+ $test_image = current($this->drupalGetTestFiles('image'));
+ list(, $test_image_extension) = explode('.', $test_image->filename);
+ $field_name = strtolower($this->randomName());
+ $instance_settings = array(
+ 'alt_field' => 1,
+ 'file_extensions' => $test_image_extension,
+ 'max_filesize' => '50 KB',
+ 'max_resolution' => '100x100',
+ 'min_resolution' => '10x10',
+ 'title_field' => 1,
+ );
+ $widget_settings = array(
+ 'preview_image_style' => 'medium',
+ );
+ $field = $this->createImageField($field_name, 'article', array(), $instance_settings, $widget_settings);
+ $field['deleted'] = 0;
+ $table = _field_sql_storage_tablename($field);
+ $schema = drupal_get_schema($table, TRUE);
+ $instance = field_info_instance('node', $field_name, 'article');
+
+ $this->drupalGet('node/add/article');
+ $this->assertText(t('Files must be less than 50 KB.'), 'Image widget max file size is displayed on article form.');
+ $this->assertText(t('Allowed file types: ' . $test_image_extension . '.'), 'Image widget allowed file types displayed on article form.');
+ $this->assertText(t('Images must be between 10x10 and 100x100 pixels.'), 'Image widget allowed resolution displayed on article form.');
+
+ // We have to create the article first and then edit it because the alt
+ // and title fields do not display until the image has been attached.
+ $nid = $this->uploadNodeImage($test_image, $field_name, 'article');
+ $this->drupalGet('node/' . $nid . '/edit');
+ $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][alt]', '', 'Alt field displayed on article form.');
+ $this->assertFieldByName($field_name . '[' . LANGUAGE_NONE . '][0][title]', '', 'Title field displayed on article form.');
+ // Verify that the attached image is being previewed using the 'medium'
+ // style.
+ $node = node_load($nid, NULL, TRUE);
+ $image_info = array(
+ 'path' => image_style_url('medium', $node->{$field_name}[LANGUAGE_NONE][0]['uri']),
+ 'width' => 220,
+ 'height' => 110,
+ );
+ $default_output = theme('image', $image_info);
+ $this->assertRaw($default_output, "Preview image is displayed using 'medium' style.");
+
+ // Add alt/title fields to the image and verify that they are displayed.
+ $image_info = array(
+ 'path' => $node->{$field_name}[LANGUAGE_NONE][0]['uri'],
+ 'alt' => $this->randomName(),
+ 'title' => $this->randomName(),
+ 'width' => 40,
+ 'height' => 20,
+ );
+ $edit = array(
+ $field_name . '[' . LANGUAGE_NONE . '][0][alt]' => $image_info['alt'],
+ $field_name . '[' . LANGUAGE_NONE . '][0][title]' => $image_info['title'],
+ );
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $default_output = theme('image', $image_info);
+ $this->assertRaw($default_output, 'Image displayed using user supplied alt and title attributes.');
+
+ // Verify that alt/title longer than allowed results in a validation error.
+ $test_size = 2000;
+ $edit = array(
+ $field_name . '[' . LANGUAGE_NONE . '][0][alt]' => $this->randomName($test_size),
+ $field_name . '[' . LANGUAGE_NONE . '][0][title]' => $this->randomName($test_size),
+ );
+ $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save'));
+ $this->assertRaw(t('Alternate text cannot be longer than %max characters but is currently %length characters long.', array(
+ '%max' => $schema['fields'][$field_name .'_alt']['length'],
+ '%length' => $test_size,
+ )));
+ $this->assertRaw(t('Title cannot be longer than %max characters but is currently %length characters long.', array(
+ '%max' => $schema['fields'][$field_name .'_title']['length'],
+ '%length' => $test_size,
+ )));
+ }
+
+ /**
+ * Test passing attributes into the image field formatters.
+ */
+ function testImageFieldFormatterAttributes() {
+ $image = theme('image_formatter', array(
+ 'item' => array(
+ 'uri' => 'http://example.com/example.png',
+ 'attributes' => array(
+ 'data-image-field-formatter' => 'testFound',
+ ),
+ 'alt' => t('Image field formatter attribute test.'),
+ 'title' => t('Image field formatter'),
+ ),
+ ));
+ $this->assertTrue(stripos($image, 'testFound') > 0, 'Image field formatters can have attributes.');
+ }
+
+ /**
+ * Test use of a default image with an image field.
+ */
+ function testImageFieldDefaultImage() {
+ // Create a new image field.
+ $field_name = strtolower($this->randomName());
+ $this->createImageField($field_name, 'article');
+
+ // Create a new node, with no images and verify that no images are
+ // displayed.
+ $node = $this->drupalCreateNode(array('type' => 'article'));
+ $this->drupalGet('node/' . $node->nid);
+ // Verify that no image is displayed on the page by checking for the class
+ // that would be used on the image field.
+ $this->assertNoPattern('
', 'No image displayed when no image is attached and no default image specified.');
+
+ // Add a default image to the public imagefield instance.
+ $images = $this->drupalGetTestFiles('image');
+ $edit = array(
+ 'files[field_settings_default_image]' => drupal_realpath($images[0]->uri),
+ );
+ $this->drupalPost('admin/structure/types/manage/article/fields/' . $field_name, $edit, t('Save settings'));
+ // Clear field info cache so the new default image is detected.
+ field_info_cache_clear();
+ $field = field_info_field($field_name);
+ $image = file_load($field['settings']['default_image']);
+ $this->assertTrue($image->status == FILE_STATUS_PERMANENT, 'The default image status is permanent.');
+ $default_output = theme('image', array('path' => $image->uri));
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertRaw($default_output, 'Default image displayed when no user supplied image is present.');
+
+ // Create a node with an image attached and ensure that the default image
+ // is not displayed.
+ $nid = $this->uploadNodeImage($images[1], $field_name, 'article');
+ $node = node_load($nid, NULL, TRUE);
+ $image_info = array(
+ 'path' => $node->{$field_name}[LANGUAGE_NONE][0]['uri'],
+ 'width' => 40,
+ 'height' => 20,
+ );
+ $image_output = theme('image', $image_info);
+ $this->drupalGet('node/' . $nid);
+ $this->assertNoRaw($default_output, 'Default image is not displayed when user supplied image is present.');
+ $this->assertRaw($image_output, 'User supplied image is displayed.');
+
+ // Remove default image from the field and make sure it is no longer used.
+ $edit = array(
+ 'field[settings][default_image][fid]' => 0,
+ );
+ $this->drupalPost('admin/structure/types/manage/article/fields/' . $field_name, $edit, t('Save settings'));
+ // Clear field info cache so the new default image is detected.
+ field_info_cache_clear();
+ $field = field_info_field($field_name);
+ $this->assertFalse($field['settings']['default_image'], 'Default image removed from field.');
+ // Create an image field that uses the private:// scheme and test that the
+ // default image works as expected.
+ $private_field_name = strtolower($this->randomName());
+ $this->createImageField($private_field_name, 'article', array('uri_scheme' => 'private'));
+ // Add a default image to the new field.
+ $edit = array(
+ 'files[field_settings_default_image]' => drupal_realpath($images[1]->uri),
+ );
+ $this->drupalPost('admin/structure/types/manage/article/fields/' . $private_field_name, $edit, t('Save settings'));
+ $private_field = field_info_field($private_field_name);
+ $image = file_load($private_field['settings']['default_image']);
+ $this->assertEqual('private', file_uri_scheme($image->uri), 'Default image uses private:// scheme.');
+ $this->assertTrue($image->status == FILE_STATUS_PERMANENT, 'The default image status is permanent.');
+ // Create a new node with no image attached and ensure that default private
+ // image is displayed.
+ $node = $this->drupalCreateNode(array('type' => 'article'));
+ $default_output = theme('image', array('path' => $image->uri));
+ $this->drupalGet('node/' . $node->nid);
+ $this->assertRaw($default_output, 'Default private image displayed when no user supplied image is present.');
+ }
+}
+
+/**
+ * Test class to check for various validations.
+ */
+class ImageFieldValidateTestCase extends ImageFieldTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image field validation tests',
+ 'description' => 'Tests validation functions such as min/max resolution.',
+ 'group' => 'Image',
+ );
+ }
+
+ /**
+ * Test min/max resolution settings.
+ */
+ function testResolution() {
+ $field_name = strtolower($this->randomName());
+ $min_resolution = 50;
+ $max_resolution = 100;
+ $instance_settings = array(
+ 'max_resolution' => $max_resolution . 'x' . $max_resolution,
+ 'min_resolution' => $min_resolution . 'x' . $min_resolution,
+ );
+ $this->createImageField($field_name, 'article', array(), $instance_settings);
+
+ // We want a test image that is too small, and a test image that is too
+ // big, so cycle through test image files until we have what we need.
+ $image_that_is_too_big = FALSE;
+ $image_that_is_too_small = FALSE;
+ foreach ($this->drupalGetTestFiles('image') as $image) {
+ $info = image_get_info($image->uri);
+ if ($info['width'] > $max_resolution) {
+ $image_that_is_too_big = $image;
+ }
+ if ($info['width'] < $min_resolution) {
+ $image_that_is_too_small = $image;
+ }
+ if ($image_that_is_too_small && $image_that_is_too_big) {
+ break;
+ }
+ }
+ $nid = $this->uploadNodeImage($image_that_is_too_small, $field_name, 'article');
+ $this->assertText(t('The specified file ' . $image_that_is_too_small->filename . ' could not be uploaded. The image is too small; the minimum dimensions are 50x50 pixels.'), 'Node save failed when minimum image resolution was not met.');
+ $nid = $this->uploadNodeImage($image_that_is_too_big, $field_name, 'article');
+ $this->assertText(t('The image was resized to fit within the maximum allowed dimensions of 100x100 pixels.'), 'Image exceeding max resolution was properly resized.');
+ }
+}
+
+/**
+ * Tests that images have correct dimensions when styled.
+ */
+class ImageDimensionsTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image dimensions',
+ 'description' => 'Tests that images have correct dimensions when styled.',
+ 'group' => 'Image',
+ );
+ }
+
+ function setUp() {
+ parent::setUp('image_module_test');
+ }
+
+ /**
+ * Test styled image dimensions cumulatively.
+ */
+ function testImageDimensions() {
+ // Create a working copy of the file.
+ $files = $this->drupalGetTestFiles('image');
+ $file = reset($files);
+ $original_uri = file_unmanaged_copy($file->uri, 'public://', FILE_EXISTS_RENAME);
+
+ // Create a style.
+ $style = image_style_save(array('name' => 'test', 'label' => 'Test'));
+ $generated_uri = 'public://styles/test/public/'. drupal_basename($original_uri);
+ $url = image_style_url('test', $original_uri);
+
+ $variables = array(
+ 'style_name' => 'test',
+ 'path' => $original_uri,
+ 'width' => 40,
+ 'height' => 20,
+ );
+
+ // Scale an image that is wider than it is high.
+ $effect = array(
+ 'name' => 'image_scale',
+ 'data' => array(
+ 'width' => 120,
+ 'height' => 90,
+ 'upscale' => TRUE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 120, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 60, 'Expected height was found.');
+
+ // Rotate 90 degrees anticlockwise.
+ $effect = array(
+ 'name' => 'image_rotate',
+ 'data' => array(
+ 'degrees' => -90,
+ 'random' => FALSE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 60, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 120, 'Expected height was found.');
+
+ // Scale an image that is higher than it is wide (rotated by previous effect).
+ $effect = array(
+ 'name' => 'image_scale',
+ 'data' => array(
+ 'width' => 120,
+ 'height' => 90,
+ 'upscale' => TRUE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 45, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 90, 'Expected height was found.');
+
+ // Test upscale disabled.
+ $effect = array(
+ 'name' => 'image_scale',
+ 'data' => array(
+ 'width' => 400,
+ 'height' => 200,
+ 'upscale' => FALSE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 45, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 90, 'Expected height was found.');
+
+ // Add a desaturate effect.
+ $effect = array(
+ 'name' => 'image_desaturate',
+ 'data' => array(),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 45, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 90, 'Expected height was found.');
+
+ // Add a random rotate effect.
+ $effect = array(
+ 'name' => 'image_rotate',
+ 'data' => array(
+ 'degrees' => 180,
+ 'random' => TRUE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+
+
+ // Add a crop effect.
+ $effect = array(
+ 'name' => 'image_crop',
+ 'data' => array(
+ 'width' => 30,
+ 'height' => 30,
+ 'anchor' => 'center-center',
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+ $image_info = image_get_info($generated_uri);
+ $this->assertEqual($image_info['width'], 30, 'Expected width was found.');
+ $this->assertEqual($image_info['height'], 30, 'Expected height was found.');
+
+ // Rotate to a non-multiple of 90 degrees.
+ $effect = array(
+ 'name' => 'image_rotate',
+ 'data' => array(
+ 'degrees' => 57,
+ 'random' => FALSE,
+ ),
+ 'isid' => $style['isid'],
+ );
+
+ $effect = image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ $this->assertFalse(file_exists($generated_uri), 'Generated file does not exist.');
+ $this->drupalGet($url);
+ $this->assertResponse(200, 'Image was generated at the URL.');
+ $this->assertTrue(file_exists($generated_uri), 'Generated file does exist after we accessed it.');
+
+ image_effect_delete($effect);
+
+ // Ensure that an effect with no dimensions callback unsets the dimensions.
+ // This ensures compatibility with 7.0 contrib modules.
+ $effect = array(
+ 'name' => 'image_module_test_null',
+ 'data' => array(),
+ 'isid' => $style['isid'],
+ );
+
+ image_effect_save($effect);
+ $img_tag = theme_image_style($variables);
+ $this->assertEqual($img_tag, '', 'Expected img tag was found.');
+ }
+}
+
+/**
+ * Tests image_dimensions_scale().
+ */
+class ImageDimensionsScaleTestCase extends DrupalUnitTestCase {
+ public static function getInfo() {
+ return array(
+ 'name' => 'image_dimensions_scale()',
+ 'description' => 'Tests all control flow branches in image_dimensions_scale().',
+ 'group' => 'Image',
+ );
+ }
+
+ /**
+ * Tests all control flow branches in image_dimensions_scale().
+ */
+ function testImageDimensionsScale() {
+ // Define input / output datasets to test different branch conditions.
+ $test = array();
+
+ // Test branch conditions:
+ // - No height.
+ // - Upscale, don't need to upscale.
+ $tests[] = array(
+ 'input' => array(
+ 'dimensions' => array(
+ 'width' => 1000,
+ 'height' => 2000,
+ ),
+ 'width' => 200,
+ 'height' => NULL,
+ 'upscale' => TRUE,
+ ),
+ 'output' => array(
+ 'dimensions' => array(
+ 'width' => 200,
+ 'height' => 400,
+ ),
+ 'return_value' => TRUE,
+ ),
+ );
+
+ // Test branch conditions:
+ // - No width.
+ // - Don't upscale, don't need to upscale.
+ $tests[] = array(
+ 'input' => array(
+ 'dimensions' => array(
+ 'width' => 1000,
+ 'height' => 800,
+ ),
+ 'width' => NULL,
+ 'height' => 140,
+ 'upscale' => FALSE,
+ ),
+ 'output' => array(
+ 'dimensions' => array(
+ 'width' => 175,
+ 'height' => 140,
+ ),
+ 'return_value' => TRUE,
+ ),
+ );
+
+ // Test branch conditions:
+ // - Source aspect ratio greater than target.
+ // - Upscale, need to upscale.
+ $tests[] = array(
+ 'input' => array(
+ 'dimensions' => array(
+ 'width' => 8,
+ 'height' => 20,
+ ),
+ 'width' => 200,
+ 'height' => 140,
+ 'upscale' => TRUE,
+ ),
+ 'output' => array(
+ 'dimensions' => array(
+ 'width' => 56,
+ 'height' => 140,
+ ),
+ 'return_value' => TRUE,
+ ),
+ );
+
+ // Test branch condition: target aspect ratio greater than source.
+ $tests[] = array(
+ 'input' => array(
+ 'dimensions' => array(
+ 'width' => 2000,
+ 'height' => 800,
+ ),
+ 'width' => 200,
+ 'height' => 140,
+ 'upscale' => FALSE,
+ ),
+ 'output' => array(
+ 'dimensions' => array(
+ 'width' => 200,
+ 'height' => 80,
+ ),
+ 'return_value' => TRUE,
+ ),
+ );
+
+ // Test branch condition: don't upscale, need to upscale.
+ $tests[] = array(
+ 'input' => array(
+ 'dimensions' => array(
+ 'width' => 100,
+ 'height' => 50,
+ ),
+ 'width' => 200,
+ 'height' => 140,
+ 'upscale' => FALSE,
+ ),
+ 'output' => array(
+ 'dimensions' => array(
+ 'width' => 100,
+ 'height' => 50,
+ ),
+ 'return_value' => FALSE,
+ ),
+ );
+
+ foreach ($tests as $test) {
+ // Process the test dataset.
+ $return_value = image_dimensions_scale($test['input']['dimensions'], $test['input']['width'], $test['input']['height'], $test['input']['upscale']);
+
+ // Check the width.
+ $this->assertEqual($test['output']['dimensions']['width'], $test['input']['dimensions']['width'], format_string('Computed width (@computed_width) equals expected width (@expected_width)', array('@computed_width' => $test['output']['dimensions']['width'], '@expected_width' => $test['input']['dimensions']['width'])));
+
+ // Check the height.
+ $this->assertEqual($test['output']['dimensions']['height'], $test['input']['dimensions']['height'], format_string('Computed height (@computed_height) equals expected height (@expected_height)', array('@computed_height' => $test['output']['dimensions']['height'], '@expected_height' => $test['input']['dimensions']['height'])));
+
+ // Check the return value.
+ $this->assertEqual($test['output']['return_value'], $return_value, 'Correct return value.');
+ }
+ }
+}
+
+/**
+ * Tests default image settings.
+ */
+class ImageFieldDefaultImagesTestCase extends ImageFieldTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image field default images tests',
+ 'description' => 'Tests setting up default images both to the field and field instance.',
+ 'group' => 'Image',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('field_ui'));
+ }
+
+ /**
+ * Tests CRUD for fields and fields instances with default images.
+ */
+ function testDefaultImages() {
+ // Create files to use as the default images.
+ $files = $this->drupalGetTestFiles('image');
+ $default_images = array();
+ foreach (array('field', 'instance', 'instance2', 'field_new', 'instance_new') as $image_target) {
+ $file = array_pop($files);
+ $file = file_save($file);
+ $default_images[$image_target] = $file;
+ }
+
+ // Create an image field and add an instance to the article content type.
+ $field_name = strtolower($this->randomName());
+ $field_settings = array(
+ 'default_image' => $default_images['field']->fid,
+ );
+ $instance_settings = array(
+ 'default_image' => $default_images['instance']->fid,
+ );
+ $widget_settings = array(
+ 'preview_image_style' => 'medium',
+ );
+ $this->createImageField($field_name, 'article', $field_settings, $instance_settings, $widget_settings);
+ $field = field_info_field($field_name);
+ $instance = field_info_instance('node', $field_name, 'article');
+
+ // Add another instance with another default image to the page content type.
+ $instance2 = array_merge($instance, array(
+ 'bundle' => 'page',
+ 'settings' => array(
+ 'default_image' => $default_images['instance2']->fid,
+ ),
+ ));
+ field_create_instance($instance2);
+ $instance2 = field_info_instance('node', $field_name, 'page');
+
+
+ // Confirm the defaults are present on the article field admin form.
+ $this->drupalGet("admin/structure/types/manage/article/fields/$field_name");
+ $this->assertFieldByXpath(
+ '//input[@name="field[settings][default_image][fid]"]',
+ $default_images['field']->fid,
+ format_string(
+ 'Article image field default equals expected file ID of @fid.',
+ array('@fid' => $default_images['field']->fid)
+ )
+ );
+ $this->assertFieldByXpath(
+ '//input[@name="instance[settings][default_image][fid]"]',
+ $default_images['instance']->fid,
+ format_string(
+ 'Article image field instance default equals expected file ID of @fid.',
+ array('@fid' => $default_images['instance']->fid)
+ )
+ );
+
+ // Confirm the defaults are present on the page field admin form.
+ $this->drupalGet("admin/structure/types/manage/page/fields/$field_name");
+ $this->assertFieldByXpath(
+ '//input[@name="field[settings][default_image][fid]"]',
+ $default_images['field']->fid,
+ format_string(
+ 'Page image field default equals expected file ID of @fid.',
+ array('@fid' => $default_images['field']->fid)
+ )
+ );
+ $this->assertFieldByXpath(
+ '//input[@name="instance[settings][default_image][fid]"]',
+ $default_images['instance2']->fid,
+ format_string(
+ 'Page image field instance default equals expected file ID of @fid.',
+ array('@fid' => $default_images['instance2']->fid)
+ )
+ );
+
+ // Confirm that the image default is shown for a new article node.
+ $article = $this->drupalCreateNode(array('type' => 'article'));
+ $article_built = node_view($article);
+ $this->assertEqual(
+ $article_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance']->fid,
+ format_string(
+ 'A new article node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance']->fid)
+ )
+ );
+
+ // Confirm that the image default is shown for a new page node.
+ $page = $this->drupalCreateNode(array('type' => 'page'));
+ $page_built = node_view($page);
+ $this->assertEqual(
+ $page_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance2']->fid,
+ format_string(
+ 'A new page node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance2']->fid)
+ )
+ );
+
+ // Upload a new default for the field.
+ $field['settings']['default_image'] = $default_images['field_new']->fid;
+ field_update_field($field);
+
+ // Confirm that the new field default is used on the article admin form.
+ $this->drupalGet("admin/structure/types/manage/article/fields/$field_name");
+ $this->assertFieldByXpath(
+ '//input[@name="field[settings][default_image][fid]"]',
+ $default_images['field_new']->fid,
+ format_string(
+ 'Updated image field default equals expected file ID of @fid.',
+ array('@fid' => $default_images['field_new']->fid)
+ )
+ );
+
+ // Reload the nodes and confirm the field instance defaults are used.
+ $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE));
+ $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE));
+ $this->assertEqual(
+ $article_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance']->fid,
+ format_string(
+ 'An existing article node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance']->fid)
+ )
+ );
+ $this->assertEqual(
+ $page_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance2']->fid,
+ format_string(
+ 'An existing page node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance2']->fid)
+ )
+ );
+
+ // Upload a new default for the article's field instance.
+ $instance['settings']['default_image'] = $default_images['instance_new']->fid;
+ field_update_instance($instance);
+
+ // Confirm the new field instance default is used on the article field
+ // admin form.
+ $this->drupalGet("admin/structure/types/manage/article/fields/$field_name");
+ $this->assertFieldByXpath(
+ '//input[@name="instance[settings][default_image][fid]"]',
+ $default_images['instance_new']->fid,
+ format_string(
+ 'Updated article image field instance default equals expected file ID of @fid.',
+ array('@fid' => $default_images['instance_new']->fid)
+ )
+ );
+
+ // Reload the nodes.
+ $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE));
+ $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE));
+
+ // Confirm the article uses the new default.
+ $this->assertEqual(
+ $article_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance_new']->fid,
+ format_string(
+ 'An existing article node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance_new']->fid)
+ )
+ );
+ // Confirm the page remains unchanged.
+ $this->assertEqual(
+ $page_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance2']->fid,
+ format_string(
+ 'An existing page node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance2']->fid)
+ )
+ );
+
+ // Remove the instance default from articles.
+ $instance['settings']['default_image'] = NULL;
+ field_update_instance($instance);
+
+ // Confirm the article field instance default has been removed.
+ $this->drupalGet("admin/structure/types/manage/article/fields/$field_name");
+ $this->assertFieldByXpath(
+ '//input[@name="instance[settings][default_image][fid]"]',
+ '',
+ 'Updated article image field instance default has been successfully removed.'
+ );
+
+ // Reload the nodes.
+ $article_built = node_view($article = node_load($article->nid, NULL, $reset = TRUE));
+ $page_built = node_view($page = node_load($page->nid, NULL, $reset = TRUE));
+ // Confirm the article uses the new field (not instance) default.
+ $this->assertEqual(
+ $article_built[$field_name]['#items'][0]['fid'],
+ $default_images['field_new']->fid,
+ format_string(
+ 'An existing article node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['field_new']->fid)
+ )
+ );
+ // Confirm the page remains unchanged.
+ $this->assertEqual(
+ $page_built[$field_name]['#items'][0]['fid'],
+ $default_images['instance2']->fid,
+ format_string(
+ 'An existing page node without an image has the expected default image file ID of @fid.',
+ array('@fid' => $default_images['instance2']->fid)
+ )
+ );
+ }
+
+}
+
+/**
+ * Tests image theme functions.
+ */
+class ImageThemeFunctionWebTestCase extends DrupalWebTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image theme functions',
+ 'description' => 'Test that the image theme functions work correctly.',
+ 'group' => 'Image',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('image'));
+ }
+
+ /**
+ * Tests usage of the image field formatters.
+ */
+ function testImageFormatterTheme() {
+ // Create an image.
+ $files = $this->drupalGetTestFiles('image');
+ $file = reset($files);
+ $original_uri = file_unmanaged_copy($file->uri, 'public://', FILE_EXISTS_RENAME);
+
+ // Create a style.
+ image_style_save(array('name' => 'test', 'label' => 'Test'));
+ $url = image_style_url('test', $original_uri);
+
+ // Test using theme_image_formatter() without an image title, alt text, or
+ // link options.
+ $path = $this->randomName();
+ $element = array(
+ '#theme' => 'image_formatter',
+ '#image_style' => 'test',
+ '#item' => array(
+ 'uri' => $original_uri,
+ ),
+ '#path' => array(
+ 'path' => $path,
+ ),
+ );
+ $rendered_element = render($element);
+ $expected_result = '';
+ $this->assertEqual($expected_result, $rendered_element, 'theme_image_formatter() correctly renders without title, alt, or path options.');
+
+ // Link the image to a fragment on the page, and not a full URL.
+ $fragment = $this->randomName();
+ $element['#path']['path'] = '';
+ $element['#path']['options'] = array(
+ 'external' => TRUE,
+ 'fragment' => $fragment,
+ );
+ $rendered_element = render($element);
+ $expected_result = '';
+ $this->assertEqual($expected_result, $rendered_element, 'theme_image_formatter() correctly renders a link fragment.');
+ }
+
+}
+
+/**
+ * Tests flushing of image styles.
+ */
+class ImageStyleFlushTest extends ImageFieldTestCase {
+
+ public static function getInfo() {
+ return array(
+ 'name' => 'Image style flushing',
+ 'description' => 'Tests flushing of image styles.',
+ 'group' => 'Image',
+ );
+ }
+
+ /**
+ * Given an image style and a wrapper, generate an image.
+ */
+ function createSampleImage($style, $wrapper) {
+ static $file;
+
+ if (!isset($file)) {
+ $files = $this->drupalGetTestFiles('image');
+ $file = reset($files);
+ }
+
+ // Make sure we have an image in our wrapper testing file directory.
+ $source_uri = file_unmanaged_copy($file->uri, $wrapper . '://');
+ // Build the derivative image.
+ $derivative_uri = image_style_path($style['name'], $source_uri);
+ $derivative = image_style_create_derivative($style, $source_uri, $derivative_uri);
+
+ return $derivative ? $derivative_uri : FALSE;
+ }
+
+ /**
+ * Count the number of images currently created for a style in a wrapper.
+ */
+ function getImageCount($style, $wrapper) {
+ return count(file_scan_directory($wrapper . '://styles/' . $style['name'], '/.*/'));
+ }
+
+ /**
+ * General test to flush a style.
+ */
+ function testFlush() {
+
+ // Setup a style to be created and effects to add to it.
+ $style_name = strtolower($this->randomName(10));
+ $style_label = $this->randomString();
+ $style_path = 'admin/config/media/image-styles/edit/' . $style_name;
+ $effect_edits = array(
+ 'image_resize' => array(
+ 'data[width]' => 100,
+ 'data[height]' => 101,
+ ),
+ 'image_scale' => array(
+ 'data[width]' => 110,
+ 'data[height]' => 111,
+ 'data[upscale]' => 1,
+ ),
+ );
+
+ // Add style form.
+ $edit = array(
+ 'name' => $style_name,
+ 'label' => $style_label,
+ );
+ $this->drupalPost('admin/config/media/image-styles/add', $edit, t('Create new style'));
+ // Add each sample effect to the style.
+ foreach ($effect_edits as $effect => $edit) {
+ // Add the effect.
+ $this->drupalPost($style_path, array('new' => $effect), t('Add'));
+ if (!empty($edit)) {
+ $this->drupalPost(NULL, $edit, t('Add effect'));
+ }
+ }
+
+ // Load the saved image style.
+ $style = image_style_load($style_name);
+
+ // Create an image for the 'public' wrapper.
+ $image_path = $this->createSampleImage($style, 'public');
+ // Expecting to find 2 images, one is the sample.png image shown in
+ // image style preview.
+ $this->assertEqual($this->getImageCount($style, 'public'), 2, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path)));
+
+ // Create an image for the 'private' wrapper.
+ $image_path = $this->createSampleImage($style, 'private');
+ $this->assertEqual($this->getImageCount($style, 'private'), 1, format_string('Image style %style image %file successfully generated.', array('%style' => $style['name'], '%file' => $image_path)));
+
+ // Remove the 'image_scale' effect and updates the style, which in turn
+ // forces an image style flush.
+ $effect = array_pop($style['effects']);
+ $this->drupalPost($style_path . '/effects/' . $effect['ieid'] . '/delete', array(), t('Delete'));
+ $this->assertResponse(200);
+ $this->drupalPost($style_path, array(), t('Update style'));
+ $this->assertResponse(200);
+
+ // Post flush, expected 1 image in the 'public' wrapper (sample.png).
+ $this->assertEqual($this->getImageCount($style, 'public'), 1, format_string('Image style %style flushed correctly for %wrapper wrapper.', array('%style' => $style['name'], '%wrapper' => 'public')));
+
+ // Post flush, expected no image in the 'private' wrapper.
+ $this->assertEqual($this->getImageCount($style, 'private'), 0, format_string('Image style %style flushed correctly for %wrapper wrapper.', array('%style' => $style['name'], '%wrapper' => 'private')));
+ }
+}
diff --git a/drupal-dev/modules/image/sample.png b/drupal-dev/modules/image/sample.png
new file mode 100644
index 0000000000000000000000000000000000000000..f22e0df98448bbaff3282f86ea5d14fdb256551c
GIT binary patch
literal 168110
zcmYhhWmFtW(=|G{y99T44elP?-DQyA9^47;?gV#tcZXoXArM@GhhX2#$@8vzSN{Nd
zXsxPUdsoj_6(t!IL;^$r0DvMVE2#zmK#>6ekUH>?pdK7-99jTmg1elgn1+|p`KFk)
zfhP9Yp6g|oW0G7}X6wfan^5UwEj^pDD4Y__&q!*=;>Rm221qPYPPFA!!R-}{>8GDG
z%7ftFQmSyW2K=adteP@u`R;JDq^tb!M}QTICNQSb+1z>2%
zVqN7WAIo!PiA4K%{Mm^Z^($Pc`|z&6MCIqiU}s1$v}_%N;y1a)R0a5@V{WSRvB;f#R)7!k#r#^y0)SAY3Ds1U=|
za$OLqIi13^Wll5N&%W3Rn!*g3h?AYp3`_WXXr`Ci(}7KY+5iV4kME~}`t`j~WGRq!hX8m^s83=ef;O$xecW~2E>xIC~z#vW4Y3xD*HZ1d9
z(3aUHLNdvi*y=OBvj4UQFH4cdiw6F8JRMcgP|!5sJuZ|Lb75IQ!lp&ifI5qqr-!w`
zSN!=I0;)=q3j=gu@EUhEKVjL_zri4X_89?ff+7nKY7BfW7z>i9sxQ)Q>zB?@8DDF_
zQ-J0Tu0KGpxsKBMcZHyipMvoJU52IZ^e`STX!_}S6B_ubLO81^Tv#nBp3+RVWZ>D>
zT19Q1jfm#sYl(*K8fPs(5AV&I>(nhhiUPwvKS$vX*57RLY=lU{AN5vdj!>W(T_V{Q
zU(>d%t<3W2b#%`s<1GH(tZg@9m}_XxTznWU-_Ekd2{^v-ZP5~{TG5`1x^Dp%bX)iV#+7wGb4I<+v7BR2fgyWOgPGC5u@ezUApwL3c!A7y{cZS^lSW#R0kJd{ZOCi_aV
zZRL}vThILJ%b!1kYU$+}%df!q!(k8ZDjbQH)~CDYtL~@uD&mTgcmFkdr01D6av@K8
z!5mK?$!`K1KkhA@*|eE{@hBT#R%tJugKP*71`ji2U&{XeS}0BoyC+&7ATE`*d&ybL
z10QW!){&Z~O(iBZf?|O)fZf~_BAo399-G4=YL}ni^ZM#V>y}^Lmfx0Nm!JJ&DeFaR
zgCZ2lsC4rHGAu$uC=FM9%3;N)615p!UCqqYMf?X%Z39G&%IVEa0ZwIBxu^g(E
z!{H~h8uzy$?|~Cnd;P%(Jv#|4;Ugm02~COst)`2;Q>d)Lh^14^u`(&6US<>9{=gu-
z3))JiWO^n9Rco8vx9Ro1Kc|%Mfb?XjUa9l=`}U@R?n}WsD1cN8!&&F#DgPG%SG_m|
zRJUL8j=&;f0v8$=nkZ`uAO*V$hLc-bxEXV%jRKz!+tqx