diff --git a/sites/all/modules/filefield/LICENSE.txt b/sites/all/modules/filefield/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/sites/all/modules/filefield/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/filefield/README.txt b/sites/all/modules/filefield/README.txt new file mode 100644 index 0000000..4a0bbf5 --- /dev/null +++ b/sites/all/modules/filefield/README.txt @@ -0,0 +1,44 @@ + +FileField provides an "File" field type to CCK. It provides many advantages over +the Drupal core "Upload" module including: + + * Per-field upload control (file extensions, file size). + * Per-node upload size limits. + * Multiple fields per content type. + * Customizable paths for saving uploads (plus token support for dynamic paths). + * Icons for uploaded file types. + +FileField was written by Darrel Opry (dopry). +Maintained by Nathan Haug (quicksketch) and Andrew Morton (drewish). + +Dependencies +------------ + * Content + +FileField also provides additional features when used with the following: + + * ImageField (See an image preview during editing.) + * Token (Generate dynamic paths when saving images.) + * ImageCache (Create thumbnails of images on output.) + +If your site is any larger than a personal blog, you should definitely install +the following modules to increase the security and stability of your uploads. + + * Transliteration (Convert unsafe characters to file system safe names.) + * MimeDetect (Check the content of files to ensure they match the extension.) + +Install +------- + +1) Copy the filefield folder to the modules folder in your installation. + +2) Enable the module using Administer -> Site building -> Modules + (/admin/build/modules). + +3) Create a new file field in through CCK's interface. Visit Administer -> + Content management -> Content types (admin/content/types), then click + Manage fields on the type you want to add an file upload field. Select + "File" as the field type and "File" as the widget type to create a new + field. + +4) Upload files on the node form for the type that you set up. diff --git a/sites/all/modules/filefield/UPGRADE.txt b/sites/all/modules/filefield/UPGRADE.txt new file mode 100644 index 0000000..2ffdf21 --- /dev/null +++ b/sites/all/modules/filefield/UPGRADE.txt @@ -0,0 +1,40 @@ + +Upgrading from previous versions of FileField does not differ from the +recommended approach for other modules. + +Upgrading from Drupal 5 +----------------------- + +1) Your Drupal 5 installation must be running at least FileField 2.3 before + migrating to Drupal 6. If your version of FileField is older than this, + install the latest version of FileField for Drupal 5 on your Drupal 5 site + and run update.php. This gets the database in a format that the Drupal 6 + upgrade expects. + +2) After ensure FileField 2.3 or higher is on your Drupal 5 site, upgrade your + site normally to Drupal 6. Download the latest version of FileField for + Drupal 6 and place it in your sites/all/modules directory. + +3) Run update.php on your Drupal 6 site to upgrade FileField to the latest + database schema. + + +Upgrading from any previous Drupal 6 version +-------------------------------------------- + +1) Download the latest version of FileField and the filefield directory in + sites/all/modules. + +2) Visit update.php on your site and run the update. + + +Running update.php +------------------ +Never, never, ever expand the "Select versions" fieldset and manually change +the update number. Drupal knows which version you are currently running and +automatically selects the appropriate version. If you change this number, you +risk running an update twice (if you select a lower number) or you'll skip +updates entirely (if you select a higher number). The ability to change the +update number was intended for developers. Due to the confusion caused by this +fieldset, it has been removed entirely in Drupal 7 to prevent such accidental +self-inflicted damage to user sites. diff --git a/sites/all/modules/filefield/field_file.inc b/sites/all/modules/filefield/field_file.inc new file mode 100644 index 0000000..a98c539 --- /dev/null +++ b/sites/all/modules/filefield/field_file.inc @@ -0,0 +1,416 @@ + 0, 'filepath' => '', 'filename' => '', 'filemime' => '', 'filesize' => 0); + } + + $files = _field_file_cache(); + + // Serve file from internal cache if available. + if (empty($files[$fid])) { + if (is_numeric($fid)) { + $file = db_fetch_object(db_query('SELECT f.* FROM {files} f WHERE f.fid = %d', $fid)); + } + else { + $file = db_fetch_object(db_query("SELECT f.* FROM {files} f WHERE f.filepath = '%s'", $fid)); + } + + if (!$file) { + $file = (object) array('fid' => 0, 'filepath' => '', 'filename' => '', 'filemime' => '', 'filesize' => 0); + } + + foreach (module_implements('file_load') as $module) { + if ($module != 'field') { + $function = $module .'_file_load'; + $function($file); + } + } + + // Cache the fully loaded file for later use. + $files = _field_file_cache($file); + } + + // Cast to an array for the field storage. + // Contrary to fields, hook_file() and core file functions expect objects. + return isset($files[$fid]) ? (array) $files[$fid] : FALSE; +} + +/** + * Save a file upload to a new location. + * The source file is validated as a proper upload and handled as such. By + * implementing hook_file($op = 'insert'), modules are able to act on the file + * upload and to add their own properties to the file. + * + * The file will be added to the files table as a temporary file. Temporary + * files are periodically cleaned. To make the file permanent file call + * file_set_status() to change its status. + * + * @param $source + * A string specifying the name of the upload field to save. + * @param $validators + * An optional, associative array of callback functions used to validate the + * file. The keys are function names and the values arrays of callback + * parameters which will be passed in after the user and file objects. The + * functions should return an array of error messages, an empty array + * indicates that the file passed validation. The functions will be called in + * the order specified. + * @param $dest + * A string containing the directory $source should be copied to. If this is + * not provided or is not writable, the temporary directory will be used. + * @return + * An array containing the file information, or 0 in the event of an error. + */ +function field_file_save_upload($source, $validators = array(), $dest = FALSE) { + if (!$file = file_save_upload($source, $validators, $dest, FILE_EXISTS_RENAME)) { + return 0; + } + if (!@chmod($file->filepath, 0664)) { + watchdog('filefield', 'Could not set permissions on destination file: %file', array('%file' => $file->filepath)); + } + + // Let modules add additional properties to the yet barebone file object. + foreach (module_implements('file_insert') as $module) { + $function = $module .'_file_insert'; + $function($file); + } + _field_file_cache($file); // cache the file in order to minimize load queries + return (array)$file; +} + +/** + * Save a file into a file node after running all the associated validators. + * + * This function is usually used to move a file from the temporary file + * directory to a permanent location. It may be used by import scripts or other + * modules that want to save an existing file into the database. + * + * @param $filepath + * The local file path of the file to be saved. + * @param $validators + * An optional, associative array of callback functions used to validate the + * file. The keys are function names and the values arrays of callback + * parameters which will be passed in after the user and file objects. The + * functions should return an array of error messages, an empty array + * indicates that the file passed validation. The functions will be called in + * the order specified. + * @param $dest + * A string containing the directory $source should be copied to. If this is + * not provided or is not writable, the temporary directory will be used. + * @param $account + * The user account object that should associated with the uploaded file. + * @return + * An array containing the file information, or 0 in the event of an error. + */ +function field_file_save_file($filepath, $validators = array(), $dest = FALSE, $account = NULL) { + if (!isset($account)) { + $account = $GLOBALS['user']; + } + + // Add in our check of the the file name length. + $validators['file_validate_name_length'] = array(); + + // Begin building file object. + $file = new stdClass(); + $file->uid = $account->uid; + $file->filename = basename($filepath); + $file->filepath = $filepath; + $file->filemime = module_exists('mimedetect') ? mimedetect_mime($file) : file_get_mimetype($file->filename); + + // Rename potentially executable files, to help prevent exploits. + if (preg_match('/\.(php|pl|py|cgi|asp|js)$/i', $file->filename) && (substr($file->filename, -4) != '.txt')) { + $file->filemime = 'text/plain'; + $file->filepath .= '.txt'; + $file->filename .= '.txt'; + } + + // If the destination is not provided, or is not writable, then use the + // temporary directory. + if (empty($dest) || file_check_path($dest) === FALSE) { + $dest = file_directory_temp(); + } + + $file->source = 'field_file_save_file'; + $file->destination = file_destination(file_create_path($dest .'/'. $file->filename), FILE_EXISTS_RENAME); + $file->filesize = filesize($filepath); + + // Call the validation functions. + $errors = array(); + foreach ($validators as $function => $args) { + // Add the $file variable to the list of arguments and pass it by + // reference (required for PHP 5.3 and higher). + array_unshift($args, NULL); + $args[0] = &$file; + $errors = array_merge($errors, call_user_func_array($function, $args)); + } + + // Check for validation errors. + if (!empty($errors)) { + $message = t('The selected file %name could not be saved.', array('%name' => $file->filename)); + if (count($errors) > 1) { + $message .= '
  • '. implode('
  • ', $errors) .'
'; + } + else { + $message .= ' '. array_pop($errors); + } + form_set_error($file->source, $message); + return 0; + } + + if (!file_copy($file, $file->destination, FILE_EXISTS_RENAME)) { + form_set_error($file->source, t('File upload error. Could not move uploaded file.')); + watchdog('file', 'Upload error. Could not move file %file to destination %destination.', array('%file' => $file->filename, '%destination' => $file->destination)); + return 0; + } + + // If we made it this far it's safe to record this file in the database. + $file->status = FILE_STATUS_TEMPORARY; + $file->timestamp = time(); + // Insert new record to the database. + drupal_write_record('files', $file); + + // Let modules add additional properties to the yet barebone file object. + foreach (module_implements('file_insert') as $module) { + $function = $module .'_file_insert'; + $function($file); + } + _field_file_cache($file); // cache the file in order to minimize load queries + return (array)$file; +} + +/** + * Save a node file. Delete items if necessary and set new items as permanent. + * + * @param $node + * Node object this file is be associated with. + * @param $file + * File to be inserted, passed by reference since fid should be attached. + * @return array + */ +function field_file_save($node, &$file) { + // If this item is marked for deletion. + if (!empty($file['delete']) || !empty($file['_remove'])) { + // If we're creating a new revision, return an empty array so CCK will + // remove the item. + if (!empty($node->old_vid)) { + return array(); + } + // Otherwise delete the file and return an empty array. + if (field_file_delete($file)) { + return array(); + } + } + + // Cast to object since core functions use objects. + $file = (object)$file; + + // Set permanent status on files if unset. + if (empty($file->status)) { + file_set_status($file, FILE_STATUS_PERMANENT); + } + + // Let modules update their additional file properties too. + foreach (module_implements('file_update') as $module) { + $function = $module .'_file_update'; + $function($file); + } + _field_file_cache($file); // update the cache, in case the file has changed + + $file = (array)$file; + return $file; +} + +/** + * Delete a field file and its database record. + * + * @param $path + * A file object. + * @param $force + * Force File Deletion ignoring reference counting. + * @return mixed + * TRUE for success, Array for reference count block, or FALSE in the event of an error. + */ +function field_file_delete($file, $force = FALSE) { + $file = (object)$file; + // If any module returns a value from the reference hook, the + // file will not be deleted from Drupal, but file_delete will + // return a populated array that tests as TRUE. + if (!$force && $references = module_invoke_all('file_references', $file)) { + $references = array_filter($references); // only keep positive values + if (!empty($references)) { + return $references; + } + } + + // Let other modules clean up on delete. + module_invoke_all('file_delete', $file); + + // Make sure the file is deleted before removing its row from the + // database, so UIs can still find the file in the database. + if (file_delete($file->filepath)) { + db_query('DELETE FROM {files} WHERE fid = %d', $file->fid); + _field_file_cache(NULL, $file); // delete the file from the cache + return TRUE; + } + return FALSE; +} + +/** + * Internal cache, in order to minimize database queries for loading files. + */ +function _field_file_cache($file = NULL, $reset = FALSE) { + static $files = array(); + + // Reset internal cache. + if (is_object($reset)) { // file object, uncache just that one + unset($files[$reset->fid]); + unset($files[$reset->filepath]); + } + else if ($reset) { // TRUE, delete the whole cache + $files = array(); + } + + // Cache the file by both fid and filepath. + // Use non-copying objects to save memory. + if (!empty($file->fid)) { + $files[$file->fid] = $file; + $files[$file->filepath] = $file; + } + return $files; +} + +/** + * A silent version of file.inc's file_check_directory(). + * + * This function differs from file_check_directory in that it checks for + * files when doing the directory check and it does not use drupal_set_message() + * when creating directories. This function may be removed in Drupal 7. + * + * Check that the directory exists and is writable. Directories need to + * have execute permissions to be considered a directory by FTP servers, etc. + * + * @param $directory A string containing the name of a directory path. + * @param $mode A Boolean value to indicate if the directory should be created + * if it does not exist or made writable if it is read-only. + * @param $form_item An optional string containing the name of a form item that + * any errors will be attached to. This is useful for settings forms that + * require the user to specify a writable directory. If it can't be made to + * work, a form error will be set preventing them from saving the settings. + * @return FALSE when directory not found, or TRUE when directory exists. + */ +function field_file_check_directory(&$directory, $mode = 0, $form_item = NULL) { + $directory = rtrim($directory, '/\\'); + + // Error if the directory is a file. + if (is_file($directory)) { + watchdog('file system', 'The path %directory was checked as a directory, but it is a file.', array('%directory' => $directory), WATCHDOG_ERROR); + if ($form_item) { + form_set_error($form_item, t('The directory %directory is a file and cannot be overwritten.', array('%directory' => $directory))); + } + return FALSE; + } + + // Create the directory if it is missing. + if (!is_dir($directory) && $mode & FILE_CREATE_DIRECTORY && !@mkdir($directory, 0775, TRUE)) { + watchdog('file system', 'The directory %directory does not exist.', array('%directory' => $directory), WATCHDOG_ERROR); + if ($form_item) { + form_set_error($form_item, t('The directory %directory does not exist.', array('%directory' => $directory))); + } + return FALSE; + } + + // Check to see if the directory is writable. + if (!is_writable($directory) && $mode & FILE_MODIFY_PERMISSIONS && !@chmod($directory, 0775)) { + watchdog('file system', 'The directory %directory is not writable, because it does not have the correct permissions set.', array('%directory' => $directory), WATCHDOG_ERROR); + if ($form_item) { + form_set_error($form_item, t('The directory %directory is not writable', array('%directory' => $directory))); + } + return FALSE; + } + + if ((file_directory_path() == $directory || file_directory_temp() == $directory) && !is_file("$directory/.htaccess")) { + $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006\nOptions None\nOptions +FollowSymLinks"; + if (($fp = fopen("$directory/.htaccess", 'w')) && fputs($fp, $htaccess_lines)) { + fclose($fp); + chmod($directory .'/.htaccess', 0664); + } + else { + $repl = array('%directory' => $directory, '!htaccess' => nl2br(check_plain($htaccess_lines))); + form_set_error($form_item, t("Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:
!htaccess", $repl)); + watchdog('security', "Security warning: Couldn't write .htaccess file. Please create a .htaccess file in your %directory directory which contains the following lines:
!htaccess", $repl, WATCHDOG_ERROR); + } + } + + return TRUE; +} + +/** + * Remove a possible leading file directory path from the given path. + */ +function field_file_strip_path($path) { + $dirpath = file_directory_path(); + $dirlen = drupal_strlen($dirpath); + if (drupal_substr($path, 0, $dirlen + 1) == $dirpath .'/') { + $path = drupal_substr($path, $dirlen + 1); + } + return $path; +} + +/** + * Encode a file path in a way that is compatible with file_create_url(). + * + * This function should be used on the $file->filepath property before any call + * to file_create_url(). This ensures that the file directory path prefix is + * unmodified, but the actual path to the file will be properly URL encoded. + */ +function field_file_urlencode_path($path) { + // Encode the parts of the path to ensure URLs operate within href attributes. + // Private file paths are urlencoded for us inside of file_create_url(). + if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PUBLIC) { + $file_directory_path = file_directory_path(); + if (strpos($path, $file_directory_path . '/') === 0) { + $path = trim(substr($path, strlen($file_directory_path)), '\\/'); + } + + $parts = explode('/', $path); + foreach ($parts as $index => $part) { + $parts[$index] = rawurlencode($part); + } + $path = implode('/', $parts); + + // Add the file directory path again (not encoded). + $path = $file_directory_path . '/' . $path; + } + return $path; +} + +/** + * Return a count of the references to a file by all modules. + */ +function field_file_references($file) { + $references = (array) module_invoke_all('file_references', $file); + $reference_count = 0; + foreach ($references as $module => $count) { + $reference_count += $count; + } + return $reference_count; +} diff --git a/sites/all/modules/filefield/filefield-rtl.css b/sites/all/modules/filefield/filefield-rtl.css new file mode 100644 index 0000000..842e1a5 --- /dev/null +++ b/sites/all/modules/filefield/filefield-rtl.css @@ -0,0 +1,20 @@ + +.filefield-icon { + margin: 0 0 0 2px; +} + +.filefield-element .widget-preview { + float: right; + padding: 0 0 0 10px; + border-size: 0 0 0 1px; + margin: 0 0 0 10px; +} + +.filefield-element .widget-edit { + float: right; +} + +.filefield-element div.ahah-progress, +.filefield-element div.throbber { + padding: 1px 3px 2px 13px; +} diff --git a/sites/all/modules/filefield/filefield.css b/sites/all/modules/filefield/filefield.css new file mode 100644 index 0000000..2b68647 --- /dev/null +++ b/sites/all/modules/filefield/filefield.css @@ -0,0 +1,72 @@ + +/** + * Formatter styles + */ + +.filefield-icon { + margin: 0 2px 0 0; /* RTL */ +} + +/* End formatter styles. */ + +/** + * General widget form styles (applicable to all widgets). + */ +.filefield-element { + margin: 1em 0; + white-space: normal; +} + +.filefield-element .widget-preview { + float: left; /* RTL */ + padding: 0 10px 0 0; /* RTL */ + margin: 0 10px 0 0; /* RTL */ + border-width: 0 1px 0 0; /* RTL */ + border-style: solid; + border-color: #CCC; + max-width: 30%; +} + +.filefield-element .widget-edit { + float: left; /* RTL */ + max-width: 70%; +} + +.filefield-element .filefield-preview { + width: 16em; + overflow: hidden; +} + +.filefield-element .widget-edit .form-item { + margin: 0 0 1em 0; +} + +.filefield-element input.form-submit, +.filefield-element input.form-file { + margin: 0; +} + +.filefield-element input.progress-disabled { + float: none; + display: inline; +} + +.filefield-element div.ahah-progress, +.filefield-element div.throbber { + display: inline; + float: none; + padding: 1px 13px 2px 3px; /* RTL */ +} + +.filefield-element div.ahah-progress-bar { + display: none; + margin-top: 4px; + width: 28em; + padding: 0; +} + +.filefield-element div.ahah-progress-bar div.bar { + margin: 0; +} + +/* End general widget form styles. */ diff --git a/sites/all/modules/filefield/filefield.devel.inc b/sites/all/modules/filefield/filefield.devel.inc new file mode 100644 index 0000000..f8faf43 --- /dev/null +++ b/sites/all/modules/filefield/filefield.devel.inc @@ -0,0 +1,126 @@ += 2); + imagefilledrectangle($im, $x, $y, $x + $width/2, $y + $height/2, $color); + } + + // Make a perfect circle in the image middle. + $color = imagecolorallocate($im, rand(0, 255), rand(0, 255), rand(0, 255)); + $smaller_dimension = min($width, $height); + $smaller_dimension = ($smaller_dimension % 2) ? $smaller_dimension : $smaller_dimension; + imageellipse($im, $width/2, $height/2, $smaller_dimension, $smaller_dimension, $color); + + $save_function = 'image'. ($extension == 'jpg' ? 'jpeg' : $extension); + $save_function($im, $temp_file); + + $images[$extension][$min_resolution][$max_resolution][$temp_file] = $temp_file; + } + } + // Select one of the images we've already generated for this field. + else { + $temp_file = array_rand($images[$extension][$min_resolution][$max_resolution]); + } + + return $temp_file; +} diff --git a/sites/all/modules/filefield/filefield.info b/sites/all/modules/filefield/filefield.info new file mode 100644 index 0000000..475e982 --- /dev/null +++ b/sites/all/modules/filefield/filefield.info @@ -0,0 +1,12 @@ +name = FileField +description = Defines a file field type. +dependencies[] = content +package = CCK +core = 6.x +php = 5.0 +; Information added by Drupal.org packaging script on 2016-02-24 +version = "6.x-3.14" +core = "6.x" +project = "filefield" +datestamp = "1456327142" + diff --git a/sites/all/modules/filefield/filefield.install b/sites/all/modules/filefield/filefield.install new file mode 100644 index 0000000..df5ca69 --- /dev/null +++ b/sites/all/modules/filefield/filefield.install @@ -0,0 +1,363 @@ +'); + $description = NULL; + if (!$apache || !$php_52) { + $value = $t('Not enabled'); + $description = $t('Your server is not capable of displaying file upload progress. File upload progress requires PHP 5.2 and an Apache server.'); + $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['filefield_progress'] = array( + 'title' => $t('Upload progress'), + 'value' => $value, + 'severity' => $severity, + 'description' => $description, + ); + } + + return $requirements; +} + +/** + * Implementation of hook_update_last_removed(). + */ +function filefield_update_last_removed() { + return 3; +} + +/** + * Upgrade FileField to Drupal 6. + */ +function filefield_update_6001() { + if ($abort = content_check_update('filefield')) { + return $abort; + } + + $ret = array(); + module_load_include('inc', 'content', 'includes/content.admin'); + + // Rename the field type from file to filefield. adhere to module namespace. + $ret[] = update_sql("UPDATE {content_node_field} SET type = 'filefield', module = 'filefield', active = 1 WHERE type = 'file'"); + // Rename default widget to filefield_widget. adhere to module namespace. + $ret[] = update_sql("UPDATE {content_node_field_instance} SET widget_type = 'filefield_widget', widget_module = 'filefield', widget_active = 1 WHERE widget_type = 'file'"); + + // Update list default value and force list settings. + $result = db_query("SELECT * FROM {content_node_field} WHERE type = 'filefield'"); + while ($field = db_fetch_object($result)) { + $updated = FALSE; + $field_settings = unserialize($field->global_settings); + if (!isset($field_settings['list_default']) || !is_numeric($field_settings['list_default'])) { + $field_settings['list_default'] = 1; + $updated = TRUE; + } + + // Set behavior to match old force_list behavior. + if (!empty($field_settings['force_list'])) { + $field_settings['list_default'] = 1; + $field_settings['force_list_default'] = 1; + $updated = TRUE; + } + if ($updated) { + db_query("UPDATE {content_node_field} SET global_settings = '%s' WHERE field_name = '%s'", serialize($field_settings), $field->field_name); + } + } + + // Re-enable all the FileFields on the site. + content_associate_fields('filefield'); + + // Build a list of fields that need data updating. + $fields = array(); + foreach (content_types_install() as $type_name => $type_fields) { + foreach ($type_fields as $field) { + if ($field['type'] == 'filefield') { + // We only process a given field once. + $fields[$field['field_name']] = $field; + } + } + } + + // Update database storage (add data column, remove description, set NOT NULL). + foreach ($fields as $field) { + $new_field = $field; + + // Setup the previous definition. + $field['columns']['description'] = array('type' => 'varchar'); + $field['columns']['fid']['not null'] = TRUE; + $field['columns']['list']['not null'] = TRUE; + unset($field['columns']['data']); + + // Setup the new definition. + $new_field['columns']['data'] = array('type' => 'text', 'serialize' => TRUE); + $new_field['columns']['fid']['not null'] = FALSE; + $new_field['columns']['list']['size'] = 'tiny'; + $new_field['columns']['list']['not null'] = FALSE; + unset($new_field['columns']['description']); + + content_alter_db($field, $new_field); + } + + // Build a batch that migrates the data in each filefield + $batch = array( + 'title' => t('Migrating filefield values'), + 'operations' => array(), + 'file' => drupal_get_path('module', 'filefield') .'/filefield.install', + ); + foreach ($fields as $field_name => $field) { + if ($field['type'] == 'filefield') { + $batch['operations'][] = array('_filefield_update_6001_move_operation', array($field)); + $batch['operations'][] = array('_filefield_update_6001_drop_operation', array($field)); + } + } + batch_set($batch); + + + // Clear caches. + cache_clear_all('*', content_cache_tablename(), TRUE); + cache_clear_all('*', 'cache', TRUE); + return $ret; +} + +/** + * Migrate field settings from 'force_list_default' and 'show_description'. + */ +function filefield_update_6100() { + $ret = array(); + + module_load_include('inc', 'content', 'includes/content.crud'); + + $fields = content_fields(); + foreach ($fields as $field) { + if ($field['type'] == 'filefield') { + if (isset($field['force_list_default'])) { + $field['list_field'] = !$field['force_list_default']; + } + if (isset($field['show_description'])) { + $field['description_field'] = $field['show_description']; + } + _content_field_write($field); + $ret[] = array('success' => TRUE, 'query' => t('The file field %field has been updated with new settings.', array('%field' => $field['field_name']))); + } + } + + cache_clear_all('*', content_cache_tablename(), TRUE); + cache_clear_all('*', 'cache', TRUE); + + return $ret; +} + +/** + * Set fid to NULL where files have been deleted. + * + * This is a double-cleanup from Drupal 5 versions, where fid used to be 0 for + * empty rows or sometimes referred to a nonexistent FID altogether. + */ +function filefield_update_6101() { + $ret = array(); + + module_load_include('inc', 'content', 'includes/content.crud'); + + $fields = content_fields(); + + foreach ($fields as $field) { + if ($field['type'] == 'filefield') { + $db_info = content_database_info($field); + if (isset($db_info['columns']['fid'])) { + $table = $db_info['table']; + $fid_column = $db_info['columns']['fid']['column']; + $list_column = $db_info['columns']['list']['column']; + $ret[] = update_sql("UPDATE {" . $table . "} SET $fid_column = NULL, $list_column = NULL WHERE $fid_column NOT IN (SELECT fid FROM {files})"); + } + } + } + + return $ret; +} + +/** + * Fix corrupted serialized data in the "data" column. + */ +function filefield_update_6102(&$sandbox) { + // Update removed. This turned out to be a bug in CCK core, so it is being + // fixed directly in CCK rather than in FileField. + // See http://drupal.org/node/407446. + return array(); +} + +/** + * Convert "Extensible File" to a normal "File" widget. + */ +function filefield_update_6103() { + $ret = array(); + + $ret[] = update_sql("UPDATE {". content_instance_tablename() ."} SET widget_type = 'filefield_widget' WHERE widget_type = 'filefield_combo'"); + + cache_clear_all('*', content_cache_tablename(), TRUE); + cache_clear_all('*', 'cache', TRUE); + + return $ret; +} + +/** + * Delete the filefield_token module entry in the system table. + */ +function filefield_update_6104() { + $ret = array(); + + $ret[] = update_sql("DELETE FROM {system} WHERE type = 'module' AND name = 'filefield_token'"); + + return $ret; +} + +/** + * Move the list and descriptions column into the serialized data column. + */ +function _filefield_update_6001_move_operation($field, &$context) { + // Setup the first through + if (!isset($context['sandbox']['processed_files'])) { + $db_info = content_database_info($field); + $context['sandbox']['db_info'] = $db_info; + $context['sandbox']['table'] = $db_info['table']; + $context['sandbox']['col_data'] = $db_info['columns']['data']['column']; + $context['sandbox']['col_desc'] = $db_info['columns']['description']['column']; + $context['sandbox']['max'] = db_result(db_query("SELECT COUNT(*) FROM {". $db_info['table'] ."}")); + $context['sandbox']['current_node'] = 0; + $context['sandbox']['current_delta'] = 0; + $context['sandbox']['processed_files'] = array(); + } + + // Work our way through the field values 50 rows at a time. + $limit = 50; + $result = NULL; + if ($field['multiple']) { + $result = db_query_range("SELECT * FROM {{$context['sandbox']['table']}} WHERE (vid = %d AND delta > %d) OR vid > %d ORDER BY vid ASC, delta ASC", $context['sandbox']['current_node'], $context['sandbox']['current_delta'], $context['sandbox']['current_node'], 0, $limit); + } + else { + $result = db_query_range("SELECT * FROM {{$context['sandbox']['table']}} WHERE vid >= %d ORDER BY vid ASC", $context['sandbox']['current_node'], 0, $limit); + } + while ($row = db_fetch_array($result)) { + // Do not process the same file twice. This may happen when a node's files + // are split across two separate batch update HTTP requests. + $delta = isset($row['delta']) ? $row['delta'] : 0; + if (isset($context['sandbox']['processed_files'][$row['vid'] . '_' . $delta])) { + continue; + } + + // Try to unserialize the data column. + if (!empty($row[$context['sandbox']['col_data']])) { + $data = unserialize($row[$context['sandbox']['col_data']]); + } + if (empty($data)) { + $data = array(); + } + + // Copy move the values from the columns into the array... + $data['description'] = $row[$context['sandbox']['col_desc']]; + + // ...serialize it and store it back to the db. + db_query("UPDATE {{$context['sandbox']['table']}} SET {$context['sandbox']['col_data']} = '%s' WHERE vid = %d", serialize($data), $row['vid']); + + // Update our progress information. + $context['sandbox']['processed_files'][$row['vid'] . '_' . $delta] = TRUE; + $context['sandbox']['current_node'] = $row['vid']; + $context['sandbox']['current_delta'] = $delta; + } + + // Inform the batch engine that we are not finished, + // and provide an estimation of the completion level we reached. + $processed_count = count($context['sandbox']['processed_files']); + if ($processed_count != $context['sandbox']['max']) { + $context['finished'] = $processed_count / $context['sandbox']['max']; + } +} + +/** + * Drop the list and description columns. + */ +function _filefield_update_6001_drop_operation($field, &$context) { + $ret = array(); + $db_info = content_database_info($field); + // TODO: Now that the data has been migrated we can drop the columns. + db_drop_field($ret, $db_info['table'], $db_info['columns']['description']['column']); + $context['finished'] = 1; +} diff --git a/sites/all/modules/filefield/filefield.js b/sites/all/modules/filefield/filefield.js new file mode 100644 index 0000000..d03b999 --- /dev/null +++ b/sites/all/modules/filefield/filefield.js @@ -0,0 +1,125 @@ + +/** + * Auto-attach standard client side file input validation. + */ +Drupal.behaviors.filefieldValidateAutoAttach = function(context) { + $("input[type=file]", context).bind('change', Drupal.filefield.validateExtensions); +}; + + +/** + * Prevent FileField uploads when using buttons not intended to upload. + */ +Drupal.behaviors.filefieldButtons = function(context) { + $('input.form-submit', context).bind('mousedown', Drupal.filefield.disableFields); + $('div.filefield-element input.form-submit', context).bind('mousedown', Drupal.filefield.progressBar); +}; + +/** + * Open links to files within the node form in a new window. + */ +Drupal.behaviors.filefieldPreviewLinks = function(context) { + $('div.filefield-element div.widget-preview a', context).click(Drupal.filefield.openInNewWindow).attr('target', '_blank'); +} + +/** + * Admin enhancement: only show the "Files listed by default" when needed. + */ +Drupal.behaviors.filefieldAdmin = function(context) { + var $listField = $('div.filefield-list-field', context); + if ($listField.size()) { + $listField.find('input').change(function() { + if (this.checked) { + if (this.value == 0) { + $('#edit-list-default-wrapper').css('display', 'none'); + } + else { + $('#edit-list-default-wrapper').css('display', 'block'); + } + } + }).change(); + } +}; + +/** + * Utility functions for use by FileField. + * @param {Object} event + */ +Drupal.filefield = { + validateExtensions: function(event) { + // Remove any previous errors. + $('.file-upload-js-error').remove(); + + var fieldName = this.name.replace(/^files\[([a-z0-9_]+)_\d+\]$/, '$1'); + var extensions = ''; + if (Drupal.settings.filefield && Drupal.settings.filefield[fieldName]) { + extensions = Drupal.settings.filefield[fieldName].replace(/[, ]+/g, '|'); + } + if (extensions.length > 1 && this.value.length > 0) { + var extensionPattern = new RegExp('\\.(' + extensions + ')$', 'gi'); + if (!extensionPattern.test(this.value)) { + var error = Drupal.t("The selected file %filename cannot be uploaded. Only files with the following extensions are allowed: %extensions.", + { '%filename' : this.value, '%extensions' : extensions.replace(/\|/g, ', ') } + ); + $(this).parent().before('
' + error + '
'); + this.value = ''; + return false; + } + } + }, + disableFields: function(event) { + var clickedButton = this; + + // Only disable upload fields for AHAH buttons. + if (!$(clickedButton).hasClass('ahah-processed')) { + return; + } + + // Check if we're working with an "Upload" button. + var $enabledFields = []; + if ($(this).parents('div.filefield-element').size() > 0) { + $enabledFields = $(this).parents('div.filefield-element').find('input.form-file'); + } + // Otherwise we're probably dealing with CCK's "Add another item" button. + else if ($(this).parents('div.content-add-more').size() > 0) { + $enabledFields = $(this).parent().parent().find('input.form-file'); + } + + var $disabledFields = $('div.filefield-element input.form-file').not($enabledFields); + + // Disable upload fields other than the one we're currently working with. + $disabledFields.attr('disabled', 'disabled'); + + // All the other mousedown handlers (like AHAH) are excuted before any + // timeout functions will be called, so this effectively re-enables + // the filefields after the AHAH process is complete even though it only + // has a 1 millisecond timeout. + setTimeout(function(){ + $disabledFields.removeAttr('disabled'); + }, 1000); + }, + progressBar: function(event) { + var clickedButton = this; + var $progressId = $(clickedButton).parents('div.filefield-element').find('input.filefield-progress'); + if ($progressId.size()) { + 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 3 seconds. + setTimeout(function() { + $(clickedButton).parents('div.filefield-element').find('div.ahah-progress-bar').slideDown(); + }, 500); + }, + openInNewWindow: function(event) { + window.open(this.href, 'filefieldPreview', 'toolbar=0,scrollbars=1,location=1,statusbar=1,menubar=0,resizable=1,width=500,height=550'); + return false; + } +}; diff --git a/sites/all/modules/filefield/filefield.module b/sites/all/modules/filefield/filefield.module new file mode 100644 index 0000000..9045c10 --- /dev/null +++ b/sites/all/modules/filefield/filefield.module @@ -0,0 +1,1148 @@ + 'filefield_js', + 'page arguments' => array(2, 3, 4), + 'access callback' => 'filefield_edit_access', + 'access arguments' => array(2, 3), + 'type' => MENU_CALLBACK, + ); + $items['filefield/progress'] = array( + 'page callback' => 'filefield_progress', + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implementation of hook_elements(). + */ +function filefield_elements() { + $elements = array(); + $elements['filefield_widget'] = array( + '#input' => TRUE, + '#columns' => array('fid', 'list', 'data'), + '#process' => array('filefield_widget_process'), + '#after_build' => array('filefield_widget_after_build'), + '#value_callback' => 'filefield_widget_value', + '#element_validate' => array('filefield_widget_validate'), + ); + return $elements; +} + +/** + * Implementation of hook_theme(). + * @todo: autogenerate theme registry entrys for widgets. + */ +function filefield_theme() { + return array( + 'filefield_file' => array( + 'arguments' => array('file' => NULL), + 'file' => 'filefield_formatter.inc', + ), + 'filefield_icon' => array( + 'arguments' => array('file' => NULL), + 'file' => 'filefield.theme.inc', + ), + 'filefield_widget' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_widget.inc', + ), + 'filefield_widget_item' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_widget.inc', + ), + 'filefield_widget_preview' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_widget.inc', + ), + 'filefield_widget_file' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_widget.inc', + ), + + + 'filefield_formatter_default' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_formatter.inc', + ), + 'filefield_formatter_url_plain' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_formatter.inc', + ), + 'filefield_formatter_path_plain' => array( + 'arguments' => array('element' => NULL), + 'file' => 'filefield_formatter.inc', + ), + 'filefield_item' => array( + 'arguments' => array('file' => NULL, 'field' => NULL), + 'file' => 'filefield_formatter.inc', + ), + ); +} + +/** + * Implementation of hook_file_download(). + */ +function filefield_file_download($filepath) { + $filepath = file_create_path($filepath); + $result = db_query("SELECT * FROM {files} WHERE filepath = '%s'", $filepath); + + // Ensure case-sensitivity of uploaded file names. + while ($file = db_fetch_object($result)) { + if (strcmp($file->filepath, $filepath) == 0) { + break; + } + } + + // If the file is not found in the database, we're not responsible for it. + if (empty($file)) { + return; + } + + // See if this is a file on a newly created node, on which the user who + // uploaded it will immediately have access. + $new_node_file = $file->status == 0 && isset($_SESSION['filefield_access']) && in_array($file->fid, $_SESSION['filefield_access']); + if ($new_node_file) { + $denied = FALSE; + } + // Loop through all fields and find if this file is used by FileField. + else { + // Find out if any file field contains this file, and if so, which field + // and node it belongs to. Required for later access checking. + $cck_files = array(); + foreach (content_fields() as $field) { + if ($field['type'] == 'filefield' || $field['type'] == 'image') { + $db_info = content_database_info($field); + $table = $db_info['table']; + $fid_column = $db_info['columns']['fid']['column']; + + $columns = array('vid', 'nid'); + foreach ($db_info['columns'] as $property_name => $column_info) { + $columns[] = $column_info['column'] .' AS '. $property_name; + } + $result = db_query("SELECT ". implode(', ', $columns) ." + FROM {". $table ."} + WHERE ". $fid_column ." = %d", $file->fid); + + while ($content = db_fetch_array($result)) { + $content['field'] = $field; + $cck_files[$field['field_name']][$content['vid']] = $content; + } + } + } + + // If no file field item is involved with this file, we don't care about it. + if (empty($cck_files)) { + return; + } + + // So the overall field view permissions are not denied, but if access is + // denied for ALL nodes containing the file, deny the download as well. + // Node access checks also include checking for 'access content'. + $nodes = array(); + $denied = TRUE; + $revision_access = FALSE; + foreach ($cck_files as $field_name => $field_files) { + foreach ($field_files as $revision_id => $content) { + // Checking separately for each revision is probably not the best idea - + // what if 'view revisions' is disabled? So, let's just check for the + // current revision of that node. + if (isset($nodes[$content['nid']])) { + continue; // Don't check the same node twice. + } + if (($node = node_load($content['nid'])) && (node_access('view', $node) && filefield_view_access($field_name, $node))) { + // They have access to the node. + $denied = FALSE; + } + if (!$denied && $node->vid == $revision_id) { + // If revision is the current revision, grant access. + $revision_access = TRUE; + } + elseif (!$denied) { + $revision_node = node_load($content['nid'], $revision_id); + // You have access to the node as well as that particular revision. + $revision_access = $revision_node && _node_revision_access($revision_node, 'view'); + } + + // If node access denied, skip other revisions; or if we have node + // access and access to at least one revision the file is present on, + // skip other revision checks. + if ($denied || $revision_access) { + $nodes[$content['nid']] = $node; + break 2; + } + } + } + } + + // If they don't have access to the node or file, or if the file is only + // attached to revisions that they don't have access to, deny access. + if ($denied || !$revision_access) { + return -1; + } + + // Access is granted. + $name = mime_header_encode($file->filename); + $type = mime_header_encode($file->filemime); + // By default, serve images, text, and flash content for display rather than + // download. Or if variable 'filefield_inline_types' is set, use its patterns. + $inline_types = variable_get('filefield_inline_types', array('^text/', '^image/', 'flash$')); + $disposition = 'attachment'; + foreach ($inline_types as $inline_type) { + // Exclamation marks are used as delimiters to avoid escaping slashes. + if (preg_match('!' . $inline_type . '!', $file->filemime)) { + $disposition = 'inline'; + } + } + return array( + 'Content-Type: ' . $type . '; name="' . $name . '"', + 'Content-Length: ' . $file->filesize, + 'Content-Disposition: ' . $disposition . '; filename="' . $name . '"', + 'Cache-Control: private', + ); +} + +/** + * Implementation of hook_views_api(). + */ +function filefield_views_api() { + return array( + 'api' => 2.0, + 'path' => drupal_get_path('module', 'filefield') . '/views', + ); +} + +/** + * Implementation of hook_form_alter(). + * + * Set the enctype on forms that need to accept file uploads. + */ +function filefield_form_alter(&$form, $form_state, $form_id) { + // Field configuration (for default images). + if ($form_id == 'content_field_edit_form' && isset($form['#field']) && $form['#field']['type'] == 'filefield') { + $form['#attributes']['enctype'] = 'multipart/form-data'; + } + + // Node forms. + if (preg_match('/_node_form$/', $form_id)) { + $form['#attributes']['enctype'] = 'multipart/form-data'; + } +} + +/** + * Implementation of CCK's hook_field_info(). + */ +function filefield_field_info() { + return array( + 'filefield' => array( + 'label' => t('File'), + 'description' => t('Store an arbitrary file.'), + ), + ); +} + +/** + * Implementation of hook_field_settings(). + */ +function filefield_field_settings($op, $field) { + $return = array(); + + module_load_include('inc', 'filefield', 'filefield_field'); + $op = str_replace(' ', '_', $op); + $function = 'filefield_field_settings_'. $op; + if (function_exists($function)) { + $result = $function($field); + if (isset($result) && is_array($result)) { + $return = $result; + } + } + + return $return; + +} + +/** + * Implementation of CCK's hook_field(). + */ +function filefield_field($op, $node, $field, &$items, $teaser, $page) { + module_load_include('inc', 'filefield', 'filefield_field'); + $op = str_replace(' ', '_', $op); + // add filefield specific handlers... + $function = 'filefield_field_'. $op; + if (function_exists($function)) { + return $function($node, $field, $items, $teaser, $page); + } +} + +/** + * Implementation of CCK's hook_widget_settings(). + */ +function filefield_widget_settings($op, $widget) { + switch ($op) { + case 'form': + return filefield_widget_settings_form($widget); + case 'save': + return filefield_widget_settings_save($widget); + } +} + +/** + * Implementation of hook_widget(). + */ +function filefield_widget(&$form, &$form_state, $field, $items, $delta = 0) { + // CCK doesn't give a validate callback at the field level... + // and FAPI's #require is naive to complex structures... + // we validate at the field level ourselves. + if (empty($form['#validate']) || !in_array('filefield_node_form_validate', $form['#validate'])) { + $form['#validate'][] = 'filefield_node_form_validate'; + } + $form['#attributes']['enctype'] = 'multipart/form-data'; + + module_load_include('inc', $field['widget']['module'], $field['widget']['module'] .'_widget'); + + $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => '')); + if (!empty($items[$delta])) { + $item = array_merge($item, $items[$delta]); + } + $element = array( + '#title' => $field['widget']['label'], + '#type' => $field['widget']['type'], + '#default_value' => $item, + '#upload_validators' => filefield_widget_upload_validators($field), + ); + + return $element; +} + +/** + * Get the upload validators for a file field. + * + * @param $field + * A CCK field array. + * @return + * An array suitable for passing to file_save_upload() or the file field + * element's '#upload_validators' property. + */ +function filefield_widget_upload_validators($field) { + $max_filesize = parse_size(file_upload_max_size()); + if (!empty($field['widget']['max_filesize_per_file']) && parse_size($field['widget']['max_filesize_per_file']) < $max_filesize) { + $max_filesize = parse_size($field['widget']['max_filesize_per_file']); + } + + // Match the default value if no file extensions have been saved at all. + if (!isset($field['widget']['file_extensions'])) { + $field['widget']['file_extensions'] = 'txt'; + } + + $validators = array( + // associate the field to the file on validation. + 'filefield_validate_associate_field' => array($field), + 'filefield_validate_size' => array($max_filesize), + // Override core since it excludes uid 1 on the extension check. + // Filefield only excuses uid 1 of quota requirements. + 'filefield_validate_extensions' => array($field['widget']['file_extensions']), + ); + return $validators; +} + +/** + * Implementation of CCK's hook_content_is_empty(). + * + * The result of this determines whether content.module will save the value of + * the field. Note that content module has some interesting behaviors for empty + * values. It will always save at least one record for every node revision, + * even if the values are all NULL. If it is a multi-value field with an + * explicit limit, CCK will save that number of empty entries. + */ +function filefield_content_is_empty($item, $field) { + return empty($item['fid']) || (int)$item['fid'] == 0; +} + +/** + * Implementation of CCK's hook_content_diff_values(). + */ +function filefield_content_diff_values($node, $field, $items) { + $return = array(); + foreach ($items as $item) { + if (is_array($item) && !empty($item['filepath'])) { + $return[] = $item['filepath']; + } + } + return $return; +} + +/** + * Implementation of CCK's hook_default_value(). + * + * Note this is a widget-level hook, so it does not affect ImageField or other + * modules that extend FileField. + * + * @see content_default_value() + */ +function filefield_default_value(&$form, &$form_state, $field, $delta) { + // Reduce the default number of upload fields to one. CCK 2 (but not 3) will + // automatically add one more field than necessary. We use the + // content_multiple_value_after_build function to determine the version. + if (!function_exists('content_multiple_value_after_build') && !isset($form_state['item_count'][$field['field_name']])) { + $form_state['item_count'][$field['field_name']] = 0; + } + + // The default value is actually handled in hook_widget(). + // hook_default_value() is only helpful for new nodes, and we need to affect + // all widgets, such as when a new field is added via "Add another item". + return array(); +} + +/** + * Implementation of CCK's hook_widget_info(). + */ +function filefield_widget_info() { + return array( + 'filefield_widget' => array( + 'label' => t('File Upload'), + 'field types' => array('filefield'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'callbacks' => array('default value' => CONTENT_CALLBACK_CUSTOM), + 'description' => t('A plain file upload widget.'), + 'file_extensions' => 'txt', + ), + ); +} + +/** + * Implementation of CCK's hook_field_formatter_info(). + */ +function filefield_field_formatter_info() { + return array( + 'default' => array( + 'label' => t('Generic files'), + 'field types' => array('filefield'), + 'multiple values' => CONTENT_HANDLE_CORE, + 'description' => t('Displays all kinds of files with an icon and a linked file description.'), + ), + 'path_plain' => array( + 'label' => t('Path to file'), + 'field types' => array('filefield'), + 'description' => t('Displays the file system path to the file.'), + ), + 'url_plain' => array( + 'label' => t('URL to file'), + 'field types' => array('filefield'), + 'description' => t('Displays a full URL to the file.'), + ), + ); +} + +/** + * Implementation of CCK's hook_content_generate(). Used by generate.module. + */ +function filefield_content_generate($node, $field) { + module_load_include('inc', 'filefield', 'filefield.devel'); + + if (content_handle('widget', 'multiple values', $field) == CONTENT_HANDLE_MODULE) { + return content_devel_multiple('_filefield_content_generate', $node, $field); + } + else { + return _filefield_content_generate($node, $field); + } +} + +/** + * Get a list of possible information stored in a file field "data" column. + */ +function filefield_data_info() { + static $columns; + + if (!isset($columns)) { + $columns = array(); + foreach (module_implements('filefield_data_info') as $module) { + $function = $module . '_filefield_data_info'; + $data = (array) $function(); + foreach ($data as $key => $value) { + $data[$key] = $value; + $data[$key]['module'] = $module; + } + $columns = array_merge($columns, $data); + } + } + + return $columns; +} + +/** + * Given an array of data options, dispatch the necessary callback function. + */ +function filefield_data_value($key, $value) { + $info = filefield_data_info(); + if (isset($info[$key]['callback'])) { + $callback = $info[$key]['callback']; + $value = $callback($value); + } + else { + $value = check_plain((string) $value); + } + return $value; +} + +/** + * Implementation of hook_filefield_data_info(). + * + * Define a list of values that this module stores in the "data" column of a + * file field. The callback function receives the portion of the data column + * defined by key and should return a value suitable for printing to the page. + */ +function filefield_filefield_data_info() { + return array( + 'description' => array( + 'title' => t('Description'), + 'callback' => 'check_plain', + ), + ); +} + +/** + * Determine the most appropriate icon for the given file's mimetype. + * + * @param $file + * A file object. + * @return + * The URL of the icon image file, or FALSE if no icon could be found. + */ +function filefield_icon_url($file) { + module_load_include('inc', 'filefield', 'filefield.theme'); + return _filefield_icon_url($file); +} + +/** + * Implementation of hook_filefield_icon_sets(). + * + * Define a list of icon sets and directories that contain the icons. + */ +function filefield_filefield_icon_sets() { + return array( + 'default' => drupal_get_path('module', 'filefield') . '/icons', + ); +} + +/** + * Access callback for AHAH upload/delete callbacks and node form validation. + * + * The content_permissions module provides nice fine-grained permissions for + * us to check, so we can make sure that the user may actually edit the file. + */ +function filefield_edit_access($type_name, $field_name, $node = NULL) { + return content_access('edit', content_fields($field_name, $type_name), NULL, $node); +} + +/** + * Access callback that checks if the current user may view the filefield. + */ +function filefield_view_access($field_name, $node = NULL) { + return content_access('view', content_fields($field_name), NULL, $node); +} + +/** + * Menu callback; Shared AHAH callback for 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 filefield_js($type_name, $field_name, $delta) { + $field = content_fields($field_name, $type_name); + + // Immediately disable devel shutdown functions so that it doesn't botch our + // JSON output. + $GLOBALS['devel_shutdown'] = FALSE; + + if (empty($field) || empty($_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'); + print drupal_to_js(array('data' => theme('status_messages'))); + exit; + } + + // Build the new form. + $form_state = array('submitted' => FALSE); + $form_build_id = $_POST['form_build_id']; + $form = form_get_cache($form_build_id, $form_state); + + if (!$form) { + // Invalid form_build_id. + drupal_set_message(t('An unrecoverable error occurred. This form was missing from the server cache. Try reloading the page and submitting again.'), 'error'); + print drupal_to_js(array('data' => theme('status_messages'))); + exit; + } + + // Build the form. This calls the file field's #value_callback function and + // saves the uploaded file. Since this form is already marked as cached + // (the #cache property is TRUE), the cache is updated automatically and we + // don't need to call form_set_cache(). + $args = $form['#parameters']; + $form_id = array_shift($args); + $form['#post'] = $_POST; + $form = form_builder($form_id, $form, $form_state); + + // Update the cached form with the new element at the right place in the form. + if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type_name, $field_name))) { + if (isset($form['#multigroups']) && isset($form['#multigroups'][$group_name][$field_name])) { + $form_element = $form[$group_name][$delta][$field_name]; + } + else { + $form_element = $form[$group_name][$field_name][$delta]; + } + } + + if (!isset($form_element)) { + $form_element = $form[$field_name][$delta]; + } + + // Prevent duplicate wrappers + unset($form_element['#prefix'], $form_element['#suffix']); + + if (isset($form_element['_weight'])) { + unset($form_element['_weight']); + } + + $output = drupal_render($form_element); + + // AHAH is not being nice to us and doesn't know the "other" button (that is, + // either "Upload" or "Delete") yet. Which in turn causes it not to attach + // AHAH behaviours after replacing the element. So we need to tell it first. + + // Loop through the JS settings and find the settings needed for our buttons. + $javascript = drupal_add_js(NULL, NULL); + $filefield_ahah_settings = array(); + if (isset($javascript['setting'])) { + foreach ($javascript['setting'] as $settings) { + if (isset($settings['ahah'])) { + foreach ($settings['ahah'] as $id => $ahah_settings) { + if (strpos($id, 'filefield-upload') || strpos($id, 'filefield-remove')) { + $filefield_ahah_settings[$id] = $ahah_settings; + } + } + } + } + } + + // Add the AHAH settings needed for our new buttons. + if (!empty($filefield_ahah_settings)) { + $output .= ''; + } + + $output = theme('status_messages') . $output; + + // For some reason, file uploads don't like drupal_json() with its manual + // setting of the text/javascript HTTP header. So use this one instead. + print drupal_to_js(array('status' => TRUE, 'data' => $output)); + exit; +} + +/** + * Menu callback for upload progress. + */ +function filefield_progress($key) { + $progress = array( + 'message' => t('Starting upload...'), + 'percentage' => -1, + ); + + $implementation = filefield_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($progress); +} + +/** + * Determine which upload progress implementation to use, if any available. + */ +function filefield_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; +} + +/** + * Implementation of hook_file_references(). + */ +function filefield_file_references($file) { + $count = filefield_get_file_reference_count($file); + return $count ? array('filefield' => $count) : NULL; +} + +/** + * Implementation of hook_file_delete(). + */ +function filefield_file_delete($file) { + filefield_delete_file_references($file); +} + +/** + * An #upload_validators callback. Check the file matches an allowed extension. + * + * If the mimedetect module is available, this will also validate that the + * content of the file matches the extension. User #1 is included in this check. + * + * @param $file + * A Drupal file object. + * @param $extensions + * A string with a space separated list of allowed extensions. + * @return + * An array of any errors cause by this file if it failed validation. + */ +function filefield_validate_extensions($file, $extensions) { + global $user; + $errors = array(); + + if (!empty($extensions)) { + $regex = '/\.('. preg_replace('/ +/', '|', preg_quote($extensions)) .')$/i'; + $matches = array(); + if (preg_match($regex, $file->filename, $matches)) { + $extension = $matches[1]; + // If the extension validates, check that the mimetype matches. + if (module_exists('mimedetect')) { + $type = mimedetect_mime($file); + if ($type != $file->filemime) { + $errors[] = t('The file contents (@type) do not match its extension (@extension).', array('@type' => $type, '@extension' => $extension)); + } + } + } + else { + $errors[] = t('Only files with the following extensions are allowed: %files-allowed.', array('%files-allowed' => $extensions)); + } + } + + return $errors; +} + +/** + * Help text automatically appended to fields that have extension validation. + */ +function filefield_validate_extensions_help($extensions) { + if (!empty($extensions)) { + return t('Allowed extensions: %ext', array('%ext' => $extensions)); + } + else { + return ''; + } +} + +/** + * An #upload_validators callback. Check the file size does not exceed a limit. + * + * @param $file + * A Drupal file object. + * @param $file_limit + * An integer value limiting the maximum file size in bytes. + * @param $file_limit + * An integer value limiting the maximum size in bytes a user can upload on + * the entire site. + * @return + * An array of any errors cause by this file if it failed validation. + */ +function filefield_validate_size($file, $file_limit = 0, $user_limit = 0) { + global $user; + + $errors = array(); + + if ($file_limit && $file->filesize > $file_limit) { + $errors[] = t('The file is %filesize exceeding the maximum file size of %maxsize.', array('%filesize' => format_size($file->filesize), '%maxsize' => format_size($file_limit))); + } + + // Bypass user limits for uid = 1. + if ($user->uid != 1) { + $total_size = file_space_used($user->uid) + $file->filesize; + if ($user_limit && $total_size > $user_limit) { + $errors[] = t('The file is %filesize which would exceed your disk quota of %quota.', array('%filesize' => format_size($file->filesize), '%quota' => format_size($user_limit))); + } + } + return $errors; +} + +/** + * Automatic help text appended to fields that have file size validation. + */ +function filefield_validate_size_help($size) { + return t('Maximum file size: %size', array('%size' => format_size(parse_size($size)))); +} + +/** + * An #upload_validators callback. Check an image resolution. + * + * @param $file + * A Drupal file object. + * @param $max_size + * A string in the format WIDTHxHEIGHT. If the image is larger than this size + * the image will be scaled to fit within these dimensions. + * @param $min_size + * A string in the format WIDTHxHEIGHT. If the image is smaller than this size + * a validation error will be returned. + * @return + * An array of any errors cause by this file if it failed validation. + */ +function filefield_validate_image_resolution(&$file, $maximum_dimensions = 0, $minimum_dimensions = 0) { + $errors = array(); + + @list($max_width, $max_height) = explode('x', $maximum_dimensions); + @list($min_width, $min_height) = explode('x', $minimum_dimensions); + + // Check first that the file is an image. + if ($info = image_get_info($file->filepath)) { + if ($maximum_dimensions) { + $resized = FALSE; + + // Check that it is smaller than the given dimensions. + if ($info['width'] > $max_width || $info['height'] > $max_height) { + $ratio = min($max_width/$info['width'], $max_height/$info['height']); + // Check for exact dimension requirements (scaling allowed). + if (strcmp($minimum_dimensions, $maximum_dimensions) == 0 && $info['width']/$max_width != $info['height']/$max_height) { + $errors[] = t('The image must be exactly %dimensions pixels.', array('%dimensions' => $maximum_dimensions)); + } + // Check that scaling won't drop the image below the minimum dimensions. + elseif ((image_get_toolkit() || module_exists('imageapi')) && (($info['width'] * $ratio < $min_width) || ($info['height'] * $ratio < $min_height))) { + $errors[] = t('The image will not fit between the dimensions of %min_dimensions and %max_dimensions pixels.', array('%min_dimensions' => $minimum_dimensions, '%max_dimensions' => $maximum_dimensions)); + } + // Try resizing the image with ImageAPI if available. + elseif (module_exists('imageapi') && imageapi_default_toolkit()) { + $res = imageapi_image_open($file->filepath); + imageapi_image_scale($res, $max_width, $max_height); + imageapi_image_close($res, $file->filepath); + $resized = TRUE; + } + // Try to resize the image to fit the dimensions. + elseif (image_get_toolkit() && @image_scale($file->filepath, $file->filepath, $max_width, $max_height)) { + $resized = TRUE; + } + else { + $errors[] = t('The image is too large; the maximum dimensions are %dimensions pixels.', array('%dimensions' => $maximum_dimensions)); + } + } + + // Clear the cached filesize and refresh the image information. + if ($resized) { + drupal_set_message(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', array('%dimensions' => $maximum_dimensions))); + clearstatcache(); + $file->filesize = filesize($file->filepath); + } + } + + if ($minimum_dimensions && empty($errors)) { + // Check that it is larger than the given dimensions. + if ($info['width'] < $min_width || $info['height'] < $min_height) { + $errors[] = t('The image is too small; the minimum dimensions are %dimensions pixels.', array('%dimensions' => $minimum_dimensions)); + } + } + } + + return $errors; +} + +/** + * Automatic help text appended to fields that have image resolution validation. + */ +function filefield_validate_image_resolution_help($max_size = '0', $min_size = '0') { + if (!empty($max_size)) { + if (!empty($min_size)) { + if ($max_size == $min_size) { + return t('Images must be exactly @min_size pixels', array('@min_size' => $min_size)); + } + else { + return t('Images must be between @min_size pixels and @max_size', array('@max_size' => $max_size, '@min_size' => $min_size)); + } + } + else { + if (image_get_toolkit()) { + return t('Images larger than @max_size pixels will be scaled', array('@max_size' => $max_size)); + } + else { + return t('Images must be smaller than @max_size pixels', array('@max_size' => $max_size)); + } + } + } + if (!empty($min_size)) { + return t('Images must be larger than @max_size pixels', array('@max_size' => $min_size)); + } +} + + +/** + * An #upload_validators callback. Check that a file is an image. + * + * This check should allow any image that PHP can identify, including png, jpg, + * gif, tif, bmp, psd, swc, iff, jpc, jp2, jpx, jb2, xbm, and wbmp. + * + * This check should be combined with filefield_validate_extensions() to ensure + * only web-based images are allowed, however it provides a better check than + * extension checking alone if the mimedetect module is not available. + * + * @param $file + * A Drupal file object. + * @return + * An array of any errors cause by this file if it failed validation. + */ +function filefield_validate_is_image(&$file) { + $errors = array(); + $info = image_get_info($file->filepath); + if (!$info || empty($info['extension'])) { + $errors[] = t('The file is not a known image format.'); + } + return $errors; +} + +/** + * An #upload_validators callback. Add the field to the file object. + * + * This validation function adds the field to the file object for later + * use in field aware modules implementing hook_file. It's not truly a + * validation at all, rather a convient way to add properties to the uploaded + * file. + */ +function filefield_validate_associate_field(&$file, $field) { + $file->field = $field; + return array(); +} + +/******************************************************************************* + * Public API functions for FileField. + ******************************************************************************/ + +/** + * Return an array of file fields within a node type or by field name. + * + * @param $field + * Optional. May be either a field array or a field name. + * @param $node_type + * Optional. The node type to filter the list of fields. + */ +function filefield_get_field_list($node_type = NULL, $field = NULL) { + // Build the list of fields to be used for retrieval. + if (isset($field)) { + if (is_string($field)) { + $field = content_fields($field, $node_type); + } + $fields = array($field['field_name'] => $field); + } + elseif (isset($node_type)) { + $type = content_types($node_type); + $fields = $type['fields']; + } + else { + $fields = content_fields(); + } + + // Filter down the list just to file fields. + foreach ($fields as $key => $field) { + if ($field['type'] != 'filefield') { + unset($fields[$key]); + } + } + + return $fields; +} + +/** + * Count the number of times the file is referenced within a field. + * + * @param $file + * A file object. + * @param $field + * Optional. The CCK field array or field name as a string. + * @return + * An integer value. + */ +function filefield_get_file_reference_count($file, $field = NULL) { + $fields = filefield_get_field_list(NULL, $field); + $file = (object) $file; + + $references = 0; + foreach ($fields as $field) { + $db_info = content_database_info($field); + $references += db_result(db_query( + 'SELECT count('. $db_info['columns']['fid']['column'] .') + FROM {'. $db_info['table'] .'} + WHERE '. $db_info['columns']['fid']['column'] .' = %d', $file->fid + )); + + // If a field_name is present in the file object, the file is being deleted + // from this field. + if (isset($file->field_name) && $field['field_name'] == $file->field_name) { + // If deleting the entire node, count how many references to decrement. + if (isset($file->delete_nid)) { + $node_references = db_result(db_query( + 'SELECT count('. $db_info['columns']['fid']['column'] .') + FROM {'. $db_info['table'] .'} + WHERE '. $db_info['columns']['fid']['column'] .' = %d AND nid = %d', $file->fid, $file->delete_nid + )); + $references = $references - $node_references; + } + else { + $references = $references - 1; + } + } + } + + return $references; +} + +/** + * Get a list of node IDs that reference a file. + * + * @param $file + * The file object for which to find references. + * @param $field + * Optional. The CCK field array or field name as a string. + * @return + * An array of IDs grouped by NID: array([nid] => array([vid1], [vid2])). + */ +function filefield_get_file_references($file, $field = NULL) { + $fields = filefield_get_field_list(NULL, $field); + $file = (object) $file; + + $references = array(); + foreach ($fields as $field) { + $db_info = content_database_info($field); + $sql = 'SELECT nid, vid FROM {'. $db_info['table'] .'} WHERE '. $db_info['columns']['fid']['column'] .' = %d'; + $result = db_query($sql, $file->fid); + while ($row = db_fetch_object($result)) { + $references[$row->nid][$row->vid] = $row->vid; + } + } + + return $references; +} + +/** + * Get all FileField files connected to a node ID. + * + * @param $node + * The node object. + * @param $field + * Optional. The CCK field array or field name as a string. + * @return + * An array of all files attached to that field (or all fields). + */ +function filefield_get_node_files($node, $field = NULL) { + $fields = filefield_get_field_list($node->type, $field); + $files = array(); + + // Get the file rows. + foreach ($fields as $field) { + $db_info = content_database_info($field); + $fields = 'f.*'; + $fields .= ', c.'. $db_info['columns']['list']['column'] .' AS list'; + $fields .= ', c.'. $db_info['columns']['data']['column'] .' AS data'; + $sql = 'SELECT '. $fields .' FROM {files} f INNER JOIN {' . $db_info['table'] . '} c ON f.fid = c.' . $db_info['columns']['fid']['column'] . ' AND c.vid = %d'; + $result = db_query($sql, $node->vid); + while ($file = db_fetch_array($result)) { + $file['data'] = unserialize($file['data']); + $files[$file['fid']] = $file; + } + } + + return $files; +} + +/** + * Delete all node references of a file. + * + * @param $file + * The file object for which to find references. + * @param $field + * Optional. The CCK field array or field name as a string. + */ +function filefield_delete_file_references($file, $field = NULL) { + $fields = filefield_get_field_list(NULL, $field); + $file = (object) $file; + + $references = filefield_get_file_references($file, $field); + foreach ($references as $nid => $node_references) { + // Do not update a node if it is already being deleted directly by the user. + if (isset($file->delete_nid) && $file->delete_nid == $nid) { + continue; + } + + foreach ($node_references as $vid) { + // Do not update the node revision if that revision is already being + // saved or deleted directly by the user. + if (isset($file->delete_vid) && $file->delete_vid == $vid) { + continue; + } + + $node = node_load($nid, $vid); + foreach ($fields as $field_name => $field) { + if (isset($node->$field_name)) { + foreach ($node->$field_name as $delta => $item) { + if ($item['fid'] == $file->fid) { + unset($node->{$field_name}[$delta]); + } + } + $node->$field_name = array_values(array_filter($node->$field_name)); + } + } + + // Save the node after removing the file references. This flag prevents + // FileField from attempting to delete the file again. + $node->skip_filefield_delete = TRUE; + node_save($node); + } + } +} diff --git a/sites/all/modules/filefield/filefield.theme.inc b/sites/all/modules/filefield/filefield.theme.inc new file mode 100644 index 0000000..8a39cf1 --- /dev/null +++ b/sites/all/modules/filefield/filefield.theme.inc @@ -0,0 +1,248 @@ + '-', '+' => '-', '.' => '-')); + + if ($icon_url = filefield_icon_url($file)) { + return ''. t('@mime icon', array('@mime' => $mime)) .''; + } +} + +/** + * Given a file object, create a URL to a matching icon. + * + * @param $file + * A file object. + * @param $theme + * Optional. The theme to be used for the icon. Defaults to the value of + * the "filefield_icon_theme" variable. + * @return + * A URL string to the icon, or FALSE if an appropriate icon could not be + * found. + */ +function _filefield_icon_url($file, $theme = NULL) { + global $base_url; + + if ($icon_path = _filefield_icon_path($file, $theme)) { + return $base_url .'/'. $icon_path; + } + return FALSE; +} + +/** + * Given a file object, create a URL to a matching icon. + * + * @param $file + * A file object. + * @param $theme + * Optional. The theme to be used for the icon. Defaults to the value of + * the "filefield_icon_theme" variable. + * @return + * A string to the icon as a local path, or FALSE if an appropriate icon could + * not be found. + */ +function _filefield_icon_path($file, $theme = NULL) { + if (!isset($theme)) { + $theme = variable_get('filefield_icon_theme', 'default'); + } + + // If there's an icon matching the exact mimetype, go for it. + $dashed_mime = strtr($file['filemime'], array('/' => '-')); + if ($icon_path = _filefield_create_icon_path($dashed_mime, $theme)) { + return $icon_path; + } + // For a couple of mimetypes, we can "manually" tell a generic icon. + if ($generic_name = _filefield_generic_icon_map($file)) { + if ($icon_path = _filefield_create_icon_path($generic_name, $theme)) { + 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) { + if ($icon_path = _filefield_create_icon_path($category .'-x-generic', $theme)) { + return $icon_path; + } + } + } + // Try application-octet-stream as last fallback. + if ($icon_path = _filefield_create_icon_path('application-octet-stream', $theme)) { + return $icon_path; + } + // Sorry, no icon can be found... + return FALSE; +} + +/** + * Internal function to convert a file icon theme name to a directory. + */ +function _filefield_icon_directory($theme = NULL) { + static $sets; + + if (!isset($sets)) { + $sets = module_invoke_all('filefield_icon_sets'); + drupal_alter('filefield_icon_sets', $sets); + } + + if (!isset($theme) || !isset($sets[$theme])) { + $theme = 'default'; + } + + return $sets[$theme]; +} + +function _filefield_create_icon_path($icon_name, $theme = NULL) { + $icons_directory = _filefield_icon_directory($theme); + $icon_path = $icons_directory .'/'. $icon_name .'.png'; + + if (file_exists($icon_path)) { + return $icon_path; + } + return FALSE; +} + +function _filefield_generic_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.openxmlformats-officedocument.presentationml.slideshow': + 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'; + + // RTF files. + case 'application/rtf': + return 'text-rtf'; + + // Google earth files. + case 'application/vnd.google-earth.kml+xml': + case 'application/vnd.google-earth.kmz': + return 'application-google-earth'; + + // Executable types. + case 'application/x-macbinary': + case 'application/x-ms-dos-executable': + case 'application/x-pef-executable': + return 'application-x-executable'; + + default: + return FALSE; + } +} diff --git a/sites/all/modules/filefield/filefield.token.inc b/sites/all/modules/filefield/filefield.token.inc new file mode 100644 index 0000000..e850186 --- /dev/null +++ b/sites/all/modules/filefield/filefield.token.inc @@ -0,0 +1,67 @@ + 'radios', + '#title' => t('List field'), + '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), + '#default_value' => $field['list_field'] === '' ? 0 : (int) $field['list_field'], + '#description' => t('Display a checkbox where users may choose if a file should be shown when viewing the content. Most formatters other than Generic file do not support this option.'), + '#attributes' => array('class' => 'filefield-list-field'), + ); + $form['list_default'] = array( + '#type' => 'checkbox', + '#title' => t('Files listed by default'), + '#default_value' => $field['list_default'] === '' ? 1 : (int) $field['list_default'], + ); + $form['description_field'] = array( + '#type' => 'radios', + '#title' => t('Description field'), + '#default_value' => $field['description_field'] === '' ? 0 : (int) $field['description_field'], + '#options' => array(0 => t('Disabled'), 1 => t('Enabled')), + '#description' => t('Display a text field where users may enter a description about the uploaded file. The description text is used when using the Generic file formatter to link to the file, the file name will be used otherwise. Most other formatters do not use the description field on output.'), + ); + + return $form; +} + +/** + * Implementation of CCK's hook_field_settings($op = 'save'). + */ +function filefield_field_settings_save($field) { + return array('list_field', 'list_default', 'description_field'); +} + +/** + * Implementation of CCK's hook_field_settings($op = 'database_columns'). + */ +function filefield_field_settings_database_columns($field) { + return array( + 'fid' => array('type' => 'int', 'not null' => FALSE, 'views' => TRUE), + 'list' => array('type' => 'int', 'size' => 'tiny', 'not null' => FALSE, 'views' => TRUE), + 'data' => array('type' => 'text', 'serialize' => TRUE, 'views' => TRUE), + ); +} + +/** + * Implementation of CCK's hook_field_settings($op = 'views_data'). + */ +function filefield_field_settings_views_data($field) { + $data = content_views_field_views_data($field); + $db_info = content_database_info($field); + $table_alias = content_views_tablename($field); + + // By defining the relationship, we already have a "Has file" filter + // plus all the filters that Views already provides for files. + // No need for having a filter by ourselves. + unset($data[$table_alias][$field['field_name'] .'_fid']['filter']); + + // Add a relationship for related file. + $data[$table_alias][$field['field_name'] .'_fid']['relationship'] = array( + 'base' => 'files', + 'field' => $db_info['columns']['fid']['column'], + 'handler' => 'content_handler_relationship', + 'label' => t($field['widget']['label']), + 'content_field_name' => $field['field_name'], + 'skip base' => array('files'), + ); + + // Use the Views boolean handler for filtering the list value. + $data[$table_alias][$field['field_name'] .'_list']['filter']['handler'] = 'views_handler_filter_boolean_operator'; + + // Add our special handler for serialized data. + $data[$table_alias][$field['field_name'] .'_data']['field'] = array( + 'title' => $data[$table_alias][$field['field_name'] .'_data']['title'], + 'title short' => $data[$table_alias][$field['field_name'] .'_data']['title short'], + 'help' => t('Can be used to display specific alt, title, or description information. May cause duplicate rows if grouping fields.'), + 'field' => $db_info['columns']['data']['column'], + 'table' => $db_info['table'], + 'handler' => 'filefield_handler_field_data', + 'click sortable' => FALSE, + 'access callback' => 'content_access', + 'access arguments' => array('view', $field), + ); + + // Remove handlers that are probably unnecessary. + unset($data[$table_alias][$field['field_name'] .'_data']['filter']); + unset($data[$table_alias][$field['field_name'] .'_data']['argument']); + unset($data[$table_alias][$field['field_name'] .'_list']['argument']); + + // Set up relationship with file views. + $data[$table_alias]['table']['join']['files'] = array( + 'table' => $db_info['table'], + 'left_field' => 'fid', + 'field' => $db_info['columns']['fid']['column'], + ); + $data[$table_alias]['vid'] = array( + 'title' => t($field['widget']['label']), + 'help' => t('The node the uploaded file is attached to'), + 'relationship' => array( + 'label' => t($field['widget']['label']), + 'base' => 'node', + 'base field' => 'vid', + // This allows us to not show this relationship if the base is already + // node so users won't create circular relationships. + 'skip base' => array('node', 'node_revisions'), + ), + ); + + return $data; +} + +/** + * Implementation of CCK's hook_field($op = 'load'). + */ +function filefield_field_load($node, $field, &$items, $teaser, $page) { + if (empty($items)) { + return array(); + } + foreach ($items as $delta => $item) { + // Despite hook_content_is_empty(), CCK still doesn't filter out + // empty items from $op = 'load', so we need to do that ourselves. + if (empty($item['fid']) || !($file = field_file_load($item['fid']))) { + $items[$delta] = NULL; + } + else { + if (isset($item['data']) && !empty($item['data'])) { + $item['data'] = unserialize($item['data']); + } + // Temporary fix to unserialize data serialized multiple times. + // See the FileField issue http://drupal.org/node/402860. + // And the CCK issue http://drupal.org/node/407446. + while (!empty($item['data']) && is_string($item['data'])) { + $item['data'] = unserialize($item['data']); + } + // Merge any data added by modules in hook_file_load(). + if (isset($file['data']) && isset($item['data'])) { + $file['data'] = array_merge((array) $file['data'], (array) $item['data']); + } + $items[$delta] = array_merge($file, $item); + } + } + + return array($field['field_name'] => $items); +} + +/** + * Implementation of CCK's hook_field($op = 'insert'). + */ +function filefield_field_insert($node, $field, &$items, $teaser, $page) { + return filefield_field_update($node, $field, $items, $teaser, $page); +} + +/** + * Implementation of CCK's hook_field($op = 'update'). + */ +function filefield_field_update($node, $field, &$items, $teaser, $page) { + + // Accumulator to gather current fid to compare with the original node + // for deleting replaced files. + $curfids = array(); + foreach ($items as $delta => $item) { + $items[$delta] = field_file_save($node, $item); + // Remove items from the array if they have been deleted. + if (empty($items[$delta]) || empty($items[$delta]['fid'])) { + $items[$delta] = NULL; + } + else { + $curfids[] = $items[$delta]['fid']; + } + } + + // If this is a new node there are no old items to worry about. + // On new revisions, old files are always maintained in the previous revision. + if ($node->is_new || !empty($node->revision) || !empty($node->skip_filefield_delete)) { + return; + } + + // Delete items from original node. + $orig = node_load($node->nid); + // If there are, figure out which ones must go. + if (!empty($orig->$field['field_name'])) { + foreach ($orig->$field['field_name'] as $oitem) { + if (isset($oitem['fid']) && !in_array($oitem['fid'], $curfids)) { + // For hook_file_references, remember that this is being deleted. + $oitem['field_name'] = $field['field_name']; + $oitem['delete_vid'] = $orig->vid; + filefield_field_delete_file($oitem, $field); + } + } + } +} + +/** + * Implementation of CCK's hook_field($op = 'delete_revision'). + */ +function filefield_field_delete_revision($node, $field, &$items, $teaser, $page) { + foreach ($items as $delta => $item) { + if (isset($item['fid'])) { + // For hook_file_references, remember that this is being deleted. + $item['field_name'] = $field['field_name']; + $item['delete_vid'] = $node->vid; + if (filefield_field_delete_file($item, $field)) { + $items[$delta] = NULL; + } + } + } +} + +/** + * Implementation of CCK's hook_field($op = 'delete'). + */ +function filefield_field_delete($node, $field, &$items, $teaser, $page) { + foreach ($items as $delta => $item) { + if (isset($item['fid'])) { + // For hook_file_references(), remember that this is being deleted. + $item['field_name'] = $field['field_name']; + // Pass in the nid of the node that is being removed so all references can + // be counted in hook_file_references(). + $item['delete_nid'] = $node->nid; + filefield_field_delete_file($item, $field); + } + } + + // Delete all the remaining items present only in older revisions. + $db_info = content_database_info($field); + $result = db_query('SELECT vid, f.* FROM {' . $db_info['table'] . '} t INNER JOIN {files} f ON t.' . $db_info['columns']['fid']['column'] . ' = f.fid WHERE nid = %d AND vid != %d', $node->nid, $node->vid); + while ($item = db_fetch_array($result)) { + if (isset($item['fid'])) { + $item['field_name'] = $field['field_name']; + $item['delete_vid'] = $item['vid']; + filefield_field_delete_file($item, $field); + } + } +} + +/** + * Check that FileField controls a file before attempting to deleting it. + */ +function filefield_field_delete_file($file, $field) { + $file = (object) $file; + + // Remove the field_name and delete_nid properties so that references can be + // counted including the files to be deleted. + $field_name = isset($file->field_name) ? $file->field_name : NULL; + $delete_nid = isset($file->delete_nid) ? $file->delete_nid : NULL; + unset($file->field_name, $file->delete_nid); + + // To prevent FileField from deleting files it doesn't know about, check the + // FileField reference count. Temporary files can be deleted because they + // are not yet associated with any content at all. + if ($file->status == 0 || filefield_get_file_reference_count($file, $field) > 0) { + $file->field_name = $field_name; + $file->delete_nid = $delete_nid; + return field_file_delete($file); + } + + // Even if the file is not deleted, return TRUE to indicate the FileField + // record can be removed from the FileField database tables. + return TRUE; +} + +/** + * Implementation of CCK's hook_field($op = 'sanitize'). + */ +function filefield_field_sanitize($node, $field, &$items, $teaser, $page) { + foreach ($items as $delta => $item) { + // Cleanup $items during node preview. + if (empty($item['fid']) || !empty($item['delete'])) { + // Check for default images at the widget level. + // TODO: Provide an API to ImageField to do this itself? + if (!empty($field['widget']['use_default_image']) && !empty($field['widget']['default_image']['filepath']) && $delta == 0) { + $items[$delta] = $field['widget']['default_image']; + $items[$delta]['default'] = TRUE; + } + else { + $items[$delta] = NULL; + continue; + } + } + + // Add nid so formatters can create a link to the node. + $items[$delta]['nid'] = isset($node->nid) ? $node->nid : NULL; + + // Get the 'data' column stored by CCK into an array. This is necessary + // for Views, which doesn't call the "load" $op and to fix an issue with + // CCK double-serializing data. + // See the FileField issue http://drupal.org/node/402860. + // And the CCK issue http://drupal.org/node/407446. + while (!empty($items[$delta]['data']) && is_string($items[$delta]['data'])) { + $items[$delta]['data'] = unserialize($items[$delta]['data']); + } + + // Load the complete file if a filepath is not available. + if (!empty($item['fid']) && empty($item['filepath'])) { + $file = (array) field_file_load($item['fid']); + if (isset($file['data']) && isset($items[$delta]['data'])) { + $file['data'] = array_merge($file['data'], $items[$delta]['data']); + } + $items[$delta] = array_merge($file, $items[$delta]); + } + + // Verify the file exists on the server. + if (!empty($item['filepath']) && !file_exists($item['filepath'])) { + watchdog('filefield', 'FileField was trying to display the file %file, but it does not exist.', array('%file' => $item['filepath']), WATCHDOG_WARNING); + } + } +} diff --git a/sites/all/modules/filefield/filefield_formatter.inc b/sites/all/modules/filefield/filefield_formatter.inc new file mode 100644 index 0000000..5742ff8 --- /dev/null +++ b/sites/all/modules/filefield/filefield_formatter.inc @@ -0,0 +1,148 @@ + array( + 'type' => $file['filemime'] . '; length=' . $file['filesize'], + ), + ); + + // Use the description as the link text if available. + if (empty($file['data']['description'])) { + $link_text = $file['filename']; + } + else { + $link_text = $file['data']['description']; + $options['attributes']['title'] = $file['filename']; + } + + return '
'. $icon . l($link_text, $url, $options) .'
'; +} diff --git a/sites/all/modules/filefield/filefield_meta/filefield_meta.info b/sites/all/modules/filefield/filefield_meta/filefield_meta.info new file mode 100644 index 0000000..645912c --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/filefield_meta.info @@ -0,0 +1,14 @@ +name = FileField Meta +description = Add metadata gathering and storage to FileField. +dependencies[] = filefield +dependencies[] = getid3 +package = CCK +core = 6.x +php = 5.0 + +; Information added by Drupal.org packaging script on 2016-02-24 +version = "6.x-3.14" +core = "6.x" +project = "filefield" +datestamp = "1456327142" + diff --git a/sites/all/modules/filefield/filefield_meta/filefield_meta.install b/sites/all/modules/filefield/filefield_meta/filefield_meta.install new file mode 100644 index 0000000..26d176f --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/filefield_meta.install @@ -0,0 +1,173 @@ + 'The table for meta data about filefield files.', + 'fields' => array( + 'fid' => array( + 'description' => 'The file id.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'width' => array( + 'description' => 'Width of a video or image file in pixels.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + 'height' => array( + 'description' => 'Height of a video or image file in pixels.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + 'duration' => array( + 'description' => 'The duration of audio or video files, in seconds.', + 'type' => 'float', + 'size' => 'normal', + 'not null' => FALSE, + ), + 'audio_format' => array( + 'description' => 'The audio format.', + 'type' => 'varchar', + 'length' => 10, + 'not null' => TRUE, + 'default' => '', + ), + 'audio_sample_rate' => array( + 'description' => 'The sample rate of the audio.', + 'type' => 'int', + 'size' => 'medium', + 'not null' => TRUE, + 'default' => 0, + ), + 'audio_channel_mode' => array( + 'description' => 'The number of channels in the audio, by name (stereo or mono).', + 'type' => 'varchar', + 'length' => 10, + 'not null' => TRUE, + 'default' => '', + ), + 'audio_bitrate' => array( + 'description' => 'The audio bitrate.', + 'type' => 'float', + 'size' => 'medium', + 'not null' => TRUE, + 'default' => 0, + ), + 'audio_bitrate_mode' => array( + 'description' => 'The kind of audio bitrate, such as VBR. Usually empty.', + 'type' => 'varchar', + 'length' => 4, + 'not null' => TRUE, + 'default' => '', + ), + 'tags' => array( + 'description' => 'ID3 tags such as artist, album, and genre.', + 'type' => 'text', + 'serialize' => TRUE, + ), + ), + 'primary key' => array('fid'), + ); + + return $schema; +} + +function filefield_meta_update_1() { + $ret = array(); + db_add_field($ret, 'filefield_meta', 'audio_format', array( + 'description' => 'The audio format.', + 'type' => 'varchar', + 'length' => 10, + 'not null' => TRUE, + 'default' => '', + )); + db_add_field($ret, 'filefield_meta', 'audio_sample_rate', array( + 'description' => 'The sample rate of the audio.', + 'type' => 'int', + 'size' => 'medium', + 'not null' => TRUE, + 'default' => 0, + )); + db_add_field($ret, 'filefield_meta', 'audio_channel_mode', array( + 'description' => 'The number of channels in the audio, by name.', + 'type' => 'varchar', + 'length' => 10, + 'not null' => TRUE, + 'default' => '', + )); + db_add_field($ret, 'filefield_meta', 'audio_bitrate', array( + 'description' => 'The audio bitrate.', + 'type' => 'float', + 'size' => 'medium', + 'not null' => TRUE, + 'default' => 0, + )); + db_add_field($ret, 'filefield_meta', 'audio_bitrate_mode', array( + 'description' => 'The kind of audio bitrate.', + 'type' => 'varchar', + 'length' => 4, + 'not null' => TRUE, + 'default' => '', + )); + return $ret; +} + +/** + * Add the tags column. + */ +function filefield_meta_update_6100(&$context) { + $ret = array(); + + // Set up our update and add the tags column. + if (!isset($context['sandbox']['progress'])) { + $context['sandbox']['progress'] = 0; + $context['sandbox']['total'] = db_result(db_query("SELECT COUNT(*) FROM {files} f INNER JOIN {filefield_meta} fm ON f.fid = fm.fid WHERE fm.audio_format <> ''")); + $context['sandbox']['current_fid'] = 0; + if (!db_column_exists('filefield_meta', 'tags')) { + db_add_field($ret, 'filefield_meta', 'tags', array('type' => 'text')); + } + // We are done if there are none to update. + if ($context['sandbox']['total'] == 0) { + return $ret; + } + } + + // Select and process 200 files at a time. + $limit = 200; + $result = db_query_range("SELECT f.* FROM {files} f INNER JOIN {filefield_meta} fm ON f.fid = fm.fid WHERE f.fid > %d AND fm.audio_format <> '' ORDER BY f.fid ASC", $context['sandbox']['current_fid'], 0, $limit); + + // Loop through each file and read in its ID3 tags if applicable. + while ($file = db_fetch_object($result)) { + filefield_meta_file_update($file); + $context['sandbox']['current_fid'] = $file->fid; + $context['sandbox']['progress']++; + } + + // Update our progress indicator. + $ret['#finished'] = $context['sandbox']['progress'] / $context['sandbox']['total']; + + return $ret; +} diff --git a/sites/all/modules/filefield/filefield_meta/filefield_meta.module b/sites/all/modules/filefield/filefield_meta/filefield_meta.module new file mode 100644 index 0000000..a9d8eff --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/filefield_meta.module @@ -0,0 +1,211 @@ + array( + 'arguments' => array('duration' => NULL), + ), + 'filefield_meta_samplerate' => array( + 'arguments' => array('samplerate' => NULL), + ), + 'filefield_meta_bitrate' => array( + 'arguments' => array('bitrate' => NULL), + ), + ); +} + +/** + * Implementation of hook_cron(). + */ +function filefield_meta_cron() { + $result = db_query('SELECT fm.fid FROM {filefield_meta} fm LEFT JOIN {files} f ON fm.fid=f.fid WHERE f.fid IS NULL'); + while ($file = db_fetch_object($result)) { + db_query('DELETE FROM {filefield_meta} WHERE fid = %d', $file->fid); + } +} + +/** + * Implementation of hook_views_api(). + */ +function filefield_meta_views_api() { + return array( + 'api' => 2.0, + 'path' => drupal_get_path('module', 'filefield_meta') . '/includes', + ); +} + +/** + * Implementation of FileField's hook_file_load(). + */ +function filefield_meta_file_load(&$file) { + $result = db_query("SELECT * FROM {filefield_meta} WHERE fid = %d", $file->fid); + $data = db_fetch_array($result); + + // Essentially this is a lazy-loader. If no record exists, read in the file. + if ($data) { + $data['tags'] = isset($data['tags']) ? unserialize($data['tags']) : array(); + $file->data = isset($file->data) ? array_merge($file->data, $data) : $data; + } + else { + filefield_meta_file_insert($file); + } +} + +/** + * Implementation of FileField's hook_file_insert(). + */ +function filefield_meta_file_insert(&$file) { + if (!empty($file->fid)) { + filefield_meta($file); + $record = array_merge($file->data, array('fid' => $file->fid)); + drupal_write_record('filefield_meta', $record); + } +} + +/** + * Implementation of FileField's hook_file_update(). + */ +function filefield_meta_file_update(&$file) { + if (!empty($file->fid)) { + filefield_meta_file_delete($file); + filefield_meta_file_insert($file); + } +} + +/** + * Implementation of FileField's hook_file_delete(). + */ +function filefield_meta_file_delete($file) { + db_query('DELETE FROM {filefield_meta} WHERE fid = %d', $file->fid); +} + +/** + * Adds the width, height and duration to the file's data property. + */ +function filefield_meta(&$file) { + $file->data = !empty($file->data) ? $file->data : array(); + + // Skip any attempts at adding information if the file is not actually + // located on this server. + if (!file_exists($file->filepath)) { + return; + } + + $info = getid3_analyze($file->filepath); + $file->data['width'] = $file->data['height'] = $file->data['duration'] = 0; + if (isset($info['video']['resolution_x'])) { + $file->data['width'] = $info['video']['resolution_x']; + $file->data['height'] = $info['video']['resolution_y']; + } + elseif (isset($info['video']['streams'])) { + foreach ($info['video']['streams'] as $stream) { + $file->data['width'] = max($file->data['width'], $stream['resolution_x']); + $file->data['height'] = max($file->data['height'], $stream['resolution_y']); + } + } + + if (isset($info['playtime_seconds'])) { + $file->data['duration'] = $info['playtime_seconds']; + } + + // Initialize fields. + $file->data['audio_format'] = $file->data['audio_channel_mode'] = $file->data['audio_bitrate_mode'] = ''; + $file->data['audio_sample_rate'] = $file->data['audio_bitrate'] = 0; + + if (isset($info['audio'])) { + $file->data['audio_format'] = $info['audio']['dataformat']; //e.g. mp3 + $file->data['audio_sample_rate'] = $info['audio']['sample_rate']; //e.g. 44100 + $file->data['audio_channel_mode'] = $info['audio']['channelmode']; // e.g. mono + $file->data['audio_bitrate'] = isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : NULL; //e.g. 64000 + $file->data['audio_bitrate_mode'] = isset($info['audio']['bitrate_mode']) ? $info['audio']['bitrate_mode'] : NULL; //e.g. cbr + } + + // Add in arbitrary ID3 tags. + if (isset($info['tags_html'])) { + // We use tags_html instead of tags because it is the most reliable data + // source for pulling in non-UTF-8 characters according to getID3 docs. + foreach ($info['tags_html'] as $type => $values) { + // Typically $type may be IDv2 (for MP3s) or quicktime (for AAC). + foreach ($values as $key => $value) { + $value = isset($value[0]) ? (string) $value[0] : (string) $value; + if (!empty($value) && $key != 'coverart' && $key != 'music_cd_identifier') { + $file->data['tags'][$key] = html_entity_decode($value, ENT_QUOTES, 'UTF-8'); + } + } + } + } +} + +/** + * Utility function that simply returns the current list of all known ID3 tags. + * + * If new or different ID3 tags are desired, these may be overridden by adding + * the following to your site's settings.php file. + * + * @code + * $conf['filefield_meta_tags'] = array( + * 'title' => t('Title'), + * 'artist' => t('Artist'), + * 'composer' => t('Composer'), + * // etc... + * ); + * @endcode + */ +function filefield_meta_tags() { + $defaults = array( + 'title' => t('Title'), + 'artist' => t('Artist'), + 'album' => t('Album'), + 'year' => t('Year'), + 'genre' => t('Genre'), + ); + return variable_get('filefield_meta_tags', $defaults); +} + +/** + * Convert the float duration into a pretty string. + * + * @param $duration + */ +function theme_filefield_meta_duration($duration) { + $seconds = round((($duration / 60) - floor($duration / 60)) * 60); + $minutes = floor($duration / 60); + if ($seconds >= 60) { + $seconds -= 60; + $minutes++; + } + return intval($minutes) . ':' . str_pad($seconds, 2, 0, STR_PAD_LEFT); +} + + +/** + * Formats audio sample rate. + */ +function theme_filefield_meta_samplerate($samplerate) { + return t('@sampleratekHz', array('@samplerate' => (int) ($samplerate/1000))); +} + +/** + * Formats audio bit rate. + */ +function theme_filefield_meta_bitrate($bitrate) { + return t('@bitrateKbps', array('@bitrate' => (int) ($bitrate/1000))); +} diff --git a/sites/all/modules/filefield/filefield_meta/filefield_meta.token.inc b/sites/all/modules/filefield/filefield_meta/filefield_meta.token.inc new file mode 100644 index 0000000..cda5e1c --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/filefield_meta.token.inc @@ -0,0 +1,59 @@ + $label) { + $tokens['file']['filefield-tag-' . $tag] = t('File ID3 @tag tag', array('@tag' => $label)); + } + + return $tokens; + } +} + +/** + * Implementation of hook_token_values(). + * + * Provide the token values for a given file item. + */ +function filefield_meta_token_values($type, $object = NULL) { + $tokens = array(); + if ($type == 'field' && isset($object[0]['fid'])) { + $item = $object[0]; + + $tokens['filefield-width'] = $item['data']['width'] ; + $tokens['filefield-height'] = $item['data']['height'] ; + $tokens['filefield-duration'] = $item['data']['duration'] ; + $tokens['filefield-audio-format'] = isset($item['data']['audio_format']) ? check_plain($item['data']['audio_format']) : ''; + $tokens['filefield-audio-sample-rate'] = isset($item['data']['sample_rate']) ? check_plain($item['data']['sample_rate']) : ''; + $tokens['filefield-audio-channel-mode'] = isset($item['data']['audio_channel_mode']) ? check_plain($item['data']['audio_channel_mode']) : ''; + $tokens['filefield-audio-bitrate'] = isset($item['data']['audio_bitrate']) ? check_plain($item['data']['audio_bitrate']) : ''; + $tokens['filefield-audio-bitrate-mode'] = isset($item['data']['audio_bitrate_mode']) ? check_plain($item['data']['audio_bitrate_mode']) : ''; + + // ID3 tags. + foreach (filefield_meta_tags() as $tag => $label) { + $tokens['filefield-tag-title'] = isset($item['data']['tags'][$tag]) ? check_plain($item['data']['tags'][$tag]) : ''; + } + } + + return $tokens; +} diff --git a/sites/all/modules/filefield/filefield_meta/includes/filefield_meta.views.inc b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta.views.inc new file mode 100644 index 0000000..431f190 --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta.views.inc @@ -0,0 +1,205 @@ + array( + 'left_table' => 'files', // Because this is a direct link it could be left out. + 'left_field' => 'fid', + 'field' => 'fid', + ), + ); + + // ---------------------------------------------------------------- + // filefield_meta table -- fields + + // width + $data['filefield_meta']['width'] = array( + 'title' => t('Video width'), + 'help' => t('Width of a video or image file in pixels.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // height + $data['filefield_meta']['height'] = array( + 'title' => t('Video height'), + 'help' => t('Height of a video or image file in pixels.'), + 'field' => array( + 'handler' => 'views_handler_field_numeric', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // duration + $data['filefield_meta']['duration'] = array( + 'title' => t('Duration'), + 'help' => t('The duration of audio or video files, in seconds.'), + 'field' => array( + 'handler' => 'filefield_meta_handler_field_duration', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // audio_format + $data['filefield_meta']['audio_format'] = array( + 'title' => t('Audio format'), + 'help' => t('The audio format.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // audio_sample_rate + $data['filefield_meta']['audio_sample_rate'] = array( + 'title' => t('Audio sample rate'), + 'help' => t('The sample rate of the audio.'), + 'field' => array( + 'handler' => 'filefield_meta_handler_field_samplerate', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // audio_channel_mode + $data['filefield_meta']['audio_channel_mode'] = array( + 'title' => t('Audio channel mode'), + 'help' => t('The number of channels in the audio, by name (stereo or mono).'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // audio_bitrate + $data['filefield_meta']['audio_bitrate'] = array( + 'title' => t('Audio bitrate'), + 'help' => t('The audio bitrate.'), + 'field' => array( + 'handler' => 'filefield_meta_handler_field_bitrate', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_numeric', + ), + ); + + // audio_bitrate_mode + $data['filefield_meta']['audio_bitrate_mode'] = array( + 'title' => t('Audio bitrate mode'), + 'help' => t('The kind of audio bitrate, such as VBR. Usually empty.'), + 'field' => array( + 'handler' => 'views_handler_field', + 'click sortable' => TRUE, + ), + 'sort' => array( + 'handler' => 'views_handler_sort', + ), + 'filter' => array( + 'handler' => 'views_handler_filter_string', + ), + ); + + // Tags. + $data['filefield_meta']['tags'] = array( + 'title' => t('ID3 tags'), + 'help' => t('ID3 tags include embedded information such as artist, album, year, genre and other information.'), + 'field' => array( + 'handler' => 'filefield_meta_handler_field_tags', + 'click sortable' => FALSE, + ), + ); + + return $data; +} + +/** + * Implementation of hook_views_handlers(). + */ +function filefield_meta_views_handlers() { + return array( + 'info' => array( + 'path' => drupal_get_path('module', 'filefield_meta') . '/includes', + ), + 'handlers' => array( + // field handlers + 'filefield_meta_handler_field_bitrate' => array( + 'parent' => 'views_handler_field_numeric', + ), + 'filefield_meta_handler_field_duration' => array( + 'parent' => 'views_handler_field_numeric', + ), + 'filefield_meta_handler_field_samplerate' => array( + 'parent' => 'views_handler_field_numeric', + ), + 'filefield_meta_handler_field_tags' => array( + 'parent' => 'views_handler_field_prerender_list', + ), + ), + ); +} + +/** + * @} + */ diff --git a/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_bitrate.inc b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_bitrate.inc new file mode 100644 index 0000000..2b94c6c --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_bitrate.inc @@ -0,0 +1,62 @@ + 'default', 'translatable' => TRUE); + + // Remove the separator options since we don't need them. + unset($options['separator']); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Remove the separator and alter options since we don't need them. + unset($form['separator']); + + $form['prefix']['#weight'] = 10; + $form['suffix']['#weight'] = 10; + $form['format'] = array( + '#type' => 'select', + '#title' => t('Format'), + '#default_value' => $this->options['format'], + '#options' => array( + 'default' => t('Default (Mbps or Kbps)'), + 'raw' => t('Raw numberic value'), + ), + ); + } + + function render($values) { + $value = $values->{$this->field_alias}; + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + switch ($this->options['format']) { + case 'raw': + $output = $value; + break; + default: + $output = theme('filefield_meta_bitrate', $value); + } + + return check_plain($this->options['prefix']) . $output . check_plain($this->options['suffix']); + } +} diff --git a/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_duration.inc b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_duration.inc new file mode 100644 index 0000000..7a7f097 --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_duration.inc @@ -0,0 +1,74 @@ + 'default', 'translatable' => TRUE); + + // Remove the separator options since we don't need them. + unset($options['separator']); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Remove the separator options since we don't need them. + unset($form['separator']); + + $form['prefix']['#weight'] = 10; + $form['suffix']['#weight'] = 10; + $form['format'] = array( + '#type' => 'select', + '#title' => t('Time format'), + '#default_value' => $this->options['format'], + '#options' => array( + 'default' => t('Default (usually mm:ss)'), + 'hours' => t('Hours: h'), + 'minutes' => t('Minutes: mm'), + 'seconds' => t('Seconds: ss'), + 'total' => t('Total seconds'), + ), + ); + } + + function render($values) { + $value = $values->{$this->field_alias}; + + switch ($this->options['format']) { + case 'hours': + $output = date('g', (int) $value); + break; + case 'minutes': + $output = date('i', (int) $value); + break; + case 'seconds': + $output = date('s', (int) $value); + break; + case 'total': + $output = check_plain($value); + break; + default: + $output = theme('filefield_meta_duration', $value); + } + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + return check_plain($this->options['prefix']) . $output . check_plain($this->options['suffix']); + } +} diff --git a/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_samplerate.inc b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_samplerate.inc new file mode 100644 index 0000000..28cafd6 --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_samplerate.inc @@ -0,0 +1,62 @@ + 'default', 'translatable' => TRUE); + + // Remove the separator options since we don't need them. + unset($options['separator']); + + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + // Remove the separator options since we don't need them. + unset($form['separator']); + + $form['prefix']['#weight'] = 10; + $form['suffix']['#weight'] = 10; + $form['format'] = array( + '#type' => 'select', + '#title' => t('Format'), + '#default_value' => $this->options['format'], + '#options' => array( + 'default' => t('Default (kHz)'), + 'raw' => t('Raw numberic value'), + ), + ); + } + + function render($values) { + $value = $values->{$this->field_alias}; + + // Check to see if hiding should happen before adding prefix and suffix. + if ($this->options['hide_empty'] && empty($value) && ($value !== 0 || $this->options['empty_zero'])) { + return ''; + } + + switch ($this->options['format']) { + case 'raw': + $output = $value; + break; + default: + $output = theme('filefield_meta_samplerate', $value); + } + + return check_plain($this->options['prefix']) . $output . check_plain($this->options['suffix']); + } +} diff --git a/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_tags.inc b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_tags.inc new file mode 100644 index 0000000..79c4535 --- /dev/null +++ b/sites/all/modules/filefield/filefield_meta/includes/filefield_meta_handler_field_tags.inc @@ -0,0 +1,43 @@ + $default, 'required' => TRUE, 'translatable' => TRUE); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $form['relationship']['#weight'] = -2; + $form['tag'] = array( + '#type' => 'select', + '#title' => t('ID3 tag'), + '#required' => TRUE, + '#default_value' => $this->options['tag'], + '#options' => filefield_meta_tags(), + '#description' => t('Select the tag to be rendered. If needing multiple tags, add another ID3 tags field.'), + '#weight' => -1, + ); + } + + function render($values) { + $value = unserialize($values->{$this->field_alias}); + $tag = $this->options['tag']; + if (isset($value[$tag])) { + return check_plain($value[$tag]); + } + } +} diff --git a/sites/all/modules/filefield/filefield_widget.inc b/sites/all/modules/filefield/filefield_widget.inc new file mode 100644 index 0000000..7ad2448 --- /dev/null +++ b/sites/all/modules/filefield/filefield_widget.inc @@ -0,0 +1,588 @@ + 'textfield', + '#title' => t('Permitted upload file extensions'), + '#default_value' => $extensions, + '#size' => 64, + '#maxlength' => 512, + '#description' => t('Extensions a user can upload to this field. Separate extensions with a space and do not include the leading dot. Leaving this blank will allow users to upload a file with any extension.'), + '#element_validate' => array('_filefield_widget_settings_extensions_validate'), + '#pre_render' => array('_filefield_widget_settings_extensions_value'), + '#weight' => 1, + ); + + $form['progress_indicator'] = array( + '#type' => 'radios', + '#title' => t('Progress indicator'), + '#options' => array( + 'bar' => t('Bar with progress meter'), + 'throbber' => t('Throbber'), + ), + '#default_value' => empty($widget['progress_indicator']) ? 'bar' : $widget['progress_indicator'], + '#description' => t('Your server supports upload progress capabilities. The "throbber" display does not indicate progress but takes up less room on the form, you may want to use it if you\'ll only be uploading small files or if experiencing problems with the progress bar.'), + '#weight' => 5, + '#access' => filefield_progress_implementation(), + ); + + $form['path_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Path settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#weight' => 6, + ); + $form['path_settings']['file_path'] = array( + '#type' => 'textfield', + '#title' => t('File path'), + '#default_value' => is_string($widget['file_path']) ? $widget['file_path'] : '', + '#description' => t('Optional subdirectory within the "%directory" directory where files will be stored. Do not include preceding or trailing slashes.', array('%directory' => variable_get('file_directory_path', 'files') . '/')), + '#element_validate' => array('_filefield_widget_settings_file_path_validate'), + '#suffix' => theme('token_help', 'user'), + ); + + $form['max_filesize'] = array( + '#type' => 'fieldset', + '#title' => t('File size restrictions'), + '#description' => t('Limits for the size of files that a user can upload. Note that these settings only apply to newly uploaded files, whereas existing files are not affected.'), + '#weight' => 6, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['max_filesize']['max_filesize_per_file'] = array( + '#type' => 'textfield', + '#title' => t('Maximum upload size per file'), + '#default_value' => is_string($widget['max_filesize_per_file']) + ? $widget['max_filesize_per_file'] + : '', + '#description' => t('Specify the size limit that applies to each file separately. Enter a value like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes) in order to restrict the allowed file size. If you leave this 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()))), + '#element_validate' => array('_filefield_widget_settings_max_filesize_per_file_validate'), + ); + $form['max_filesize']['max_filesize_per_node'] = array( + '#type' => 'textfield', + '#title' => t('Maximum upload size per node'), + '#default_value' => is_string($widget['max_filesize_per_node']) + ? $widget['max_filesize_per_node'] + : '', + '#description' => t('Specify the total size limit for all files in field on a given node. Enter a value like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes) in order to restrict the total size of a node. Leave this empty if there should be no size restriction.'), + '#element_validate' => array('_filefield_widget_settings_max_filesize_per_node_validate'), + ); + + return $form; +} + +/** + * Implementation of CCK's hook_widget_settings($op == 'save'). + */ +function filefield_widget_settings_save($widget) { + return array('file_extensions', 'file_path', 'progress_indicator', 'max_filesize_per_file', 'max_filesize_per_node'); +} + +/** + * A FAPI #pre_render() function to set a cosmetic default value for extensions. + */ +function _filefield_widget_settings_extensions_value($element) { + $element['#value'] = implode(', ', array_filter(explode(' ', str_replace(',', ' ', $element['#value'])))); + return $element; +} + +/** + * A FAPI #element_validate callback to strip commas from extension lists. + */ +function _filefield_widget_settings_extensions_validate($element, &$form_state) { + // Remove commas and leading dots from file extensions. + $value = str_replace(',', ' ', $element['#value']); + $value = str_replace(' .', ' ', $value); + $value = array_filter(explode(' ', $value)); + $value = implode(' ', $value); + form_set_value($element, $value, $form_state); +} + +function _filefield_widget_settings_file_path_validate($element, &$form_state) { + // Strip slashes from the beginning and end of $widget['file_path'] + $form_state['values']['file_path'] = trim($form_state['values']['file_path'], '\\/'); + + // Do not allow the file path to be the same as the file_directory_path(). + // This causes all sorts of problems with things like file_create_url(). + if (strpos($form_state['values']['file_path'], file_directory_path()) === 0) { + form_error($element, t('The file path (@file_path) cannot start with the system files directory (@files_directory), as this may cause conflicts when building file URLs.', array('@file_path' => $form_state['values']['file_path'], '@files_directory' => file_directory_path()))); + } +} + +function _filefield_widget_settings_max_filesize_per_file_validate($element, &$form_state) { + if (empty($form_state['values']['max_filesize_per_file'])) { + return; // Empty means no size restrictions, so don't throw an error. + } + elseif (!is_numeric(parse_size($form_state['values']['max_filesize_per_file']))) { + form_error($element, t('The "@field" option must contain a valid value. You can either leave the text field empty or enter a string like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes).', array('@field' => t('Maximum upload size per file')))); + } +} + +function _filefield_widget_settings_max_filesize_per_node_validate($element, &$form_state) { + if (empty($form_state['values']['max_filesize_per_node'])) { + return; // Empty means no size restrictions, so don't throw an error. + } + elseif (!is_numeric(parse_size($form_state['values']['max_filesize_per_node']))) { + form_error($element, t('The "@field" option must contain a valid value. You can either leave the text field empty or enter a string like "512" (bytes), "80K" (kilobytes) or "50M" (megabytes).', array('@field' => t('Maximum upload size per node')))); + } +} + +/** + * Determine the widget's files directory + * + * @param $field + * A CCK field array. + * @param $account + * The user account object to calculate the file path for. + * @return + * The files directory path, with any tokens replaced. + */ +function filefield_widget_file_path($field, $account = NULL) { + $account = isset($account) ? $account : $GLOBALS['user']; + $dest = $field['widget']['file_path']; + // Replace user level tokens. + // Node level tokens require a lot of complexity like temporary storage + // locations when values don't exist. See the filefield_paths module. + if (module_exists('token')) { + $dest = token_replace($dest, 'user', $account); + } + // Replace nasty characters in the path if possible. + if (module_exists('transliteration')) { + module_load_include('inc', 'transliteration'); + $dest_array = array_filter(explode('/', $dest)); + foreach ($dest_array as $key => $directory) { + $dest_array[$key] = transliteration_clean_filename($directory); + } + $dest = implode('/', $dest_array); + } + + return file_directory_path() .'/'. $dest; +} + +/** + * Given a FAPI element, save any files that may have been uploaded into it. + * + * This function should only be called during validate, submit, or + * value_callback functions. + * + * @param $element + * The FAPI element whose values are being saved. + */ +function filefield_save_upload($element) { + $upload_name = implode('_', $element['#array_parents']); + $field = content_fields($element['#field_name'], $element['#type_name']); + + if (empty($_FILES['files']['name'][$upload_name])) { + return 0; + } + + $dest = filefield_widget_file_path($field); + if (!field_file_check_directory($dest, FILE_CREATE_DIRECTORY)) { + watchdog('filefield', 'The upload directory %directory for the file field %field (content type %type) 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' => $dest, '%field' => $element['#field_name'], '%type' => $element['#type_name'])); + form_set_error($upload_name, t('The file could not be uploaded.')); + return 0; + } + + if (!$file = field_file_save_upload($upload_name, $element['#upload_validators'], $dest)) { + watchdog('filefield', 'The file upload failed. %upload', array('%upload' => $upload_name)); + form_set_error($upload_name, t('The file in the @field field was unable to be uploaded.', array('@field' => $element['#title']))); + return 0; + } + return $file['fid']; +} + +/** + * The #value_callback for the filefield_widget type element. + */ +function filefield_widget_value($element, $edit = FALSE) { + if (!$edit) { + $file = field_file_load($element['#default_value']['fid']); + $item = $element['#default_value']; + } + else { + $item = $edit; + $field = content_fields($element['#field_name'], $element['#type_name']); + + // Uploads take priority over value of fid text field. + if ($fid = filefield_save_upload($element)) { + $item['fid'] = $fid; + } + // 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 FileField to be extended purely through FAPI. + elseif (isset($element['#filefield_value_callback'])) { + foreach ($element['#filefield_value_callback'] as $callback) { + $callback($element, $item); + } + } + + // Load file if the FID has changed so that it can be saved by CCK. + $file = isset($item['fid']) ? field_file_load($item['fid']) : NULL; + + // If the file entry doesn't exist, don't save anything. + if (empty($file)) { + $item = array(); + $file = array(); + } + + // Checkboxes loose their value when empty. + // If the list field is present make sure its unchecked value is saved. + if (!empty($field['list_field']) && empty($edit['list'])) { + $item['list'] = 0; + } + } + // Merge file and item data so it is available to all widgets. + if (isset($item['data']) && isset($file['data'])) { + $file['data'] = array_merge($item['data'], $file['data']); + } + $item = array_merge($item, $file); + + return $item; +} + +/** + * An element #process callback for the filefield_widget field type. + * + * Expands the filefield_widget type to include the upload field, upload and + * remove buttons, and the description field. + */ +function filefield_widget_process($element, $edit, &$form_state, $form) { + static $settings_added; + + $item = $element['#value']; + $field_name = $element['#field_name']; + $delta = $element['#delta']; + $element['#theme'] = 'filefield_widget_item'; + + $field = $form['#field_info'][$field_name]; + + // The widget is being presented, so apply the JavaScript. + drupal_add_js(drupal_get_path('module', 'filefield') .'/filefield.js'); + if (!isset($settings_added[$field_name]) && isset($element['#upload_validators']['filefield_validate_extensions'])) { + $settings_added[$field_name] = TRUE; + $settings = array( + 'filefield' => array( + $field_name => $element['#upload_validators']['filefield_validate_extensions'][0], + ), + ); + drupal_add_js($settings, 'setting'); + } + + // Title is not necessary for each individual field. + if ($field['multiple'] > 0) { + unset($element['#title']); + } + + // Set up the buttons first since we need to check if they were clicked. + $element['filefield_upload'] = array( + '#type' => 'submit', + '#value' => t('Upload'), + '#submit' => array('node_form_submit_build_node'), + '#ahah' => array( // with JavaScript + 'path' => 'filefield/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'], + 'wrapper' => $element['#id'] .'-ahah-wrapper', + 'method' => 'replace', + 'effect' => 'fade', + ), + '#field_name' => $element['#field_name'], + '#delta' => $element['#delta'], + '#type_name' => $element['#type_name'], + '#upload_validators' => $element['#upload_validators'], + '#weight' => 100, + '#post' => $element['#post'], + ); + $element['filefield_remove'] = array( + // With default CCK edit forms, $element['#parents'] is array($element['#field_name'], $element['#delta']). + // However, if some module (for example, flexifield) places our widget deeper in the tree, we want to + // use that information in constructing the button name. + '#name' => implode('_', $element['#parents']) .'_filefield_remove', + '#type' => 'submit', + '#value' => t('Remove'), + '#submit' => array('node_form_submit_build_node'), + '#ahah' => array( // with JavaScript + 'path' => 'filefield/ahah/'. $element['#type_name'] .'/'. $element['#field_name'] .'/'. $element['#delta'], + 'wrapper' => $element['#id'] .'-ahah-wrapper', + 'method' => 'replace', + 'effect' => 'fade', + ), + '#field_name' => $element['#field_name'], + '#delta' => $element['#delta'], + '#weight' => 101, + '#post' => $element['#post'], + ); + + // Because the output of this field changes depending on the button clicked, + // we need to ask FAPI immediately if the remove button was clicked. + // It's not good that we call this private function, but + // $form_state['clicked_button'] is only available after this #process + // callback is finished. + if (_form_button_was_clicked($element['filefield_remove'])) { + // Delete the file if it is currently unused. Note that field_file_delete() + // does a reference check in addition to our basic status check. + if (isset($edit['fid'])) { + $removed_file = field_file_load($edit['fid']); + // Users are only allowed to remove their own files. + if ($removed_file['status'] == 0 && $GLOBALS['user']->uid == $removed_file['uid']) { + field_file_delete($removed_file); + } + } + $item = array('fid' => 0, 'list' => $field['list_default'], 'data' => array('description' => '')); + } + + // Add progress bar support to the upload if possible. + $progress_indicator = isset($field['widget']['progress_indicator']) ? $field['widget']['progress_indicator'] : 'bar'; + if ($progress_indicator != 'throbber' && $implementation = filefield_progress_implementation()) { + $upload_progress_key = md5(mt_rand()); + + if ($implementation == 'uploadprogress') { + $element['UPLOAD_IDENTIFIER'] = array( + '#type' => 'hidden', + '#value' => $upload_progress_key, + '#attributes' => array('class' => 'filefield-progress'), + ); + } + elseif ($implementation == 'apc') { + $element['APC_UPLOAD_PROGRESS'] = array( + '#type' => 'hidden', + '#value' => $upload_progress_key, + '#attributes' => array('class' => 'filefield-progress'), + ); + } + + // Add the upload progress callback. + $element['filefield_upload']['#ahah']['progress']['type'] = 'bar'; + $element['filefield_upload']['#ahah']['progress']['path'] = 'filefield/progress/' . $upload_progress_key; + } + + // Set the FID. + $element['fid'] = array( + '#type' => 'hidden', + '#value' => isset($item['fid']) ? $item['fid'] : 0, + ); + + if (!empty($item['fid'])) { + if (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE && function_exists('file_download_access') && !empty($item['status']) && !file_download_access($item['filepath'])) { + $element['preview'] = array( + '#type' => 'markup', + '#value' => t('Access denied'), + ); + } + else { + $element['preview'] = array( + '#type' => 'markup', + '#value' => theme('filefield_widget_preview', $item), + ); + } + } + + // Grant access to temporary files. + if (!empty($item['fid']) && isset($item['status']) && $item['status'] == 0 && variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE) { + $_SESSION['filefield_access'][] = $item['fid']; + } + + // placeholder.. will be serialized into the data column. this is a place for widgets + // to put additional data. + $element['data'] = array( + '#tree' => 'true', + '#access' => !empty($item['fid']), + ); + + if (!empty($field['description_field'])) { + $element['data']['description'] = array( + '#title' => t('Description'), + '#value' => isset($item['data']['description']) ? $item['data']['description'] : '', + '#type' => variable_get('filefield_description_type', 'textfield'), + '#maxlength' => variable_get('filefield_description_length', 128), + ); + } + + if (!empty($field['list_field'])) { + $element['list'] = array( + '#type' => empty($item['fid']) ? 'hidden' : 'checkbox', + '#title' => t('List'), + '#value' => isset($item['list']) && !empty($item['fid']) ? $item['list'] : $field['list_default'], + '#attributes' => array('class' => 'filefield-list'), + ); + } + else { + $element['list'] = array( + '#type' => 'hidden', + '#value' => '1', + ); + } + + foreach ($element['#upload_validators'] as $callback => $arguments) { + $help_func = $callback .'_help'; + if (function_exists($help_func)) { + $desc[] = call_user_func_array($help_func, $arguments); + } + } + + $element['upload'] = array( + '#name' => 'files[' . implode('_', $element['#array_parents']) . ']', + '#type' => 'file', + '#description' => implode('
', $desc), + '#size' => 22, + '#access' => empty($item['fid']), + ); + + $element['#attributes']['id'] = $element['#id'] .'-ahah-wrapper'; + $element['#prefix'] = '
'; + $element['#suffix'] = '
'; + + return $element; +} + +function filefield_widget_after_build($element, &$form_state) { + // Set access on the buttons. + $element['filefield_upload']['#access'] = empty($element['fid']['#value']); + $element['filefield_remove']['#access'] = !empty($element['fid']['#value']); + return $element; +} + +/** + * An #element_validate callback for the filefield_widget field. + */ +function filefield_widget_validate(&$element, &$form_state) { + // If referencing an existing file, only allow if there are existing + // references. This prevents unmanaged files (outside of FileField) from + // being deleted if this node were to be deleted. + if (!empty($element['fid']['#value'])) { + $field = content_fields($element['#field_name'], $element['#type_name']); + if ($file = field_file_load($element['fid']['#value'])) { + $file = (object) $file; + if ($file->status == FILE_STATUS_PERMANENT) { + if (field_file_references($file) == 0 || (variable_get('file_downloads', FILE_DOWNLOADS_PUBLIC) == FILE_DOWNLOADS_PRIVATE && function_exists('file_download_access') && !file_download_access($file->filepath))) { + form_error($element, t('Referencing to the file used in the %field field is not allowed.', array('%field' => $element['#title']))); + } + } + } + else { + form_error($element, t('The file referenced by the %field field does not exist.', array('%field' => $element['#title']))); + } + } +} + +/** + * FormAPI theme function. Theme the output of an image field. + */ +function theme_filefield_widget($element) { + $element['#id'] .= '-upload'; // Link the label to the upload field. + return theme('form_element', $element, $element['#children']); +} + +function theme_filefield_widget_preview($item) { + // Remove the current description so that we get the filename as the link. + if (isset($item['data']) && is_array($item['data']) && isset($item['data']['description'])) { + unset($item['data']['description']); + } + + return '
'. + '
'. theme('filefield_file', $item) .'
'. + '
'. format_size($item['filesize']) .'
'. + '
'. $item['filemime'] .'
'. + '
'; +} + +function theme_filefield_widget_item($element) { + // Put the upload button directly after the upload field. + $element['upload']['#field_suffix'] = drupal_render($element['filefield_upload']); + $element['upload']['#theme'] = 'filefield_widget_file'; + + $output = ''; + $output .= '
'; + + if ($element['fid']['#value'] != 0) { + $output .= '
'; + $output .= drupal_render($element['preview']); + $output .= '
'; + } + + $output .= '
'; + $output .= drupal_render($element); + $output .= '
'; + $output .= '
'; + + return $output; +} + +/** + * Custom theme function for FileField upload elements. + * + * This function allows us to put the "Upload" button immediately after the + * file upload field by respecting the #field_suffix property. + */ +function theme_filefield_widget_file($element) { + $output = ''; + + $output .= '
'; + + if (isset($element['#field_prefix'])) { + $output .= $element['#field_prefix']; + } + + _form_set_class($element, array('form-file')); + $output .= '\n"; + + if (isset($element['#field_suffix'])) { + $output .= $element['#field_suffix']; + } + + $output .= '
'; + + return theme('form_element', $element, $output); +} + +/** + * Additional #validate handler for the node form. + * + * This function checks the #required properties on file fields and calculates + * node upload totals for all file fields. The #required property is not + * properly supported on file fields by Drupal core, so we do this manually. + */ +function filefield_node_form_validate($form, &$form_state) { + foreach ($form['#field_info'] as $field_name => $field) { + if (!(in_array($field['module'], array('imagefield', 'filefield')))) continue; + $empty = $field['module'] .'_content_is_empty'; + $valid = FALSE; + $total_filesize = 0; + if (!empty($form_state['values'][$field_name])) { + foreach ($form_state['values'][$field_name] as $delta => $item) { + if ($empty($item, $field)) continue; + $valid = TRUE; + $total_filesize += (int)$item['filesize']; + } + } + + if (!$valid && $field['required'] && filefield_edit_access($field['type_name'], $field_name, $form['#node'])) { + form_set_error($field_name, t('%title field is required.', array('%title' => t($field['widget']['label'])))); + } + $max_filesize = parse_size($field['widget']['max_filesize_per_node']); + if ($max_filesize && $total_filesize > $max_filesize) { + form_set_error($field_name, t('Total filesize for %title, %tsize, exceeds field settings of %msize.', + array( + '%title' => t($field['widget']['label']), + '%tsize' => format_size($total_filesize), + '%msize' => format_size($max_filesize) + ) + )); + } + } +} diff --git a/sites/all/modules/filefield/icons/application-octet-stream.png b/sites/all/modules/filefield/icons/application-octet-stream.png new file mode 100644 index 0000000..cc56d6b Binary files /dev/null and b/sites/all/modules/filefield/icons/application-octet-stream.png differ diff --git a/sites/all/modules/filefield/icons/application-pdf.png b/sites/all/modules/filefield/icons/application-pdf.png new file mode 100644 index 0000000..7263cf2 Binary files /dev/null and b/sites/all/modules/filefield/icons/application-pdf.png differ diff --git a/sites/all/modules/filefield/icons/application-x-executable.png b/sites/all/modules/filefield/icons/application-x-executable.png new file mode 100644 index 0000000..cc56d6b Binary files /dev/null and b/sites/all/modules/filefield/icons/application-x-executable.png differ diff --git a/sites/all/modules/filefield/icons/audio-x-generic.png b/sites/all/modules/filefield/icons/audio-x-generic.png new file mode 100644 index 0000000..b4777fc Binary files /dev/null and b/sites/all/modules/filefield/icons/audio-x-generic.png differ diff --git a/sites/all/modules/filefield/icons/image-x-generic.png b/sites/all/modules/filefield/icons/image-x-generic.png new file mode 100644 index 0000000..96acccf Binary files /dev/null and b/sites/all/modules/filefield/icons/image-x-generic.png differ diff --git a/sites/all/modules/filefield/icons/package-x-generic.png b/sites/all/modules/filefield/icons/package-x-generic.png new file mode 100644 index 0000000..80fa1bc Binary files /dev/null and b/sites/all/modules/filefield/icons/package-x-generic.png differ diff --git a/sites/all/modules/filefield/icons/text-html.png b/sites/all/modules/filefield/icons/text-html.png new file mode 100644 index 0000000..07ef1a2 Binary files /dev/null and b/sites/all/modules/filefield/icons/text-html.png differ diff --git a/sites/all/modules/filefield/icons/text-plain.png b/sites/all/modules/filefield/icons/text-plain.png new file mode 100644 index 0000000..7ee2c2d Binary files /dev/null and b/sites/all/modules/filefield/icons/text-plain.png differ diff --git a/sites/all/modules/filefield/icons/text-x-generic.png b/sites/all/modules/filefield/icons/text-x-generic.png new file mode 100644 index 0000000..7ee2c2d Binary files /dev/null and b/sites/all/modules/filefield/icons/text-x-generic.png differ diff --git a/sites/all/modules/filefield/icons/text-x-script.png b/sites/all/modules/filefield/icons/text-x-script.png new file mode 100644 index 0000000..4e66ed8 Binary files /dev/null and b/sites/all/modules/filefield/icons/text-x-script.png differ diff --git a/sites/all/modules/filefield/icons/video-x-generic.png b/sites/all/modules/filefield/icons/video-x-generic.png new file mode 100644 index 0000000..2743ac2 Binary files /dev/null and b/sites/all/modules/filefield/icons/video-x-generic.png differ diff --git a/sites/all/modules/filefield/icons/x-office-document.png b/sites/all/modules/filefield/icons/x-office-document.png new file mode 100644 index 0000000..4cd6b61 Binary files /dev/null and b/sites/all/modules/filefield/icons/x-office-document.png differ diff --git a/sites/all/modules/filefield/icons/x-office-presentation.png b/sites/all/modules/filefield/icons/x-office-presentation.png new file mode 100644 index 0000000..c1ca337 Binary files /dev/null and b/sites/all/modules/filefield/icons/x-office-presentation.png differ diff --git a/sites/all/modules/filefield/icons/x-office-spreadsheet.png b/sites/all/modules/filefield/icons/x-office-spreadsheet.png new file mode 100644 index 0000000..375a43a Binary files /dev/null and b/sites/all/modules/filefield/icons/x-office-spreadsheet.png differ diff --git a/sites/all/modules/filefield/tests/filefield.test b/sites/all/modules/filefield/tests/filefield.test new file mode 100644 index 0000000..7dbf977 --- /dev/null +++ b/sites/all/modules/filefield/tests/filefield.test @@ -0,0 +1,637 @@ +admin_user = $this->drupalCreateUser(array('access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'administer files')); + $this->drupalLogin($this->admin_user); + } + + /** + * Get a sample file of the specified type. + */ + function getTestFile($type, $size = NULL) { + // Get a file to upload. + $file = current($this->drupalGetTestFiles($type, $size)); + + // SimpleTest files incorrectly use "filename" instead of "filepath". + $file->filepath = $file->filename; + $file->filename = basename($file->filepath); + $file->filesize = filesize($file->filepath); + + return $file; + } + + /** + * Create a new file field. + * + * @param $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param $type + * The node type that this field will be added to. + * @param $field_options + * A list of field options that will be added to the defaults. + * @param $widget_options + * A list of widget options that will be added to the widget defaults. + */ + function createFileField($name, $type, $field_options = array(), $widget_options = array()) { + module_load_include('inc', 'content', 'includes/content.crud'); + $field = array( + 'label' => $name, + 'field_name' => $name, + 'type' => 'filefield', + 'widget_type' => 'filefield_widget', + 'weight' => 0, + 'parent' => 0, + 'type_name' => $type, + 'list_field' => 0, + 'list_default' => 1, + 'description_field' => 0, + ); + + $field = array_merge($field, $field_options); + $field = content_field_instance_create($field); + + $widget = array( + 'type' => 'filefield_widget', + 'file_extensions' => '', + ); + + $field['widget'] = array_merge($field['widget'], $widget, $widget_options); + $field = content_field_instance_update($field); + + return $field; + } + + /** + * Update an existing FileField with new settings. + */ + function updateFileField($name, $type, $field_options = array(), $widget_options = array()) { + $field = content_fields($name, $type); + $field = array_merge($field, $field_options); + $field['widget'] = array_merge($field['widget'], $widget_options); + + return content_field_instance_update($field); + } + + /** + * Upload a file to a node. + */ + function uploadNodeFile($file, $field, $nid_or_type, $new_revision = TRUE) { + $field_name = $field['field_name']; + $edit = array( + 'title' => $this->randomName(), + 'revision' => (string) (int) $new_revision, + ); + + if (is_numeric($nid_or_type)) { + $node = node_load($nid_or_type); + $delta = isset($node->$field_name) ? count($node->$field_name) : 0; + $edit['files[' . $field_name . '_' . $delta . ']'] = realpath($file->filepath); + $this->drupalPost('node/' . $nid_or_type . '/edit', $edit, t('Save')); + } + else { + $delta = '0'; + $edit['files[' . $field_name . '_' . $delta . ']'] = realpath($file->filepath); + $type = str_replace('_', '-', $nid_or_type); + $this->drupalPost('node/add/' . $type, $edit, t('Save')); + } + + $matches = array(); + preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches); + $nid = isset($matches[1]) ? $matches[1] : FALSE; + + // Edit the node and add a description if possible. + if ($nid && $field['description_field']) { + $edit = array( + 'revision' => 0, + $field_name . '[' . $delta . '][data][description]' => $this->randomString(), + ); + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); + } + + return $nid; + } + + /** + * Remove 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')); + } + + /** + * Replace a file within a node. + */ + function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) { + $edit = array( + 'files[' . $field_name . '_0]' => realpath($file->filepath), + 'revision' => (string) (int) $new_revision, + ); + + $this->drupalPost('node/' . $nid . '/edit', array(), t('Remove')); + $this->drupalPost(NULL, $edit, t('Save')); + } + + /** + * Assert that a file exists physically on disk. + */ + function assertFileExists($file, $message = NULL) { + $message = isset($message) ? $message : t('File %file exists on the disk.', array('%file' => $file['filepath'])); + $this->assertTrue(is_file($file['filepath']), $message); + } + + /** + * Assert that a file exists in the database. + */ + function assertFileEntryExists($file, $message = NULL) { + module_load_include('inc', 'filefield', 'field_file'); + $db_file = field_file_load($file['fid'], TRUE); + $message = isset($message) ? $message : t('File %file exists in database at the correct path.', array('%file' => $file['filepath'])); + $this->assertEqual($db_file['filepath'], $file['filepath'], $message); + } + + /** + * Assert that a file does not exist on disk. + */ + function assertFileNotExists($file, $message = NULL) { + $message = isset($message) ? $message : t('File %file exists on the disk.', array('%file' => $file['filepath'])); + $this->assertFalse(is_file($file['filepath']), $message); + } + + /** + * Assert that a file does not exist in the database. + */ + function assertFileEntryNotExists($file, $message) { + module_load_include('inc', 'filefield', 'field_file'); + $message = isset($message) ? $message : t('File %file exists in database at the correct path.', array('%file' => $file['filepath'])); + $this->assertFalse(field_file_load($file['fid'], TRUE), $message); + } +} + +/** + * Test class to test file handling with node revisions. + */ +class FileFieldRevisionTestCase extends FileFieldTestCase { + function getInfo() { + return array( + 'name' => t('FileField revision test'), + 'description' => t('Test creating and deleting revisions with files attached.'), + 'group' => t('FileField'), + ); + } + + /** + * Test creating multiple revisions of a node and managing the 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() { + $field_name = 'field_' . strtolower($this->randomName()); + $type = $this->drupalCreateContentType(); + $field_options = array( + 'description_field' => '1', + ); + $field = $this->createFileField($field_name, $type->name, $field_options); + + $test_file = $this->getTestFile('text'); + + // Create a new node with the uploaded file. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + + // Check that the file exists on disk and in the database. + $node = node_load($nid, NULL, TRUE); + $node_file_r1 = $node->{$field['field_name']}[0]; + $node_vid_r1 = $node->vid; + $this->assertFileExists($node_file_r1, t('New file saved to disk on node creation.')); + $this->assertFileEntryExists($node_file_r1, t('File entry exists in database on node creation.')); + + // 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 = $node->{$field['field_name']}[0]; + $node_vid_r2 = $node->vid; + $this->assertFileExists($node_file_r2, t('Replacement file exists on disk after creating new revision.')); + $this->assertFileEntryExists($node_file_r2, t('Replacement file entry exists in database after creating new revision.')); + + // 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, $node->{$field['field_name']}[0], t('Original file still in place after replacing file in new revision.')); + $this->assertFileExists($node_file_r1, t('Original file still in place after replacing file in new revision.')); + $this->assertFileEntryExists($node_file_r1, t('Original file entry still in place after replacing file in new revision')); + + // 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 = $node->{$field['field_name']}[0]; + $node_vid_r3 = $node->vid; + + // FileField Meta's extensive meta data can be difficult to match up exactly + // (mostly differences between strings and integers). Just compare the + // descriptions. + $node_file_r2['data'] = array('description' => $node_file_r2['data']['description']); + $node_file_r3['data'] = array('description' => $node_file_r3['data']['description']); + $this->assertEqual($node_file_r2, $node_file_r3, t('Previous revision file still in place after creating a new revision without a new file.')); + + // 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 = $node->{$field['field_name']}[0]; + $node_vid_r4 = $node->vid; + $this->assertEqual($node_file_r1, $node_file_r4, t('Original revision file still in place 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, t('Second file is still available after deleting second revision, since it is being used by the third revision.')); + $this->assertFileEntryExists($node_file_r3, t('Second file entry is still available after deleting second revision, since it is being used by the third revision.')); + + // Delete the third revision and check that the file is deleted also. + $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', array(), t('Delete')); + $this->assertFileNotExists($node_file_r3, t('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, t('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, t('Original file is deleted after deleting the entire node with two revisions remaining.')); + $this->assertFileEntryNotExists($node_file_r1, t('Original file entry is deleted after deleting the entire node with two revisions remaining.')); + } +} + +/** + * Test class to check that formatters are working properly. + */ +class FileFieldDisplayTestCase extends FileFieldTestCase { + function getInfo() { + return array( + 'name' => t('FileField display tests'), + 'description' => t('Test the display of file fields in node and views.'), + 'group' => t('FileField'), + ); + } + + /** + * Test normal formatter display on node display. + */ + function testNodeDisplay() { + $field_name = 'field_' . strtolower($this->randomName()); + $type = $this->drupalCreateContentType(); + $field_options = array( + 'description_field' => '1', + 'list_field' => '1', + 'list_default' => '1', + ); + $field = $this->createFileField($field_name, $type->name, $field_options); + $test_file = $this->getTestFile('text'); + + // Create a new node with the uploaded file. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + $this->drupalGet('node/' . $nid); + + // Check that the default formatter is displaying with the file name. + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $default_output = theme('filefield_file', $node_file); + $this->assertRaw($default_output, t('Default formatter displaying correctly on full node view.')); + + // Turn the "list" option off and check that the file is no longer listed. + $edit = array($field['field_name'] . '[0][list]' => FALSE); + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); + + $this->assertNoRaw($default_output, t('Field is hidden when "list" option is unchecked.')); + + } +} + +/** + * Test class to check for various validations. + */ +class FileFieldValidateTestCase extends FileFieldTestCase { + protected $field; + protected $node_type; + + function getInfo() { + return array( + 'name' => t('FileField validation tests'), + 'description' => t('Tests validation functions such as file type, max file size, max size per node, and required.'), + 'group' => t('FileField'), + ); + } + + /** + * Implementation of setUp(). + */ + function setUp() { + $modules = array_merge(func_get_args(), array('content', 'filefield', 'filefield_meta', 'getid3', 'mimedetect', 'token', 'views')); + call_user_func_array(array('parent', 'setUp'), $modules); + + $this->node_type = $this->drupalCreateContentType(); + $this->node_type->url_name = str_replace('_', '-', $this->node_type->name); + $field_name = 'field_' . strtolower($this->randomName()); + $this->field = $this->createFileField($field_name, $this->node_type->name); + } + + /** + * Test required property on file fields. + */ + function testRequired() { + $type = $this->node_type; + $field = $this->field; + + // Make our field required. + $this->updateFileField($field['field_name'], $type->name, array('required' => '1')); + + $test_file = $this->getTestFile('image'); + + // Try to post a new node without uploading a file. + $edit = array('title' => $this->randomName()); + $this->drupalPost('node/add/' . $type->url_name, $edit, t('Save')); + + $this->assertRaw(t('%title field is required.', array('%title' => $field['widget']['label'])), t('Node save failed when required file field was empty.')); + + // Create a new node with the uploaded file. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading to the required field.')); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading to the required field.')); + + // Try again with a multiple value field. + $this->updateFileField($field['field_name'], $type->name, array('multiple' => '0', '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->url_name, $edit, t('Save')); + + $this->assertRaw(t('%title field is required.', array('%title' => $field['widget']['label'])), t('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, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading to the required multiple value field.')); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading to the required multipel value field.')); + + // Set the field back to not required. + $this->updateFileField($field['field_name'], $type->name, array('multiple' => '0', 'required' => '1')); + } + + /** + * Test the max file size validator. + */ + function testFileMaxSize() { + $type = $this->node_type; + $field = $this->field; + + $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_per_file => $file_limit) { + // Set the max file upload size. + $this->updateFileField($field['field_name'], $type->name, array(), array('max_filesize_per_file' => $max_filesize_per_file)); + + // Create a new node with the small file, which should pass. + $nid = $this->uploadNodeFile($small_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize_per_file))); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) under the max limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_filesize_per_file))); + + // Check that uploading the large file fails (1M limit). + $nid = $this->uploadNodeFile($large_file, $field, $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, t('Node save failed when file (%filesize) exceeded the max upload size (%maxsize).', array('%filesize' => format_size($large_file->filesize), '%maxsize' => $max_filesize_per_file))); + } + + // Turn off the max filesize. + $this->updateFileField($field['field_name'], $type->name, array(), array('max_filesize_per_file' => '')); + + // Upload the big file successfully. + $nid = $this->uploadNodeFile($large_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) with no max limit.', array('%filesize' => format_size($large_file->filesize)))); + } + + /** + * Test the max file size per node validator. + */ + function testNodeMaxSize() { + $type = $this->node_type; + $field = $this->field; + + $small_file = $this->getTestFile('text', 131072); // 128KB. + $large_file = $this->getTestFile('text', 1310720); // 1.2MB + + // Set the max file upload size. + $max_node_limit = '256K'; + $file_limit = 262144; + $this->updateFileField($field['field_name'], $type->name, array('multiple' => '1'), array('max_filesize_per_node' => $max_node_limit)); + + // Create a new node with the small file, which should pass. + $nid = $this->uploadNodeFile($small_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) under the max node limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_node_limit))); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) under the max node limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_node_limit))); + + // Add a second file to the same node which should pass. + $nid = $this->uploadNodeFile($small_file, $field, $nid); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file (%filesize) under the max node limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_node_limit))); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file (%filesize) under the max node limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_node_limit))); + + // Add a third file to the same node which should fail. + $nid = $this->uploadNodeFile($small_file, $field, $nid); + $error_message = t('exceeds field settings of %msize.', array('%msize' => format_size($file_limit))); + $this->assertRaw($error_message, t('File not uploaded as the file (%filesize) exceeds the max node limit (%maxsize).', array('%filesize' => format_size($small_file->filesize), '%maxsize' => $max_node_limit))); + + // Check that uploading the large file fails (1M limit). + $nid = $this->uploadNodeFile($large_file, $field, $type->name); + $error_message = t('exceeds field settings of %msize.', array('%msize' => format_size($file_limit))); + $this->assertRaw($error_message, t('File not uploaded as the file (%filesize) exceeds the max node limit (%maxsize).', array('%filesize' => format_size($large_file->filesize), '%maxsize' => $max_node_limit))); + + // Turn off the max filesize per node. + $this->updateFileField($field['field_name'], $type->name, array('multiple' => '0'), array('max_filesize_per_node' => '')); + } + + /** + * Test the file extension, do additional checks if mimedetect is installed. + */ + function testFileExtension() { + $type = $this->node_type; + $field = $this->field; + + // Setup files for extension checking. + $test_file = $this->getTestFile('image'); + preg_match('/(?<=\.)[^\.]*$/', $test_file->filename, $matches); + $extention = current($matches); + $wrong_extension_file = drupal_clone($test_file); + $wrong_extension_file->filename = str_replace(".$extention", '.jpg', $test_file->filename); + $wrong_extension_file->filepath = file_directory_path() .'/'. $wrong_extension_file->filename; + $original_path = $test_file->filepath; + file_copy($original_path, $wrong_extension_file->filepath); + + $this->updateFileField($field['field_name'], $type->name, array(), array('file_extensions' => '')); + + // Check that the file can be uploaded with no extension checking. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file with no extension checking.')); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file with no extension checking.')); + + // Enable extension checking. + $this->updateFileField($field['field_name'], $type->name, array(), array('file_extensions' => "txt png jpg $extention")); + + // Check that the file can be uploaded with extension checking. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertFileExists($node_file, t('File exists after uploading a file with extension checking.')); + $this->assertFileEntryExists($node_file, t('File entry exists after uploading a file with extension checking.')); + + // Check that a mismatched extension cannot be uploaded. + $mimedetect = FALSE; + if (module_exists('mimedetect')) { + $mimedetect = TRUE; + module_load_include('install', 'mimedetect'); + if (!extension_loaded('fileinfo')) { + variable_set('mimedetect_enable_file_binary', 1); + } + $requirements = mimedetect_requirements('runtime'); + foreach ($requirements as $requirement) { + if (isset($requirement['severity']) && $requirement['severity'] == REQUIREMENT_ERROR) { + $mimedetect = FALSE; + } + } + } + if ($mimedetect) { + $this->uploadNodeFile($wrong_extension_file, $field, $type->name); + $error_pattern = "/The file contents \([a-z\-\/]+\) do not match its extension \([a-z]+\)\./"; + $this->assertPattern($error_pattern, t('File prevented from uploading because its extension does not match its content.')); + } + else { + $this->assertTrue(TRUE, t('Mime type checking and extension spoofing skipped because the mimedetect module is not available.')); + } + + // Disable the extension checking. + $this->updateFileField($field['field_name'], $type->name, array(), array('file_extensions' => '')); + } +} + +/** + * Test class to check that files are uploaded to proper locations. + */ +class FileFieldPathTestCase extends FileFieldTestCase { + function getInfo() { + return array( + 'name' => t('FileField file path tests'), + 'description' => t('Test that files are uploaded to the proper location, extra testing if Token module is available.'), + 'group' => t('FileField'), + ); + } + + /** + * Test normal formatter display on node display. + */ + function testUploadPath() { + $field_name = 'field_' . strtolower($this->randomName()); + $type = $this->drupalCreateContentType(); + $field = $this->createFileField($field_name, $type->name); + $test_file = $this->getTestFile('text'); + + // Create a new node. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + + // Check that the file was uploaded to the file root. + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertPathMatch(file_directory_path() . '/' . $test_file->filename, $node_file['filepath'], t('The file %file was uploaded to the correct path.', array('%file' => $node_file['filepath']))); + + // Change the path to contain multiple subdirectories. + $field = $this->updateFileField($field['field_name'], $type->name, array(), array('file_path' => 'foo/bar/baz')); + + // Upload a new file into the subdirectories. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + + // Check that the file was uploaded into the subdirectory. + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $this->assertPathMatch(file_directory_path() . '/foo/bar/baz/' . $test_file->filename, $node_file['filepath'], t('The file %file was uploaded to the correct path.', array('%file' => $node_file['filepath']))); + + // Check the path when used with tokens. + if (module_exists('token')) { + // Change the path to contain multiple token directories. + $field = $this->updateFileField($field['field_name'], $type->name, array(), array('file_path' => '[uid]/[user-raw]')); + + // Upload a new file into the token subdirectories. + $nid = $this->uploadNodeFile($test_file, $field, $type->name); + + // Check that the file was uploaded into the subdirectory. + $node = node_load($nid, NULL, TRUE); + $node_file = $node->{$field['field_name']}[0]; + $subdirectory = token_replace('[uid]/[user-raw]', 'user', $this->admin_user); + $this->assertPathMatch(file_directory_path() . '/' . $subdirectory . '/' . $test_file->filename, $node_file['filepath'], t('The file %file was uploaded to the correct path with token replacements.', array('%file' => $node_file['filepath']))); + } + else { + $this->assertTrue(TRUE, t('File path token test skipped because the Token module is not available.')); + } + + } + + /** + * A loose assertion to check 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); + } +} diff --git a/sites/all/modules/filefield/views/filefield.views.inc b/sites/all/modules/filefield/views/filefield.views.inc new file mode 100644 index 0000000..f35dc11 --- /dev/null +++ b/sites/all/modules/filefield/views/filefield.views.inc @@ -0,0 +1,57 @@ + t('Icon'), + 'help' => t('An icon corresponding to the file MIME type.'), + 'real field' => 'filemime', + 'field' => array( + 'handler' => 'filefield_handler_field_icon', + 'click sortable' => FALSE, + ), + ); + + return $data; +} + +/** + * Implementation of hook_views_handlers(). + */ +function filefield_views_handlers() { + return array( + 'info' => array( + 'path' => drupal_get_path('module', 'filefield') . '/views', + ), + 'handlers' => array( + // field handlers + 'filefield_handler_field_data' => array( + 'parent' => 'views_handler_field_node', + ), + 'filefield_handler_field_icon' => array( + 'parent' => 'views_handler_field', + ), + ), + ); +} + +/** + * @} + */ diff --git a/sites/all/modules/filefield/views/filefield.views_convert.inc b/sites/all/modules/filefield/views/filefield.views_convert.inc new file mode 100644 index 0000000..4d6bdd1 --- /dev/null +++ b/sites/all/modules/filefield/views/filefield.views_convert.inc @@ -0,0 +1,81 @@ + $fields) { + unset($fields['table']); + foreach ($fields as $filefield_field => $definition) { + switch ($filefield_field) { + case $content_field['field_name'] .'_fid': + $filefield_fields[$filefield_field] = array( + 'table' => $table, + 'field' => $filefield_field, + ); + $filefield_arguments['content: '. $content_field['field_name']] = &$filefield_fields[$filefield_field]; + break; + case $content_field['field_name'] .'_list': + $filefield_filters[$content_field['field_name'] .'_fid_not null'] = array( + 'table' => $table, + 'field' => $filefield_field, + ); + break; + } + } + } + } + } + } + } + switch ($type) { + case 'field': + if (isset($filefield_fields[$field['field']])) { + $multiple = array(); + switch ($field['handler']) { + case 'content_views_field_handler_ungroup': + $view->set_item_option($display, 'field', $id, 'multiple', array('group' => FALSE)); + break; + case 'content_views_field_handler_last': + $multiple['multiple_reversed'] = TRUE; + case 'content_views_field_handler_first': + $multiple['multiple_number'] = 1; + $view->set_item_option($display, 'field', $id, 'multiple', $multiple); + break; + } + $view->set_item_option($display, 'field', $id, 'format', $field['options']); + } + break; + case 'filter': + if (isset($filefield_filters[$field['field']])) { + $filter = $filefield_filters[$field['field']]; + $item = $view->get_item($display, 'filter', $id); + $item['value'] = $field['value']; + $item['table'] = $filter['table']; + $item['field'] = $filter['field']; + $view->set_item($display, 'filter', $id, $item); + } + break; + case 'argument': + if (isset($filefield_arguments[$field['type']])) { + $argument = $filefield_arguments[$field['type']]; + $options = $field['argoptions']; + $view->add_item($display, 'argument', $argument['table'], $argument['field'], $options, $field['id']); + } + break; + } +} diff --git a/sites/all/modules/filefield/views/filefield_handler_field_data.inc b/sites/all/modules/filefield/views/filefield_handler_field_data.inc new file mode 100644 index 0000000..428804d --- /dev/null +++ b/sites/all/modules/filefield/views/filefield_handler_field_data.inc @@ -0,0 +1,54 @@ + 'description'); + return $options; + } + + function options_form(&$form, &$form_state) { + parent::options_form($form, $form_state); + + $options = array(); + $info = filefield_data_info(); + foreach ($info as $key => $data) { + $options[$key] = $data['title'] . ' (' . $data['module'] .')'; + } + + $form['data_key'] = array( + '#title' => t('Data key'), + '#type' => 'radios', + '#options' => $options, + '#required' => TRUE, + '#default_value' => $this->options['data_key'], + '#description' => t('The data column may contain only a few or none any of these data options. The name of the module that provides the data is shown in parentheses.'), + '#weight' => 4, + ); + } + + function admin_summary() { + // Display the data to be displayed. + $info = filefield_data_info(); + return isset($info[$this->options['data_key']]['title']) ? $info[$this->options['data_key']]['title'] : $this->options['data_key']; + } + + function render($values) { + $values = drupal_clone($values); // Prevent affecting the original. + $data = unserialize($values->{$this->field_alias}); + $values->{$this->field_alias} = filefield_data_value($this->options['data_key'], $data[$this->options['data_key']]); + + // Copied from views_handler_field_node(). We just remove the call to + // sanitize_value() from the original call, becaue our value has already + // been cleaned by filefield_data_value(). + return $this->render_link($values->{$this->field_alias}, $values); + } + +} diff --git a/sites/all/modules/filefield/views/filefield_handler_field_icon.inc b/sites/all/modules/filefield/views/filefield_handler_field_icon.inc new file mode 100644 index 0000000..faa5faf --- /dev/null +++ b/sites/all/modules/filefield/views/filefield_handler_field_icon.inc @@ -0,0 +1,24 @@ +additional_fields['fid'] = 'fid'; + $this->additional_fields['filename'] = 'filename'; + $this->additional_fields['filesize'] = 'filesize'; + } + + function render($values) { + $pseudo_file = array( + 'fid' => $values->{$this->aliases['fid']}, + 'filemime' => $values->{$this->field_alias}, + 'filename' => $values->{$this->aliases['filename']}, + 'filesize' => $values->{$this->aliases['filesize']}, + ); + return theme('filefield_icon', $pseudo_file); + } +}