diff options
Diffstat (limited to 'sites/all/modules/media')
101 files changed, 14171 insertions, 0 deletions
diff --git a/sites/all/modules/media/LICENSE.txt b/sites/all/modules/media/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/sites/all/modules/media/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. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/sites/all/modules/media/README.txt b/sites/all/modules/media/README.txt new file mode 100644 index 000000000..6875a0722 --- /dev/null +++ b/sites/all/modules/media/README.txt @@ -0,0 +1,7 @@ + +/** + * @file + * README for the Media Module. + */ + +See http://drupal.org/node/356802 diff --git a/sites/all/modules/media/css/media.css b/sites/all/modules/media/css/media.css new file mode 100644 index 000000000..a93715622 --- /dev/null +++ b/sites/all/modules/media/css/media.css @@ -0,0 +1,147 @@ +/** + * @file + * Styles for the media library. + * + * The display and layout of the Media browser assumes Drupal's Seven theme as + * the theme active when this is displayed. + */ + +/* jQuery UI Resets */ +.ui-tabs { + padding: 0; +} +.ui-front { + z-index: 10001 !important; +} + +.ui-dialog.media-wrapper .ui-dialog-content { + padding: 0; +} + +.ui-dialog.media-wrapper .ui-dialog-buttonpane { + display: none; +} + +#media-browser-tabset .ui-widget-header { + background: none; +} + +/* Remove the default border */ +.ui-widget-content { + border: none; +} + +/* *********************************************************** */ +/* Browser layout themeing */ + +/* Size the branding header appropriately */ +#media-browser-tabset #branding { + padding: 10px 10px 0px 10px; +} + +#media-browser-tabset #branding h1 { + float: left; + height: 16px; + margin-top: 0px; +} + +/* Float the tabs right to keep the UI consistent across themes */ +#media-tabs-wrapper { + float: right; +} + +#media-browser-tabset ul.tabs { + padding: 0; + border: none; +} + +/* Reset the height to match the browser */ +#media-browser-tabset ul.tabs.primary li a:link { + font-weight: bold; + margin-right: 0; +} + +/* *********************************************************** */ +/* Media item display */ + +.media-item { + background: #eee; + border: 1px solid #CCCCCC; + box-shadow: inset 0 0 15px rgba(0,0,0,.1), inset 0 0 0 1px rgba(0,0,0,.05); + display: inline-block; + padding: 5px; + position: relative; +} + +.media-item img { + display: block; +} + +.media-item .label-wrapper { + background: rgba(255,255,255,.8); + bottom: 0; + box-shadow: inset 0 0 0 1px rgba(0,0,0,.15); + left: 0; + max-height: 100%; + overflow: hidden; + position: absolute; + right: 0; + text-align: center; + word-wrap: break-word; +} + +.media-item .label-wrapper label { + font-size: 10px; + padding: 5px 10px; +} + +/* Media item lists */ + +#media-browser-library-list { + margin: 0; + padding: 0; +} + +.media-list-thumbnails li { + float: left; + list-style: none; + margin: 0 10px 10px 0; +} + +.media-list-thumbnails li a { + text-decoration: none; +} + +.media-list-thumbnails .media-item.selected { + background: #F4ECC7; + border-color: #058AC5; +} + +.media-list-thumbnails .media-item:hover { + border-color: #058AC5; + cursor: pointer; +} + +.media-list-thumbnails .media-item .label-wrapper label { + color: #058AC5; +} + +.media-list-thumbnails .media-item .label-wrapper label:hover { + cursor: pointer; +} + +.media-list-thumbnails .form-type-checkbox { + bottom: 117px; + left: 6px; + margin: 0; + padding: 0; + position: relative; +} + +/* File field */ + +.media-widget .preview { + display: inline-block; + margin-right: 10px; + vertical-align: middle; +} diff --git a/sites/all/modules/media/images/icons/default/application-octet-stream.png b/sites/all/modules/media/images/icons/default/application-octet-stream.png Binary files differnew file mode 100644 index 000000000..0e6de2f54 --- /dev/null +++ b/sites/all/modules/media/images/icons/default/application-octet-stream.png diff --git a/sites/all/modules/media/images/icons/default/audio-mpeg.png b/sites/all/modules/media/images/icons/default/audio-mpeg.png Binary files differnew file mode 100644 index 000000000..b8763985d --- /dev/null +++ b/sites/all/modules/media/images/icons/default/audio-mpeg.png diff --git a/sites/all/modules/media/images/icons/default/audio-x-generic.png b/sites/all/modules/media/images/icons/default/audio-x-generic.png Binary files differnew file mode 100644 index 000000000..b8763985d --- /dev/null +++ b/sites/all/modules/media/images/icons/default/audio-x-generic.png diff --git a/sites/all/modules/media/images/icons/default/file-unknown.png b/sites/all/modules/media/images/icons/default/file-unknown.png Binary files differnew file mode 100644 index 000000000..46125e7f6 --- /dev/null +++ b/sites/all/modules/media/images/icons/default/file-unknown.png diff --git a/sites/all/modules/media/images/icons/default/image-x-generic.png b/sites/all/modules/media/images/icons/default/image-x-generic.png Binary files differnew file mode 100644 index 000000000..c50e3c780 --- /dev/null +++ b/sites/all/modules/media/images/icons/default/image-x-generic.png diff --git a/sites/all/modules/media/images/icons/default/video-x-generic.png b/sites/all/modules/media/images/icons/default/video-x-generic.png Binary files differnew file mode 100644 index 000000000..58add033f --- /dev/null +++ b/sites/all/modules/media/images/icons/default/video-x-generic.png diff --git a/sites/all/modules/media/includes/MediaBrowserPlugin.inc b/sites/all/modules/media/includes/MediaBrowserPlugin.inc new file mode 100644 index 000000000..807a4e41d --- /dev/null +++ b/sites/all/modules/media/includes/MediaBrowserPlugin.inc @@ -0,0 +1,86 @@ +<?php + +/** + * @file + * Definition of MediaBrowserPlugin. + */ + +/** + * Defines a Media browser plugin base class. + * + * MediaBrowserPlugin implementations need to implement at least the + * view() method. + */ +abstract class MediaBrowserPlugin implements MediaBrowserPluginInterface { + /** + * The plugin metadata array from hook_media_browser_plugin_info(). + * + * @var array + */ + protected $info; + + /** + * The parameters for the current media browser from + * media_get_browser_params(). + * + * @var array + */ + protected $params; + + /** + * Implements MediaBrowserPluginInterface::__construct(). + */ + public function __construct($info, $params) { + $this->info = $info; + $this->params = $params; + } + + /** + * Implements MediaBrowserPluginInterface::access(). + */ + public function access($account = NULL) { + // Backwards compatible support for 'access callback' definitions. + if (isset($this->info['access callback'])) { + $access_callback = $this->info['access callback']; + $access_arguments = isset($this->info['access arguments']) ? $this->info['access arguments'] : array(); + return function_exists($access_callback) && call_user_func_array($access_callback, $access_arguments); + } + + return TRUE; + } + + /** + * Provide a render array to display the plugin in a media browser. + * + * This render array will be a jQuery tab in the media browser. + * + * Some elements are special: + * - #settings: Drupal.settings.media.browser.$key (where key is the array + * key). + * - #callback: If provided, will make the tab an "ajax" tab. + * - #title: If provided, will be used as the tab's title. Otherwise the + * 'title' value from the plugin's hook_media_browser_plugin_info() will + * be used. + * - #weight: If provided, will be used to order the tabs between each other. + * A lower weight will be displayed first while a higher weight will be + * displayed later. If not provided, and there is a 'weight' value in the + * plugin's hook_media_browser_plugin_info() then it will be used, + * otherwise a default of 0 will be used. + * - form: If the plugin is to display a native Drupal form, then the output + * of drupal_get_form should be returned into the 'form' render key. If a + * form's callback isn't normally loaded, module_load_include() should be + * used to ensure that the form can be displayed. + * + * Example usage: + * @code + * module_load_include('inc', 'mymodule', 'mymodule.pages'); + * $build['#attached']['js'][] = drupal_get_path('module', 'mymodule') . '/js/mymodule.media.browser.js'; + * $build['form'] = drupal_get_form('mymodule_media_form'); + * return $build; + * @endcode + * + * @return array + * Renderable array. + */ + abstract public function view(); +} diff --git a/sites/all/modules/media/includes/MediaBrowserPluginInterface.inc b/sites/all/modules/media/includes/MediaBrowserPluginInterface.inc new file mode 100644 index 000000000..3ff11340a --- /dev/null +++ b/sites/all/modules/media/includes/MediaBrowserPluginInterface.inc @@ -0,0 +1,44 @@ +<?php + +/** + * @file + * Definition of MediaBrowserPluginInterface. + */ + +/** + * Defines a Media browser plugin. + * + * Extends the MediaBrowserPluginInterface with methods expected by all + * Media browser classes. + */ +interface MediaBrowserPluginInterface { + /** + * Set up the plugin class. + * + * @param array $info + * An array of plugin info from hook_media_browser_plugin_info() + * implementations. + * @param array $params + * An array of parameters which came in is $_GET['params']. The expected + * parameters are still being defined. + * - 'types': array of media types to support + * - 'multiselect': boolean; TRUE enables multiselect + */ + public function __construct($info, $params); + + /** + * Check if a user can access this plugin. + * + * @param object $account + * An optional user account object from user_load(). Defaults to the current + * global user. + * + * @return bool + * TRUE if the user can access this plugin, or FALSE otherwise. + */ + public function access($account = NULL); + + // The view() method is an abstract function so it is defined in MediaBrowser + // Plugin. + // @todo public function view(); +} diff --git a/sites/all/modules/media/includes/MediaBrowserUpload.inc b/sites/all/modules/media/includes/MediaBrowserUpload.inc new file mode 100644 index 000000000..57721cb97 --- /dev/null +++ b/sites/all/modules/media/includes/MediaBrowserUpload.inc @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Definition of MediaBrowserUpload. + */ + +/** + * Media browser plugin for showing the upload form. + * + * @deprecated + */ +class MediaBrowserUpload extends MediaBrowserPlugin { + /** + * Implements MediaBrowserPluginInterface::access(). + */ + public function access($account = NULL) { + return file_entity_access('create', NULL, $account); + } + + /** + * Implements MediaBrowserPlugin::view(). + */ + public function view() { + module_load_include('inc', 'file_entity', 'file_entity.pages'); + + $build = array(); + $build['form'] = drupal_get_form('file_entity_add_upload', $this->params); + + return $build; + } +} diff --git a/sites/all/modules/media/includes/MediaBrowserView.inc b/sites/all/modules/media/includes/MediaBrowserView.inc new file mode 100644 index 000000000..0321a119c --- /dev/null +++ b/sites/all/modules/media/includes/MediaBrowserView.inc @@ -0,0 +1,56 @@ +<?php + +/** + * @file + * Definition of MediaBrowserView. + */ + +/** + * Media browser plugin for displaying a specific view and display. + */ +class MediaBrowserView extends MediaBrowserPlugin { + /** + * The view object from views_get_view() for this plugin. + * + * @var view + */ + protected $view; + + /** + * Implements MediaBrowserPluginInterface::__construct(). + */ + public function __construct($info, $params) { + parent::__construct($info, $params); + + // Set up the view object with the proper display. + if ($view = views_get_view($info['view_name'])) { + $display_id = !empty($info['view_display_id']) ? $info['view_display_id'] : NULL; + if ($view->set_display($display_id)) { + $this->view = $view; + } + } + } + + /** + * Implements MediaBrowserPluginInterface::access(). + */ + public function access($account = NULL) { + return !empty($this->view) && $this->view->access($this->view->current_display, $account); + } + + /** + * Implements MediaBrowserPlugin::view(). + */ + public function view() { + if (!empty($this->view)) { + $build['#markup'] = $this->view->preview(); + + // Allow the View title to override the plugin title. + if ($title = $this->view->get_title()) { + $build['#title'] = $title; + } + + return $build; + } + } +} diff --git a/sites/all/modules/media/includes/MediaEntityTranslationHandler.inc b/sites/all/modules/media/includes/MediaEntityTranslationHandler.inc new file mode 100644 index 000000000..4e78ed18d --- /dev/null +++ b/sites/all/modules/media/includes/MediaEntityTranslationHandler.inc @@ -0,0 +1,63 @@ +<?php + +/** + * @file + * Media translation handler for the entity translation module. + */ + + +/** + * Media translation handler. + */ +class MediaEntityTranslationHandler extends EntityTranslationDefaultHandler { + + /** + * Constructor function. + */ + public function __construct($entity_type, $entity_info, $entity) { + parent::__construct('file', $entity_info, $entity); + } + + /** + * Entity form handler. + * + * @see EntityTranslationDefaultHandler::entityForm() + */ + public function entityForm(&$form, &$form_state) { + parent::entityForm($form, $form_state); + + if (isset($form['actions']['delete_translation'])) { + $form['actions']['delete_translation']['#weight'] = 10; + } + + if ($this->getPathScheme() == 'media') { + $language = $GLOBALS[LANGUAGE_TYPE_CONTENT]; + $form_langcode = $this->getFormLanguage(); + $source_langcode = $this->getSourceLanguage(); + $translations = $this->getTranslations(); + + // If a translation in the current content language is missing we display + // a link to create it, unless we are not already doing it. + if ($language->language != $form_langcode && empty($source_langcode) && !isset($translations->data[$language->language])) { + $link = array( + 'title' => t('Add @language translation', array('@language' => $language->name)), + 'href' => $this->getEditPath() . '/add/' . $form_langcode . '/' . $language->language, + 'localized_options' => array('attributes' => array('class' => array('ctools-use-modal'))), + ); + $form['media_add_translation'] = array( + '#theme' => 'menu_local_action', + '#link' => $link, + '#weight' => -110, + '#prefix' => '<ul class="action-links">', + '#suffix' => '</ul>', + ); + } + + // Hide unsupported elements. + $form['source_language']['#access'] = FALSE; + if (isset($form['actions']['delete_translation'])) { + $form['actions']['delete_translation']['#access'] = FALSE; + } + } + } +} diff --git a/sites/all/modules/media/includes/MediaReadOnlyStreamWrapper.inc b/sites/all/modules/media/includes/MediaReadOnlyStreamWrapper.inc new file mode 100644 index 000000000..ee051fdbd --- /dev/null +++ b/sites/all/modules/media/includes/MediaReadOnlyStreamWrapper.inc @@ -0,0 +1,476 @@ +<?php + +/** + * @file + * Implements a base class for Resource Stream Wrappers. + */ + +/** + * A base class for Resource Stream Wrappers. + * + * This class provides a complete stream wrapper implementation. It passes + * incoming URL's through an interpolation method then recursively calls + * the invoking PHP filesystem function. + * + * MediaReadOnlyStreamWrapper implementations need to override at least the + * interpolateUrl method to rewrite the URL before is it passed back into the + * calling function. + */ +abstract class MediaReadOnlyStreamWrapper implements DrupalStreamWrapperInterface { + protected $parameters = array(); + protected $base_url = NULL; + private $_DEBUG_MODE = NULL; + + /** + * Utility function to return paramenters. + */ + public function get_parameters() { + return $this->parameters; + } + + // As part of the inode protection mode returned by stat(), identifies the + // file as a regular file, as opposed to a directory, symbolic link, or other + // type of "file". + // @see http://linux.die.net/man/2/stat + const S_IFREG = 0100000; + + /** + * Template for stat calls. + * + * All elements must be initialized. + */ + protected $_stat = array( + 0 => 0, // Device number + 'dev' => 0, + 1 => 0, // Inode number + 'ino' => 0, + // Inode protection mode. file_unmanaged_delete() requires is_file() to + // return TRUE. + 2 => self::S_IFREG, + 'mode' => self::S_IFREG, + 3 => 0, // Number of links. + 'nlink' => 0, + 4 => 0, // Userid of owner. + 'uid' => 0, + 5 => 0, // Groupid of owner. + 'gid' => 0, + 6 => -1, // Device type, if inode device * + 'rdev' => -1, + 7 => 0, // Size in bytes. + 'size' => 0, + 8 => 0, // Time of last access (Unix timestamp). + 'atime' => 0, + 9 => 0, // Time of last modification (Unix timestamp). + 'mtime' => 0, + 10 => 0, // Time of last inode change (Unix timestamp). + 'ctime' => 0, + 11 => -1, // Blocksize of filesystem IO. + 'blksize' => -1, + 12 => -1, // Number of blocks allocated. + 'blocks' => -1, + ); + + /** + * Handles parameters on the URL string. + */ + public function interpolateUrl() { + if ($parameters = $this->get_parameters()) { + return $this->base_url . '?' . http_build_query($parameters); + } + } + + /** + * Returns a web accessible URL for the resource. + * + * This function should return a URL that can be embedded in a web page + * and accessed from a browser. For example, the external URL of + * "youtube://xIpLd0WQKCY" might be + * "http://www.youtube.com/watch?v=xIpLd0WQKCY". + * + * @return string + * Returns a string containing a web accessible URL for the resource. + */ + public function getExternalUrl() { + return $this->interpolateUrl(); + } + + /** + * Base implementation of getMimeType(). + */ + public static function getMimeType($uri, $mapping = NULL) { + return 'application/octet-stream'; + } + + /** + * Base implementation of realpath(). + */ + public function realpath() { + return $this->getExternalUrl(); + } + + /** + * Stream context resource. + * + * @var Resource + */ + public $context; + + /** + * A generic resource handle. + * + * @var Resource + */ + public $handle = NULL; + + /** + * Instance URI (stream). + * + * A stream is referenced as "scheme://target". + * + * @var String + */ + protected $uri; + + /** + * Base implementation of setUri(). + */ + public function setUri($uri) { + $this->uri = $uri; + $this->parameters = $this->_parse_url($uri); + } + + /** + * Base implementation of getUri(). + */ + public function getUri() { + return $this->uri; + } + + /** + * Report an error. + * + * @param string $message + * The untranslated string to report. + * @param array $options + * An optional array of options to send to t(). + * @param bool $display + * If TRUE, then we display the error to the user. + * + * @return bool + * We return FALSE, since we sometimes pass that back from the reporting + * function. + */ + private function _report_error($message, $options = array(), $display = FALSE) { + watchdog('resource', $message, $options, WATCHDOG_ERROR); + if ($display) { + drupal_set_message(t($message, $options), 'error'); + } + return FALSE; + } + + /** + * Sets the debug mode. + */ + private function _debug($message, $type = 'status') { + if ($this->_DEBUG_MODE) { + drupal_set_message($message, $type); + } + } + + /** + * Returns an array of any parameters stored in the URL's path. + * + * @param string $url + * The URL to parse, such as youtube://v/[video-code]/t/[tags+more-tags]. + * + * @return array + * An associative array of all the parameters in the path, + * or FALSE if the $url is ill-formed. + */ + protected function _parse_url($url) { + $path = explode('://', $url); + $parts = explode('/', $path[1]); + $params = array(); + $count = 0; + $total = count($parts); + if (!$total || ($total % 2)) { + // If we have no parts, or an odd number of parts, it's malformed. + return FALSE; + } + while ($count < $total) { + // We iterate count for each step of the assignment to keep us honest. + $params[$parts[$count++]] = $parts[$count++]; + } + return $params; + } + + /** + * Support for fopen(), file_get_contents(), file_put_contents() etc. + * + * @param string $url + * A string containing the path to the file to open. + * @param string $mode + * The file mode ("r", "wb" etc.). + * @param bitmask $options + * A bit mask of STREAM_USE_PATH and STREAM_REPORT_ERRORS. + * @param string &$opened_url + * A string containing the path actually opened. + * + * @return bool + * TRUE if file was opened successfully. + */ + public function stream_open($url, $mode, $options, &$opened_url) { + $this->_debug(t('Stream open: %url', array('%url' => $url))); + + // We only handle Read-Only mode by default. + if ($mode != 'r' && $mode != 'rb') { + return $this->_report_error('Attempted to open %url as mode: %mode.', array('%url' => $url, '%mode' => $mode), ($options & STREAM_REPORT_ERRORS)); + } + + // We parse a URL as youtube://v/dsyiufo34/t/cats+dogs to store + // the relevant code(s) in our private array of parameters. + $this->parameters = $this->_parse_url($url); + + if ($this->parameters === FALSE) { + return $this->_report_error('Attempted to parse an ill-formed url: %url.', array('%url' => $url), ($options & STREAM_REPORT_ERRORS)); + } + + if ((bool) $this->parameters && ($options & STREAM_USE_PATH)) { + $opened_url = $url; + } + + $this->_debug(t('Stream opened: %parameters', array('%parameters' => print_r($this->parameters, TRUE)))); + + return (bool) $this->parameters; + } + + /** + * Undocumented PHP stream wrapper method. + */ + function stream_lock($operation) { + return FALSE; + } + + /** + * Support for fread(), file_get_contents() etc. + * + * @param int $count + * Maximum number of bytes to be read. + * + * @return bool + * The string that was read, or FALSE in case of an error. + */ + public function stream_read($count) { + return FALSE; + } + + /** + * Support for fwrite(), file_put_contents() etc. + * + * Since this is a read only stream wrapper this always returns false. + * + * @param string $data + * The string to be written. + * + * @return bool + * Returns FALSE. + */ + public function stream_write($data) { + return FALSE; + } + + /** + * Support for feof(). + * + * @return bool + * TRUE if end-of-file has been reached. + */ + public function stream_eof() { + return FALSE; + } + + /** + * Support for fseek(). + * + * @todo document why this returns false. + * + * @param int $offset + * The byte offset to got to. + * @param string $whence + * SEEK_SET, SEEK_CUR, or SEEK_END. + * + * @return bool + * TRUE on success + */ + public function stream_seek($offset, $whence) { + return FALSE; + } + + /** + * Support for fflush(). + * + * @todo document why this returns false. + * + * @return bool + * TRUE if data was successfully stored (or there was no data to store). + */ + public function stream_flush() { + return FALSE; + } + + /** + * Support for ftell(). + * + * @todo document why this returns false. + * + * @return bool + * The current offset in bytes from the beginning of file. + */ + public function stream_tell() { + return FALSE; + } + + /** + * Support for fstat(). + * + * @return array + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + */ + public function stream_stat() { + return $this->_stat; + } + + /** + * Support for fclose(). + * + * @todo document why this returns TRUE. + * + * @return bool + * TRUE if stream was successfully closed. + */ + public function stream_close() { + return TRUE; + } + + /** + * Support for stat(). + * + * @param string $url + * A string containing the url to get information about. + * @param bitmask $flags + * A bit mask of STREAM_URL_STAT_LINK and STREAM_URL_STAT_QUIET. + * + * @return array + * An array with file status, or FALSE in case of an error - see fstat() + * for a description of this array. + */ + public function url_stat($url, $flags) { + return $this->stream_stat(); + } + + /** + * Support for opendir(). + * + * @param string $url + * A string containing the url to the directory to open. + * @param int $options + * Whether or not to enforce safe_mode (0x04). + * + * @return bool + * TRUE on success. + */ + public function dir_opendir($url, $options) { + return FALSE; + } + + /** + * Support for readdir(). + * + * @return bool + * The next filename, or FALSE if there are no more files in the directory. + */ + public function dir_readdir() { + return FALSE; + } + + /** + * Support for rewinddir(). + * + * @return bool + * TRUE on success. + */ + public function dir_rewinddir() { + return FALSE; + } + + /** + * Support for closedir(). + * + * @return bool + * TRUE on success. + */ + public function dir_closedir() { + return FALSE; + } + + /** + * Undocumented. + * + * @todo document. + */ + public function getDirectoryPath() { + return ''; + } + + /** + * DrupalStreamWrapperInterface requires that these methods be implemented, + * but none of them apply to a read-only stream wrapper. On failure they + * are expected to return FALSE. + */ + + /** + * Implements DrupalStreamWrapperInterface::unlink(). + */ + public function unlink($uri) { + // Although the remote file itself can't be deleted, return TRUE so that + // file_delete() can remove the file record from the Drupal database. + return TRUE; + } + + /** + * Implements DrupalStreamWrapperInterface::rename(). + */ + public function rename($from_uri, $to_uri) { + return FALSE; + } + + /** + * Implements DrupalStreamWrapperInterface::mkdir(). + */ + public function mkdir($uri, $mode, $options) { + return FALSE; + } + + /** + * Implements DrupalStreamWrapperInterface::rmdir(). + */ + public function rmdir($uri, $options) { + return FALSE; + } + + /** + * Implements DrupalStreamWrapperInterface::chmod(). + */ + public function chmod($mode) { + return FALSE; + } + + /** + * Implements DrupalStreamWrapperInterface::dirname(). + */ + public function dirname($uri = NULL) { + return FALSE; + } + +} diff --git a/sites/all/modules/media/includes/media.admin.inc b/sites/all/modules/media/includes/media.admin.inc new file mode 100644 index 000000000..9cc9fa145 --- /dev/null +++ b/sites/all/modules/media/includes/media.admin.inc @@ -0,0 +1,136 @@ +<?php + +/** + * @file + * Administration page callbacks for the Media module. + */ + +/** + * Displays the media administration page. + */ +function media_admin_config_browser($form, &$form_state) { + $theme_options = array(); + $theme_options[NULL] = t('Default administration theme'); + + foreach (list_themes() as $key => $theme) { + if ($theme->status) { + $theme_options[$key] = $theme->info['name']; + } + } + + $form['media_dialog_theme'] = array( + '#type' => 'select', + '#title' => t('Media browser theme'), + '#options' => $theme_options, + '#description' => t("This theme will be used for all media related dialogs. It can be different from your site's theme because many site themes do not work well in the small windows which media uses."), + '#default_value' => variable_get('media_dialog_theme', ''), + ); + + $form['array_filter'] = array( + '#type' => 'value', + '#value' => TRUE, + ); + + $form['mediapopup'] = array( + '#type' => 'fieldset', + '#title' => t('Media Popup'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + $form['mediapopup']['media_dialogclass'] = array( + '#type' => 'textfield', + '#title' => t('Dialog Class'), + '#default_value' => variable_get('media_dialogclass', 'media-wrapper'), + '#description' => t('The class used to identify the popup wrapper element.'), + ); + $form['mediapopup']['media_modal'] = array( + '#type' => 'select', + '#title' => t('Modal'), + '#options' => array( + FALSE => t('False'), + TRUE => t('True'), + ), + '#default_value' => variable_get('media_modal', TRUE), + '#description' => t('Open as modal window.'), + ); + $form['mediapopup']['media_draggable'] = array( + '#type' => 'select', + '#title' => t('Draggable'), + '#options' => array( + FALSE => t('False'), + TRUE => t('True'), + ), + '#default_value' => variable_get('media_draggable', FALSE), + '#description' => t('Draggable modal window.'), + ); + $form['mediapopup']['media_resizable'] = array( + '#type' => 'select', + '#title' => t('Resizable'), + '#options' => array( + FALSE => t('False'), + TRUE => t('True'), + ), + '#default_value' => variable_get('media_resizable', FALSE), + '#description' => t('Resizable modal window.'), + ); + $form['mediapopup']['media_minwidth'] = array( + '#type' => 'textfield', + '#title' => t('Min Width'), + '#default_value' => variable_get('media_minwidth', 500), + '#description' => t('CSS property min-width.'), + ); + $form['mediapopup']['media_width'] = array( + '#type' => 'textfield', + '#title' => t('Width'), + '#default_value' => variable_get('media_width', 670), + '#description' => t('CSS property width.'), + ); + $form['mediapopup']['media_height'] = array( + '#type' => 'textfield', + '#title' => t('Height'), + '#default_value' => variable_get('media_height', 280), + '#description' => t('CSS property height.'), + ); + $form['mediapopup']['media_position'] = array( + '#type' => 'textfield', + '#title' => t('Position'), + '#default_value' => variable_get('media_position', 'center'), + '#description' => t('CSS property position.'), + ); + $form['mediapopup']['media_zindex'] = array( + '#type' => 'textfield', + '#title' => t('Z-Index'), + '#default_value' => variable_get('media_zindex', 10000), + '#description' => t('CSS property z-index.'), + ); + $form['mediapopup']['overlay'] = array( + '#type' => 'fieldset', + '#title' => t('Overlay'), + ); + $form['mediapopup']['overlay']['media_backgroundcolor'] = array( + '#type' => 'textfield', + '#title' => t('Background Color'), + '#default_value' => variable_get('media_backgroundcolor', '#000000'), + '#description' => t('CSS property background-color; used with overlay.'), + ); + $form['mediapopup']['overlay']['media_opacity'] = array( + '#type' => 'textfield', + '#title' => t('Opacity'), + '#default_value' => variable_get('media_opacity', 0.4), + '#description' => t('CSS property opacity; used with overlay.'), + ); + + $form['#submit'][] = 'media_admin_config_browser_pre_submit'; + + return system_settings_form($form); +} + +/** + * Form submission handler for media_admin_config_browser(). + */ +function media_admin_config_browser_pre_submit(&$form, &$form_state) { + if (!$form_state['values']['media_dialog_theme']) { + variable_del('media_dialog_theme'); + unset($form_state['values']['media_dialog_theme']); + } +} diff --git a/sites/all/modules/media/includes/media.browser.inc b/sites/all/modules/media/includes/media.browser.inc new file mode 100644 index 000000000..d5285f0a4 --- /dev/null +++ b/sites/all/modules/media/includes/media.browser.inc @@ -0,0 +1,244 @@ +<?php + +/** + * @file + * Summon plugins and render the media browser. + */ + +/** + * Media browser page callback. + */ +function media_browser($selected = NULL) { + $output = array(); + $output['#attached']['library'][] = array('media', 'media_browser_page'); + + $params = media_get_browser_params(); + + // If one or more files have been selected, the browser interaction is now + // complete. Return empty page content to the dialog which now needs to close, + // but populate Drupal.settings with information about the selected files. + if (isset($params['fid'])) { + $fids = is_array($params['fid']) ? $params['fid'] : array($params['fid']); + if (!is_numeric($fids[0])) { + throw new Exception('Error selecting media, fid param is not an fid or an array of fids'); + } + $files = file_load_multiple($fids); + foreach ($files as $file) { + $view_mode = isset($params['view_mode']) ? $params['view_mode'] : 'preview'; + media_browser_build_media_item($file, $view_mode); + } + $setting = array('media' => array('selectedMedia' => array_values($files))); + drupal_add_js($setting, 'setting'); + return $output; + } + + $plugins = media_get_browser_plugin_info(); + + // Allow parameters to provide a list of enabled or disabled media browser + // plugins. + if (!empty($params['enabledPlugins'])) { + $plugins = array_intersect_key($plugins, array_fill_keys($params['enabledPlugins'], 1)); + } + elseif (!empty($params['disabledPlugins'])) { + $plugins = array_diff_key($plugins, array_fill_keys($params['disabledPlugins'], 1)); + } + + // Render plugins. + $plugin_output = array(); + foreach ($plugins as $key => $plugin_info) { + // Support the old CTools style handler definition. + if (!isset($plugin_info['class']) && !empty($plugin_info['handler'])) { + if (is_string($plugin_info['handler'])) { + $plugin_info['class'] = $plugin_info['handler']; + } + elseif (isset($plugin_info['handler']['class'])) { + $plugin_info['class'] = $plugin_info['handler']['class']; + } + } + + if (empty($plugin_info['class']) || !class_exists($plugin_info['class'])) { + continue; + } + + $plugin = new $plugin_info['class']($plugin_info, $params); + if ($plugin->access()) { + $plugin_output[$key] = $plugin->view(); + if (!empty($plugin_output[$key]) && is_array($plugin_output[$key])) { + $plugin_output[$key] += array( + '#title' => $plugin_info['title'], + '#weight' => isset($plugin_info['weight']) ? $plugin_info['weight'] : 0, + ); + } + else { + unset($plugin_output[$key]); + continue; + } + } + else { + continue; + } + + // We need to ensure that a submit button is available on each tab. If the + // plugin is not returning a form element we need to add a submit button. + // This is a fairly broad assumption. + if (empty($plugin_output[$key]['#form']) && !empty($plugin_output[$key]['#markup'])) { + $fake_buttons = '<div class="form-actions form-wrapper">'; + $fake_buttons .= l(t('Submit'), '', array( + 'attributes' => array( + 'class' => array('button', 'button-yes', 'fake-submit', $key), + ), + )); + $fake_buttons .= '</div>'; + $plugin_output[$key]['#markup'] .= $fake_buttons; + } + } + + // Allow modules to change the tab names or whatever else they want to change + // before we render. Perhaps this should be an alter on the theming function + // that we should write to be making the tabs. + drupal_alter('media_browser_plugins', $plugin_output); + + $tabs = array(); + $settings = array('media' => array('browser' => array())); + + foreach (element_children($plugin_output, TRUE) as $key) { + // Add any JavaScript settings from the browser tab. + if (!empty($plugin_output[$key]['#settings'])) { + $settings['media']['browser'][$key] = $plugin_output[$key]['#settings']; + } + + // If this is a "ajax" style tab, add the href, otherwise an id. jQuery UI + // will use an href value to load content from that url + $tabid = 'media-tab-' . check_plain($key); + if (!empty($plugin_output[$key]['#callback'])) { + $href = $plugin_output[$key]['#callback']; + } + else { + $attributes = array( + 'class' => array('media-browser-tab'), + 'id' => $tabid, + 'data-tabid' => $key, + ); + // Create a div for each tab's content. + $plugin_output[$key] += array( + '#prefix' => '<div '. drupal_attributes($attributes) . ">\n", + '#suffix' => "</div>\n", + ); + } + + $attributes = array( + 'href' => '#' . $tabid, + 'data-tabid' => $key, + 'title' => $plugin_output[$key]['#title'], + ); + $tabs[]['element'] = array( + '#markup' => '<li><a' . drupal_attributes($attributes) . '>' . check_plain($plugin_output[$key]['#title']) . "</a></li>\n", + ); + } + + drupal_add_js($settings, 'setting'); + + $output['tabset']['tabs'] = array( + '#theme' => 'menu_local_tasks', + '#attributes' => array('class' => array('tabs', 'primary')), + '#primary' => $tabs, + ); + + $output['tabset']['panes'] = $plugin_output; + + return $output; +} + +/** + * Menu callback for testing the media browser. + */ +function media_browser_testbed($form) { + $form['#attached']['library'][] = array('media', 'media_browser'); + $form['#attached']['library'][] = array('media', 'media_browser_settings'); + + $form['test_element'] = array( + '#type' => 'media', + '#media_options' => array( + 'global' => array( + 'types' => array('video', 'audio'), + ), + ), + ); + + $launcher = '<a href="#" id="launcher"> Launch Media Browser</a>'; + + $form['options'] = array( + '#type' => 'textarea', + '#title' => 'Options (JSON)', + '#rows' => 10, + ); + + $form['launcher'] = array( + '#markup' => $launcher, + ); + + $form['result'] = array( + '#type' => 'textarea', + '#title' => 'Result', + ); + + $js = <<<EOF + Drupal.behaviors.mediaTest = { + attach: function(context, settings) { + var delim = "---"; + var recentOptions = []; + var recentOptionsCookie = jQuery.cookie("recentOptions"); + if (recentOptionsCookie) { + recentOptions = recentOptionsCookie.split("---"); + } + + var recentSelectBox = jQuery('<select id="recent_options" style="width:100%"></select>').change(function() { jQuery('#edit-options').val(jQuery(this).val())}); + + jQuery('.form-item-options').append('<label for="recent_options">Recent</a>'); + jQuery('.form-item-options').append(recentSelectBox); + jQuery('.form-item-options').append(jQuery('<a href="#">Reset</a>').click(function() {alert('reset'); jQuery.cookie("recentOptions", null); window.location.reload(); })); + + jQuery.each(recentOptions, function (idx, val) { + recentSelectBox.append(jQuery('<option></option>').val(val).html(val)); + }); + + + jQuery('#launcher').click(function () { + jQuery('#edit-result').val(''); + var options = {}; + var optionsTxt = jQuery('#edit-options').val(); + if (optionsTxt) { + // Store it in the recent box + recentOptionsCookie += "---" + optionsTxt + jQuery.cookie("recentOptions", recentOptionsCookie, { expires: 7 }); + recentSelectBox.append(jQuery('<option></option>').val(optionsTxt).html(optionsTxt)); + options = eval('(' + optionsTxt + ')'); + } + Drupal.media.popups.mediaBrowser(Drupal.behaviors.mediaTest.mediaSelected, options); + return false; + }); + }, + + mediaSelected: function(selectedMedia) { + var result = JSON.stringify(selectedMedia); + jQuery('#edit-result').val(result); + } + } + +EOF; + + drupal_add_js($js, array('type' => 'inline')); + return $form; +} + +/** + * Adds additional properties to a file which are needed by the browser JS code. + * + * @param object $file + * A Drupal file object. + */ +function media_browser_build_media_item($file, $view_mode = 'preview') { + $preview = media_get_thumbnail_preview($file, NULL, $view_mode); + $file->preview = drupal_render($preview); + $file->url = file_create_url($file->uri); +} diff --git a/sites/all/modules/media/includes/media.fields.inc b/sites/all/modules/media/includes/media.fields.inc new file mode 100644 index 000000000..0820fc164 --- /dev/null +++ b/sites/all/modules/media/includes/media.fields.inc @@ -0,0 +1,622 @@ +<?php + +/** + * @file + * Provide the media selector widget and media field formatters to the Fields + * API. + */ + +/** + * Implements hook_field_widget_info(). + */ +function media_field_widget_info() { + return array( + 'media_generic' => array( + 'label' => t('Media browser'), + 'field types' => array('file', 'image'), + 'settings' => array( + 'allowed_types' => array( + 'image' => 'image', + ), + 'browser_plugins' => array(), + 'allowed_schemes' => array( + 'public' => 'public', + ), + ), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_CUSTOM, + 'default value' => FIELD_BEHAVIOR_NONE, + ), + ), + ); +} + +/** + * Implements hook_field_widget_settings_form(). + */ +function media_field_widget_settings_form($field, $instance) { + $widget = $instance['widget']; + $settings = $widget['settings']; + + $plugins = media_get_browser_plugin_info(); + $options = array(); + foreach ($plugins as $key => $plugin) { + $options[$key] = check_plain($plugin['title']); + } + + $form['browser_plugins'] = array( + '#type' => 'checkboxes', + '#title' => t('Enabled browser plugins'), + '#options' => $options, + '#default_value' => $settings['browser_plugins'], + '#description' => t('Media browser plugins which are allowed for this field. If no plugins are selected, they will all be available.'), + ); + + $form['allowed_types'] = array( + '#type' => 'checkboxes', + '#title' => t('Allowed file types'), + '#options' => file_entity_type_get_names(), + '#default_value' => $settings['allowed_types'], + '#description' => t('File types which are allowed for this field. If no file types are selected, they will all be available.'), + ); + + $visible_steam_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE); + $options = array(); + foreach ($visible_steam_wrappers as $scheme => $information) { + $options[$scheme] = check_plain($information['name']); + } + + $form['allowed_schemes'] = array( + '#type' => 'checkboxes', + '#title' => t('Allowed URI schemes'), + '#options' => $options, + '#default_value' => $settings['allowed_schemes'], + '#description' => t('URI schemes which are allowed for this field. If no schemes are selected, they will all be available.'), + ); + + return $form; +} + +/** + * Implements hook_field_widget_form(). + */ +function media_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + $defaults = array( + 'fid' => 0, + 'display' => !empty($field['settings']['display_default']), + 'description' => '', + ); + + // Load the items for form rebuilds from the field state as they might not be + // in $form_state['values'] because of validation limitations. Also, they are + // only passed in as $items when editing existing entities. + $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state); + if (isset($field_state['items'])) { + $items = $field_state['items']; + } + + $field_settings = $instance['settings']; + $widget_settings = $instance['widget']['settings']; + + // Essentially we use the media type, extended with some enhancements. + $element_info = element_info('media'); + $element += array( + '#type' => 'media', + '#value_callback' => 'media_field_widget_value', + '#process' => array_merge($element_info['#process'], array('media_field_widget_process')), + '#media_options' => array( + 'global' => array( + 'types' => array_filter($widget_settings['allowed_types']), + 'enabledPlugins' => array_filter($instance['widget']['settings']['browser_plugins']), + 'schemes' => array_filter($widget_settings['allowed_schemes']), + 'file_directory' => isset($field_settings['file_directory']) ? $field_settings['file_directory'] : '', + 'file_extensions' => isset($field_settings['file_extensions']) ? $field_settings['file_extensions'] : variable_get('file_entity_default_allowed_extensions', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp mp3 mov mp4 m4a m4v mpeg avi ogg oga ogv weba webp webm'), + 'max_filesize' => isset($field_settings['max_filesize']) ? $field_settings['max_filesize'] : 0, + 'uri_scheme' => !empty($field['settings']['uri_scheme']) ? $field['settings']['uri_scheme'] : file_default_scheme(), + ), + ), + // Allows this field to return an array instead of a single value. + '#extended' => TRUE, + ); + + // Add image field specific validators. + if ($field['type'] == 'image') { + if ($field_settings['min_resolution'] || $field_settings['max_resolution']) { + $element['#media_options']['global']['min_resolution'] = $field_settings['min_resolution']; + $element['#media_options']['global']['max_resolution'] = $field_settings['max_resolution']; + } + } + + if ($field['cardinality'] == 1) { + // Set the default value. + $element['#default_value'] = !empty($items) ? $items[0] : $defaults; + // If there's only one field, return it as delta 0. + if (empty($element['#default_value']['fid'])) { + $element['#description'] = theme('media_upload_help', array('description' => $element['#description'])); + } + $elements = array($element); + } + else { + // If there are multiple values, add an element for each existing one. + foreach ($items as $item) { + $elements[$delta] = $element; + $elements[$delta]['#default_value'] = $item; + $elements[$delta]['#weight'] = $delta; + $delta++; + } + // And then add one more empty row for new uploads except when this is a + // programmed form as it is not necessary. + if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) { + $elements[$delta] = $element; + $elements[$delta]['#default_value'] = $defaults; + $elements[$delta]['#weight'] = $delta; + $elements[$delta]['#required'] = ($element['#required'] && $delta == 0); + } + // The group of elements all-together need some extra functionality + // after building up the full list (like draggable table rows). + $elements['#file_upload_delta'] = $delta; + $elements['#theme'] = 'media_widget_multiple'; + $elements['#theme_wrappers'] = array('fieldset'); + $elements['#process'] = array('media_field_widget_process_multiple'); + $elements['#title'] = $element['#title']; + $elements['#description'] = $element['#description']; + $elements['#field_name'] = $element['#field_name']; + $elements['#language'] = $element['#language']; + $elements['#display_field'] = intval(!empty($field['settings']['display_field'])); + + // Add some properties that will eventually be added to the media upload + // field. These are added here so that they may be referenced easily through + // a hook_form_alter(). + $elements['#file_upload_title'] = t('Attach media'); + $elements['#file_upload_description'] = theme('media_upload_help', array('description' => '')); + } + + return $elements; +} + +/** + * The #value_callback for the media field element. + */ +function media_field_widget_value($element, $input = FALSE, $form_state) { + if ($input) { + // Checkboxes lose their value when empty. + // If the display field is present make sure its unchecked value is saved. + $field = field_widget_field($element, $form_state); + if (empty($input['display'])) { + $input['display'] = intval(!empty($field['settings']['display_field'])); + } + } + + // We depend on the media element to handle uploads. + $return = media_file_value($element, $input, $form_state); + + // Ensure that all the required properties are returned even if empty. + $return += array( + 'fid' => 0, + 'display' => 1, + 'description' => '', + ); + + return $return; +} + +/** + * An element #process callback for the media field type. + * + * Expands the media type to include the description and display fields. + */ +function media_field_widget_process($element, &$form_state, $form) { + $item = $element['#value']; + $item['fid'] = $element['fid']['#value']; + + $field = field_widget_field($element, $form_state); + $instance = field_widget_instance($element, $form_state); + $settings = $instance['widget']['settings']; + + $element['#theme'] = 'media_widget'; + + // Add the display field if enabled. + if (!empty($field['settings']['display_field']) && $item['fid']) { + $element['display'] = array( + '#type' => empty($item['fid']) ? 'hidden' : 'checkbox', + '#title' => t('Include file in display'), + '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'], + '#attributes' => array('class' => array('file-display')), + ); + } + else { + $element['display'] = array( + '#type' => 'hidden', + '#value' => '1', + ); + } + + // Add the description field if enabled. + if (!empty($instance['settings']['description_field']) && $item['fid']) { + $element['description'] = array( + '#type' => variable_get('file_description_type', 'textfield'), + '#title' => t('Description'), + '#value' => isset($item['description']) ? $item['description'] : '', + '#maxlength' => variable_get('file_description_length', 128), + '#description' => t('The description may be used as the label of the link to the file.'), + ); + } + + // Adjust the Ajax settings so that on upload and remove of any individual + // file, the entire group of file fields is updated together. + if ($field['cardinality'] != 1) { + $parents = array_slice($element['#array_parents'], 0, -1); + $new_path = 'media/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value']; + $field_element = drupal_array_get_nested_value($form, $parents); + $new_wrapper = $field_element['#id'] . '-ajax-wrapper'; + foreach (element_children($element) as $key) { + if (isset($element[$key]['#ajax'])) { + $element[$key]['#ajax']['path'] = $new_path; + $element[$key]['#ajax']['wrapper'] = $new_wrapper; + } + } + unset($element['#prefix'], $element['#suffix']); + } + + // Add another submit handler to the upload and remove buttons, to implement + // functionality needed by the field widget. This submit handler, along with + // the rebuild logic in media_field_widget_form() requires the entire field, + // not just the individual item, to be valid. + foreach (array('attach_button', 'remove_button') as $key) { + $element[$key]['#submit'][] = 'media_field_widget_submit'; + $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1)); + } + + return $element; +} + +/** + * An element #process callback for a group of media fields. + * + * Adds the weight field to each row so it can be ordered and adds a new Ajax + * wrapper around the entire group so it can be replaced all at once. + */ +function media_field_widget_process_multiple($element, &$form_state, $form) { + $element_children = element_children($element, TRUE); + $count = count($element_children); + + foreach ($element_children as $delta => $key) { + if ($key != $element['#file_upload_delta']) { + $description = _media_field_get_description_from_element($element[$key]); + $element[$key]['_weight'] = array( + '#type' => 'weight', + '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'), + '#title_display' => 'invisible', + '#delta' => $count, + '#default_value' => $delta, + ); + } + else { + // The title needs to be assigned to the attach field so that validation + // errors include the correct widget label. + $element[$key]['#title'] = $element['#title']; + $element[$key]['_weight'] = array( + '#type' => 'hidden', + '#default_value' => $delta, + ); + } + } + + // Add a new wrapper around all the elements for Ajax replacement. + $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">'; + $element['#suffix'] = '</div>'; + + return $element; +} + +/** + * Retrieves the file description from a media field element. + * + * This helper function is used by media_field_widget_process_multiple(). + * + * @param $element + * The element being processed. + * + * @return + * A description of the file suitable for use in the administrative interface. + */ +function _media_field_get_description_from_element($element) { + // Use the actual file description, if it's available. + if (!empty($element['#default_value']['description'])) { + return $element['#default_value']['description']; + } + // Otherwise, fall back to the filename. + if (!empty($element['#default_value']['filename'])) { + return $element['#default_value']['filename']; + } + // This is probably a newly uploaded file; no description is available. + return FALSE; +} + +/** + * Form submission handler for attach/remove button of media_field_widget_form(). + * + * This runs in addition to and after media_field_widget_submit(). + * + * @see media_field_widget_submit() + * @see media_field_widget_form() + * @see media_field_widget_process() + */ +function media_field_widget_submit($form, &$form_state) { + // During the form rebuild, media_field_widget_form() will create field item + // widget elements using re-indexed deltas, so clear out $form_state['input'] + // to avoid a mismatch between old and new deltas. The rebuilt elements will + // have #default_value set appropriately for the current state of the field, + // so nothing is lost in doing this. + $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2); + drupal_array_set_nested_value($form_state['input'], $parents, NULL); + + $button = $form_state['triggering_element']; + + // Go one level up in the form, to the widgets container. + $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1)); + $field_name = $element['#field_name']; + $langcode = $element['#language']; + $parents = $element['#field_parents']; + + $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2)); + foreach ($submitted_values as $delta => $submitted_value) { + if (!$submitted_value['fid']) { + unset($submitted_values[$delta]); + } + } + + // Re-index deltas after removing empty items. + $submitted_values = array_values($submitted_values); + + // Update form_state values. + drupal_array_set_nested_value($form_state['values'], array_slice($button['#parents'], 0, -2), $submitted_values); + + // Update items. + $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state); + $field_state['items'] = $submitted_values; + field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state); +} + +/** + * Returns HTML for an individual media widget. + * + * @param $variables + * An associative array containing: + * - element: A render element representing the widget. + * + * @ingroup themeable + */ +function theme_media_widget($variables) { + $element = $variables['element']; + $output = ''; + + // The "form-media" class is required for proper Ajax functionality. + $output .= '<div id="' . $element['#id'] . '" class="media-widget form-media clearfix">'; + $output .= drupal_render_children($element); + $output .= '</div>'; + + return $output; +} + +/** + * Returns HTML for a group of media widgets. + * + * @param $variables + * An associative array containing: + * - element: A render element representing the widgets. + * + * @ingroup themeable + */ +function theme_media_widget_multiple($variables) { + $element = $variables['element']; + + // Special ID and classes for draggable tables. + $weight_class = $element['#id'] . '-weight'; + $table_id = $element['#id'] . '-table'; + + // Build up a table of applicable fields. + $headers = array(); + $headers[] = t('File information'); + if ($element['#display_field']) { + $headers[] = array( + 'data' => t('Display'), + 'class' => array('checkbox'), + ); + } + $headers[] = t('Weight'); + $headers[] = t('Operations'); + + // Get our list of widgets in order (needed when the form comes back after + // preview or failed validation). + $widgets = array(); + foreach (element_children($element) as $key) { + $widgets[] = &$element[$key]; + } + usort($widgets, '_field_sort_items_value_helper'); + + $rows = array(); + foreach ($widgets as $key => &$widget) { + // Save the uploading row for last. + if ($widget['#file'] == FALSE) { + $widget['#title'] = $element['#file_upload_title']; + $widget['#description'] = $element['#file_upload_description']; + continue; + } + + // Delay rendering of the buttons, so that they can be rendered later in the + // "operations" column. + $operations_elements = array(); + foreach (element_children($widget) as $sub_key) { + if (isset($widget[$sub_key]['#type']) && ($widget[$sub_key]['#type'] == 'submit' || $widget[$sub_key]['#type'] == 'link')) { + hide($widget[$sub_key]); + $operations_elements[] = &$widget[$sub_key]; + } + } + + // Delay rendering of the "Display" option and the weight selector, so that + // each can be rendered later in its own column. + if ($element['#display_field']) { + hide($widget['display']); + } + hide($widget['_weight']); + + // Render everything else together in a column, without the normal wrappers. + $widget['#theme_wrappers'] = array(); + $information = drupal_render($widget); + + // Render the previously hidden elements, using render() instead of + // drupal_render(), to undo the earlier hide(). + $operations = ''; + foreach ($operations_elements as $operation_element) { + $operations .= render($operation_element); + } + $display = ''; + if ($element['#display_field']) { + unset($widget['display']['#title']); + $display = array( + 'data' => render($widget['display']), + 'class' => array('checkbox'), + ); + } + $widget['_weight']['#attributes']['class'] = array($weight_class); + $weight = render($widget['_weight']); + + // Arrange the row with all of the rendered columns. + $row = array(); + $row[] = $information; + if ($element['#display_field']) { + $row[] = $display; + } + $row[] = $weight; + $row[] = $operations; + $rows[] = array( + 'data' => $row, + 'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'), + ); + } + + drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class); + + $output = ''; + $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id))); + $output .= drupal_render_children($element); + return $output; +} + +/** + * Returns HTML for help text. + * + * @param $variables + * An associative array containing: + * - description: The normal description for this field, specified by the + * user. + * + * @ingroup themeable + */ +function theme_media_upload_help($variables) { + $description = $variables['description']; + + $descriptions = array(); + + if (strlen($description)) { + $descriptions[] = $description; + } + + return implode('<br />', $descriptions); +} + +/** + * Implements hook_field_formatter_info(). + * + * Provides legacy support for the "Large filetype icon" file field formatter. + * This was originally used when media entities contained file fields. The + * current file entity architecture no longer needs this, but people may + * have used this formatter for other file fields on their website. + * + * @todo Some day, remove this. + */ +function media_field_formatter_info() { + $formatters = array( + 'media_large_icon' => array( + 'label' => t('Large filetype icon'), + 'field types' => array('file'), + 'settings' => array( + 'image_style' => '', + ), + ), + ); + + return $formatters; +} + +/** + * Implements hook_field_formatter_settings_form(). + * + * Legacy support for the "Large filetype icon" file field formatter. + * @see media_field_formatter_info() + */ +function media_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $image_styles = image_style_options(FALSE, PASS_THROUGH); + $element['image_style'] = array( + '#title' => t('Image style'), + '#type' => 'select', + '#default_value' => $settings['image_style'], + '#empty_option' => t('None (original image)'), + '#options' => $image_styles, + ); + + return $element; +} + +/** + * Implements hook_field_formatter_settings_summary(). + * + * Legacy support for the "Large filetype icon" file field formatter. + * @see media_field_formatter_info() + */ +function media_field_formatter_settings_summary($field, $instance, $view_mode) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $summary = array(); + + $image_styles = image_style_options(FALSE, PASS_THROUGH); + // Unset possible 'No defined styles' option. + unset($image_styles['']); + // Styles could be lost because of enabled/disabled modules that defines + // their styles in code. + if (isset($image_styles[$settings['image_style']])) { + $summary[] = t('Image style: @style', array('@style' => $image_styles[$settings['image_style']])); + } + else { + $summary[] = t('Original image'); + } + + return implode('<br />', $summary); +} + +/** + * Implements hook_field_formatter_view(). + * + * Legacy support for the "Large filetype icon" file field formatter. + * @see media_field_formatter_info() + */ +function media_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + + if ($display['type'] == 'media_large_icon') { + foreach ($items as $delta => $item) { + $element[$delta] = array( + '#theme' => 'media_formatter_large_icon', + '#file' => (object) $item, + '#style_name' => $display['settings']['image_style'], + ); + } + } + + return $element; +} diff --git a/sites/all/modules/media/includes/media.pages.inc b/sites/all/modules/media/includes/media.pages.inc new file mode 100644 index 000000000..b22a8b140 --- /dev/null +++ b/sites/all/modules/media/includes/media.pages.inc @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Common pages for the Media module. + */ + +/** + * CTools modal callback for editing a file. + */ +function media_file_edit_modal($form, &$form_state, $file, $js) { + ctools_include('modal'); + ctools_include('ajax'); + + $form_state['ajax'] = $js; + form_load_include($form_state, 'inc', 'file_entity', 'file_entity.pages'); + + $output = ctools_modal_form_wrapper('file_entity_edit', $form_state); + + if ($js) { + $commands = $output; + + if ($form_state['executed']) { + $commands = array(ctools_modal_command_dismiss(t('File saved'))); + } + + print ajax_render($commands); + exit(); + } + + // Otherwise, just return the output. + return $output; +} diff --git a/sites/all/modules/media/includes/media.theme.inc b/sites/all/modules/media/includes/media.theme.inc new file mode 100644 index 000000000..2b57782e7 --- /dev/null +++ b/sites/all/modules/media/includes/media.theme.inc @@ -0,0 +1,102 @@ +<?php + +/** + * @file + * Media Theming + * + * Theming functions for the Media module. + */ + +/** + * Adds a wrapper around a preview of a media file. + */ +function theme_media_thumbnail($variables) { + $label = ''; + $element = $variables['element']; + + // Wrappers to go around the thumbnail. + $attributes = array( + 'title' => $element['#name'], + 'class' => 'media-item', + 'data-fid' => $element['#file']->fid, + ); + $prefix = '<div ' . drupal_attributes($attributes) . '><div class="media-thumbnail">'; + $suffix = '</div></div>'; + + // Arguments for the thumbnail link. + $thumb = $element['#children']; + if (file_entity_access('update', $element['#file'])) { + $target = 'file/' . $element['#file']->fid . '/edit'; + $title = t('Click to edit details'); + } + else { + $target = 'file/' . $element['#file']->fid; + $title = t('Click to view details'); + } + $options = array( + 'query' => drupal_get_destination(), + 'html' => TRUE, + 'attributes' => array('title' => $title), + ); + + // Element should be a field renderable array. This should be improved. + if (!empty($element['#show_names']) && $element['#name']) { + $label = '<div class="label-wrapper"><label class="media-filename">' . $element['#name'] . '</label></div>'; + } + + $output = $prefix; + if (!empty($element['#add_link'])) { + $output .= l($thumb, $target, $options); + } + else { + $output .= $thumb; + } + $output .= $label . $suffix; + return $output; +} + +/** + * Preprocess the media thumbnail. + */ +function template_preprocess_media_thumbnail(&$variables) { + // Set the name for the thumbnail to be the filename. This is done here so + // that other modules can hijack the name displayed if it should not be the + // filename. + $variables['element']['#name'] = isset($variables['element']['#file']->filename) ? check_plain($variables['element']['#file']->filename) : NULL; +} + +/** + * Field formatter for displaying a file as a large icon. + */ +function theme_media_formatter_large_icon($variables) { + $file = $variables['file']; + $icon_dir = variable_get('media_icon_base_directory', 'public://media-icons') . '/' . variable_get('media_icon_set', 'default'); + $icon = file_icon_path($file, $icon_dir); + $variables['path'] = $icon; + + // theme_image() requires the 'alt' attribute passed as its own variable. + // @see http://drupal.org/node/999338 + if (!isset($variables['alt']) && isset($variables['attributes']['alt'])) { + $variables['alt'] = $variables['attributes']['alt']; + } + + // Add image height and width for the image theme functions. + if ($info = image_get_info($icon)) { + $variables += $info; + } + + if ($variables['style_name']) { + $output = theme('image_style', $variables); + } + else { + $output = theme('image', $variables); + } + return $output; +} + +/** + * Add messages to the page. + */ +function template_preprocess_media_dialog_page(&$variables) { + $variables['messages'] = theme('status_messages'); +} diff --git a/sites/all/modules/media/includes/media_views_plugin_display_media_browser.inc b/sites/all/modules/media/includes/media_views_plugin_display_media_browser.inc new file mode 100644 index 000000000..eba27ba63 --- /dev/null +++ b/sites/all/modules/media/includes/media_views_plugin_display_media_browser.inc @@ -0,0 +1,19 @@ +<?php +/** + * @file + * Contains the media browser tab display plugin. + */ + +/** + * Display plugin to provide a view as a tab in the media browser. + * + * This is currently just a stub--there's nothing special we need to set up for + * rendering a view as a tab in the media browser. It would be possible to + * provide a special method that returns the plugin info for + * media_media_browser_plugin_info() and the render array for + * media_media_browser_plugin_view(). + * + * @ingroup views_display_plugins + */ +class media_views_plugin_display_media_browser extends views_plugin_display { +} diff --git a/sites/all/modules/media/includes/media_views_plugin_style_media_browser.inc b/sites/all/modules/media/includes/media_views_plugin_style_media_browser.inc new file mode 100644 index 000000000..413111600 --- /dev/null +++ b/sites/all/modules/media/includes/media_views_plugin_style_media_browser.inc @@ -0,0 +1,55 @@ +<?php + +/** + * @file + * The media browser style plugin. + */ + +/** + * Media Views style plugin. + * + * Style plugin to render media items as an interactive grid for the media + * browser. + * + * @ingroup views_style_plugins + */ +class media_views_plugin_style_media_browser extends views_plugin_style_list { + + // Stores the files loaded with pre_render. + public $files = array(); + + /** + * Set default options. + */ + function option_definition() { + $options = parent::option_definition(); + + $options['type'] = array('default' => 'ul'); + $options['class'] = array('default' => 'media-list-thumbnails'); + $options['wrapper_class'] = array('default' => ''); + + return $options; + } + + /** + * Prevents a problem with views when get_row_class() is not set. + */ + public function get_row_class($row_index) { + } + + /** + * Add the base field (fid) to the query. + */ + public function query() { + if (method_exists($this->view->query, 'add_field')) { + // Normal file_managed based view. + $this->view->query->add_field($this->view->base_table, $this->view->base_field); + } + if (method_exists($this->view->query, 'addField')) { + // Search API based view. + $this->view->query->addField('fid'); + } + parent::query(); + } + +} diff --git a/sites/all/modules/media/js/media.admin.js b/sites/all/modules/media/js/media.admin.js new file mode 100644 index 000000000..058e9318c --- /dev/null +++ b/sites/all/modules/media/js/media.admin.js @@ -0,0 +1,77 @@ +/** + * @file + * Javascript for the interface at admin/content/media and also for interfaces + * related to setting up media fields and for media type administration. + * + * Basically, if it's on the /admin path, it's probably here. + */ + +(function ($) { + +/** + * Functionality for the administrative file listings. + */ +Drupal.behaviors.mediaAdmin = { + attach: function (context) { + // Show a javascript confirmation dialog if a user has files selected and + // they try to switch between the "Thumbnail" and "List" local tasks. + $('.tabs.secondary a').once('media-admin').bind('click', function () { + if ($(':checkbox:checked', $('.file-entity-admin-file-form')).length != 0) { + return confirm(Drupal.t('If you switch views, you will lose your selection.')); + } + }); + + if ($('.media-display-thumbnails').length && !$('.media-thumbnails-select').length) { + // Implements 'select all/none' for thumbnail view. + // @TODO: Support grabbing more than one page of thumbnails. + var allLink = $('<a href="#">' + Drupal.t('all') + '</a>') + .click(function () { + $('.media-display-thumbnails', $(this).parents('form')).find(':checkbox').attr('checked', true).change(); + return false; + }); + var noneLink = $('<a href="#">' + Drupal.t('none') + '</a>') + .click(function () { + $('.media-display-thumbnails', $(this).parents('form')).find(':checkbox').attr('checked', false).change(); + return false; + }); + $('<div class="media-thumbnails-select" />') + .append('<strong>' + Drupal.t('Select') + ':</strong> ') + .append(allLink) + .append(', ') + .append(noneLink) + .prependTo('.media-display-thumbnails') + // If the media item is clicked anywhere other than on the image itself + // check the checkbox. For the record, JS thinks this is wonky. + $('.media-item').bind('click', function (e) { + if ($(e.target).is('img, a')) { + return; + } + var checkbox = $(this).parent().find(':checkbox'); + if (checkbox.is(':checked')) { + checkbox.attr('checked', false).change(); + } else { + checkbox.attr('checked', true).change(); + } + }); + + // Add an extra class to selected thumbnails. + $('.media-display-thumbnails :checkbox').each(function () { + var checkbox = $(this); + if (checkbox.is(':checked')) { + $(checkbox.parents('li').find('.media-item')).addClass('selected'); + } + + checkbox.bind('change.media', function () { + if (checkbox.is(':checked')) { + $(checkbox.parents('li').find('.media-item')).addClass('selected'); + } + else { + $(checkbox.parents('li').find('.media-item')).removeClass('selected'); + } + }); + }); + } + } +}; + +})(jQuery); diff --git a/sites/all/modules/media/js/media.browser.js b/sites/all/modules/media/js/media.browser.js new file mode 100644 index 000000000..88490e598 --- /dev/null +++ b/sites/all/modules/media/js/media.browser.js @@ -0,0 +1,124 @@ +/** + * @file + * Provides default functions for the media browser + */ + +(function ($) { +namespace('Drupal.media.browser'); + +Drupal.media.browser.selectedMedia = []; +Drupal.media.browser.mediaAdded = function () {}; +Drupal.media.browser.selectionFinalized = function (selectedMedia) { + // This is intended to be overridden if a callee wants to be triggered + // when the media selection is finalized from inside the browser. + // This is used for the file upload form for instance. +}; + +Drupal.behaviors.MediaBrowser = { + attach: function (context) { + if (Drupal.settings.media && Drupal.settings.media.selectedMedia) { + Drupal.media.browser.selectMedia(Drupal.settings.media.selectedMedia); + // Fire a confirmation of some sort. + Drupal.media.browser.finalizeSelection(); + } + + // Instantiate the tabs. + var showFunc = function(event, ui) { + // Store index of the tab being activated. + if (parent_iframe = Drupal.media.browser.getParentIframe(window)) { + $(parent_iframe).attr('current_tab', $('#media-tabs-wrapper > ul > li.ui-state-active').index()); + } + }; + var activeTab = Drupal.media.browser.tabFromHash(); + $('#media-browser-tabset').once('MediaBrowser').tabs({ + selected: activeTab, // jquery < 1.9 + active: activeTab, // jquery >= 1.9 + show: showFunc, // jquery ui < 1.8 + activate: showFunc // jquery ui >= 1.8 + }); + + $('.media-browser-tab').each( Drupal.media.browser.validateButtons ); + } + // Wait for additional params to be passed in. +}; + +Drupal.media.browser.getParentIframe = function (window) { + var arrFrames = parent.document.getElementsByTagName("IFRAME"); + for (var i = 0; i < arrFrames.length; i++) { + if (arrFrames[i].contentWindow === window) { + return arrFrames[i]; + } + } +} + +/** + * Get index of the active tab from window.location.hash + */ +Drupal.media.browser.tabFromHash = function () { + if (parent_iframe = Drupal.media.browser.getParentIframe(window)) { + return $(parent_iframe).attr('current_tab'); + } + return 0; +}; + +Drupal.media.browser.launch = function () { + +}; + +Drupal.media.browser.validateButtons = function() { + // The media browser runs in an IFRAME. The Drupal.media.popups.mediaBrowser() + // function sets up the IFRAME and an "OK" button that is outside of the + // IFRAME, so that its click handlers can destroy the IFRAME while retaining + // information about what media items were selected. However, Drupal UI + // convention is to place all action buttons on the same "line" at the bottom + // of the form, so if the form within the IFRAME contains a "Submit" button or + // other action buttons, then the "OK" button will appear below the IFRAME + // which breaks this convention and is confusing to the user. Therefore, we + // add a "Submit" button inside the IFRAME, and have its click action trigger + // the click action of the corresponding "OK" button that is outside the + // IFRAME. media.css contains CSS rules that hide the outside buttons. + + // If a submit button is present, another round-trip to the server is needed + // before the user's selection is finalized. For these cases, when the form's + // real Submit button is clicked, the server either returns another form for + // the user to fill out, or else a completion page that contains or sets the + // Drupal.media.browser.selectedMedia variable. If the latter, then + // Drupal.media.popups.mediaBrowser.mediaBrowserOnLoad() auto-triggers the + // "OK" button action to finalize the selection and remove the IFRAME. + + // We need to check for the fake submit button that is used on non-form based + // pane content. On these items we need to bind the clicks so that media can + // be selected or the window can be closed. This is still a hacky approach, + // but it is a step in the right direction. + + $('a.button.fake-submit', this).once().bind('click', Drupal.media.browser.submit); +}; + +Drupal.media.browser.submit = function () { + // @see Drupal.media.browser.validateButtons(). + var buttons = $(parent.window.document.body).find('#mediaBrowser').parent('.ui-dialog').find('.ui-dialog-buttonpane button'); + buttons[0].click(); + + // Return false to prevent the fake link "click" from continuing. + return false; +} + +Drupal.media.browser.selectMedia = function (selectedMedia) { + Drupal.media.browser.selectedMedia = selectedMedia; +}; + +Drupal.media.browser.selectMediaAndSubmit = function (selectedMedia) { + Drupal.media.browser.selectedMedia = selectedMedia; + Drupal.media.browser.submit(); +}; + +Drupal.media.browser.finalizeSelection = function () { + if (!Drupal.media.browser.selectedMedia) { + throw new exception(Drupal.t('Cannot continue, nothing selected')); + } + else { + Drupal.media.browser.selectionFinalized(Drupal.media.browser.selectedMedia); + } +}; + +}(jQuery)); diff --git a/sites/all/modules/media/js/media.core.js b/sites/all/modules/media/js/media.core.js new file mode 100644 index 000000000..f99971c85 --- /dev/null +++ b/sites/all/modules/media/js/media.core.js @@ -0,0 +1,19 @@ + +/** + * Creates a namespace. + * + * @return + * The created namespace object. + */ +function namespace () { + var a=arguments, o=null, i, j, d; + for (i=0; i<a.length; i=i+1) { + d=a[i].split("."); + o=window; + for (j=0; j<d.length; j=j+1) { + o[d[j]]=o[d[j]] || {}; + o=o[d[j]]; + } + } + return o; +}; diff --git a/sites/all/modules/media/js/media.js b/sites/all/modules/media/js/media.js new file mode 100644 index 000000000..5288dd307 --- /dev/null +++ b/sites/all/modules/media/js/media.js @@ -0,0 +1,125 @@ +/** + * @file + * Provides JavaScript additions to the media field widget. + * + * This file provides support for launching the media browser to select existing + * files and disabling of other media fields during Ajax uploads (which prevents + * separate media fields from accidentally attaching files). + */ + +(function ($) { + +/** + * Attach behaviors to media element browse fields. + */ +Drupal.behaviors.mediaElement = { + attach: function (context, settings) { + if (settings.media && settings.media.elements) { + $.each(settings.media.elements, function(selector) { + $(selector, context).once('media-browser-launch', function () { + var configuration = settings.media.elements[selector]; + // The user has JavaScript enabled, so display the browse field and hide + // the upload and attach fields which are only used as a fallback in + // case the user is unable to use the media browser. + $(selector, context).children('.browse').show(); + $(selector, context).children('.upload').hide(); + $(selector, context).children('.attach').hide(); + $(selector, context).children('.browse').bind('click', {configuration: configuration}, Drupal.media.openBrowser); + }); + }); + } + } +}; + +/** + * Attach behaviors to the media attach and remove buttons. + */ +Drupal.behaviors.mediaButtons = { + attach: function (context) { + $('input.form-submit', context).bind('mousedown', Drupal.media.disableFields); + }, + detach: function (context) { + $('input.form-submit', context).unbind('mousedown', Drupal.media.disableFields); + } +}; + +/** + * Media attach utility functions. + */ +Drupal.media = Drupal.media || {}; + +/** + * Opens the media browser with the element's configuration settings. + */ +Drupal.media.openBrowser = function (event) { + var clickedButton = this; + var configuration = event.data.configuration.global; + + // Find the file ID, preview and upload fields. + var fidField = $(this).siblings('.fid'); + var previewField = $(this).siblings('.preview'); + var uploadField = $(this).siblings('.upload'); + + // Find the edit and remove buttons. + var editButton = $(this).siblings('.edit'); + var removeButton = $(this).siblings('.remove'); + + // Launch the media browser. + Drupal.media.popups.mediaBrowser(function (mediaFiles) { + // Ensure that there was at least one media file selected. + if (mediaFiles.length < 0) { + return; + } + + // Grab the first of the selected media files. + var mediaFile = mediaFiles[0]; + + // Set the value of the hidden file ID field and trigger a change. + uploadField.val(mediaFile.fid); + uploadField.trigger('change'); + + // Find the attach button and automatically trigger it. + var attachButton = uploadField.siblings('.attach'); + attachButton.trigger('mousedown'); + + // Display a preview of the file using the selected media file's display. + previewField.html(mediaFile.preview); + }, configuration); + + return false; +}; + +/** + * Prevent media browsing when using buttons not intended to browse. + */ +Drupal.media.disableFields = function (event) { + var clickedButton = this; + + // Only disable browse fields for Ajax buttons. + if (!$(clickedButton).hasClass('ajax-processed')) { + return; + } + + // Check if we're working with an "Attach" button. + var $enabledFields = []; + if ($(this).closest('div.media-widget').length > 0) { + $enabledFields = $(this).closest('div.media-widget').find('input.attach'); + } + + // Temporarily disable attach fields other than the one we're currently + // working with. Filter out fields that are already disabled so that they + // do not get enabled when we re-enable these fields at the end of behavior + // processing. Re-enable in a setTimeout set to a relatively short amount + // of time (1 second). All the other mousedown handlers (like Drupal's Ajax + // behaviors) are excuted before any timeout functions are called, so we + // don't have to worry about the fields being re-enabled too soon. + // @todo If the previous sentence is true, why not set the timeout to 0? + var $fieldsToTemporarilyDisable = $('div.media-widget input.attach').not($enabledFields).not(':disabled'); + $fieldsToTemporarilyDisable.attr('disabled', 'disabled'); + setTimeout(function (){ + $fieldsToTemporarilyDisable.attr('disabled', false); + }, 1000); +}; + +})(jQuery); + diff --git a/sites/all/modules/media/js/media.popups.js b/sites/all/modules/media/js/media.popups.js new file mode 100644 index 000000000..2abb96e6f --- /dev/null +++ b/sites/all/modules/media/js/media.popups.js @@ -0,0 +1,384 @@ + +/** + * @file: Popup dialog interfaces for the media project. + * + * Drupal.media.popups.mediaBrowser + * Launches the media browser which allows users to pick a piece of media. + * + * Drupal.media.popups.mediaStyleSelector + * Launches the style selection form where the user can choose + * what format / style they want their media in. + * + */ + +(function ($) { +namespace('Drupal.media.popups'); + +/** + * Media browser popup. Creates a media browser dialog. + * + * @param {function} + * onSelect Callback for when dialog is closed, received (Array + * media, Object extra); + * @param {Object} + * globalOptions Global options that will get passed upon initialization of the browser. + * @see Drupal.media.popups.mediaBrowser.getDefaults(); + * + * @param {Object} + * pluginOptions Options for specific plugins. These are passed + * to the plugin upon initialization. If a function is passed here as + * a callback, it is obviously not passed, but is accessible to the plugin + * in Drupal.settings.variables. + * + * Example + * pluginOptions = {library: {url_include_patterns:'/foo/bar'}}; + * + * @param {Object} + * widgetOptions Options controlling the appearance and behavior of the + * modal dialog. + * @see Drupal.media.popups.mediaBrowser.getDefaults(); + */ +Drupal.media.popups.mediaBrowser = function (onSelect, globalOptions, pluginOptions, widgetOptions) { + var options = Drupal.media.popups.mediaBrowser.getDefaults(); + options.global = $.extend({}, options.global, globalOptions); + options.plugins = pluginOptions; + options.widget = $.extend({}, options.widget, widgetOptions); + + // Create it as a modal window. + var browserSrc = options.widget.src; + if ($.isArray(browserSrc) && browserSrc.length) { + browserSrc = browserSrc[browserSrc.length - 1]; + } + // Params to send along to the iframe. WIP. + var params = {}; + $.extend(params, options.global); + params.plugins = options.plugins; + + browserSrc += '&' + $.param(params); + var mediaIframe = Drupal.media.popups.getPopupIframe(browserSrc, 'mediaBrowser'); + // Attach the onLoad event + mediaIframe.bind('load', options, options.widget.onLoad); + + /** + * Setting up the modal dialog + */ + var ok = 'OK'; + var notSelected = 'You have not selected anything!'; + + if (Drupal && Drupal.t) { + ok = Drupal.t(ok); + notSelected = Drupal.t(notSelected); + } + + // @todo: let some options come through here. Currently can't be changed. + var dialogOptions = options.dialog; + + dialogOptions.buttons[ok] = function () { + var selected = this.contentWindow.Drupal.media.browser.selectedMedia; + if (selected.length < 1) { + alert(notSelected); + return; + } + onSelect(selected); + $(this).dialog("close"); + }; + + var dialog = mediaIframe.dialog(dialogOptions); + + Drupal.media.popups.sizeDialog(dialog); + Drupal.media.popups.resizeDialog(dialog); + Drupal.media.popups.scrollDialog(dialog); + Drupal.media.popups.overlayDisplace(dialog.parents(".ui-dialog")); + + return mediaIframe; +}; + +Drupal.media.popups.mediaBrowser.mediaBrowserOnLoad = function (e) { + var options = e.data; + if (this.contentWindow.Drupal.media == undefined) return; + + if (this.contentWindow.Drupal.media.browser.selectedMedia.length > 0) { + var ok = (Drupal && Drupal.t) ? Drupal.t('OK') : 'OK'; + var ok_func = $(this).dialog('option', 'buttons')[ok]; + ok_func.call(this); + return; + } +}; + +Drupal.media.popups.mediaBrowser.getDefaults = function () { + return { + global: { + types: [], // Types to allow, defaults to all. + activePlugins: [] // If provided, a list of plugins which should be enabled. + }, + widget: { // Settings for the actual iFrame which is launched. + src: Drupal.settings.media.browserUrl, // Src of the media browser (if you want to totally override it) + onLoad: Drupal.media.popups.mediaBrowser.mediaBrowserOnLoad // Onload function when iFrame loads. + }, + dialog: Drupal.media.popups.getDialogOptions() + }; +}; + +Drupal.media.popups.mediaBrowser.finalizeSelection = function () { + var selected = this.contentWindow.Drupal.media.browser.selectedMedia; + if (selected.length < 1) { + alert(notSelected); + return; + } + onSelect(selected); + $(this).dialog("close"); +} + +/** + * Style chooser Popup. Creates a dialog for a user to choose a media style. + * + * @param mediaFile + * The mediaFile you are requesting this formatting form for. + * @todo: should this be fid? That's actually all we need now. + * + * @param Function + * onSubmit Function to be called when the user chooses a media + * style. Takes one parameter (Object formattedMedia). + * + * @param Object + * options Options for the mediaStyleChooser dialog. + */ +Drupal.media.popups.mediaStyleSelector = function (mediaFile, onSelect, options) { + var defaults = Drupal.media.popups.mediaStyleSelector.getDefaults(); + // @todo: remove this awful hack :( + if (typeof defaults.src === 'string' ) { + defaults.src = defaults.src.replace('-media_id-', mediaFile.fid) + '&fields=' + encodeURIComponent(JSON.stringify(mediaFile.fields)); + } + else { + var src = defaults.src.shift(); + defaults.src.unshift(src); + defaults.src = src.replace('-media_id-', mediaFile.fid) + '&fields=' + encodeURIComponent(JSON.stringify(mediaFile.fields)); + } + options = $.extend({}, defaults, options); + // Create it as a modal window. + var mediaIframe = Drupal.media.popups.getPopupIframe(options.src, 'mediaStyleSelector'); + // Attach the onLoad event + mediaIframe.bind('load', options, options.onLoad); + + /** + * Set up the button text + */ + var ok = 'OK'; + var notSelected = 'Very sorry, there was an unknown error embedding media.'; + + if (Drupal && Drupal.t) { + ok = Drupal.t(ok); + notSelected = Drupal.t(notSelected); + } + + // @todo: let some options come through here. Currently can't be changed. + var dialogOptions = Drupal.media.popups.getDialogOptions(); + + dialogOptions.buttons[ok] = function () { + + var formattedMedia = this.contentWindow.Drupal.media.formatForm.getFormattedMedia(); + if (!formattedMedia) { + alert(notSelected); + return; + } + onSelect(formattedMedia); + $(this).dialog("close"); + }; + + var dialog = mediaIframe.dialog(dialogOptions); + + Drupal.media.popups.sizeDialog(dialog); + Drupal.media.popups.resizeDialog(dialog); + Drupal.media.popups.scrollDialog(dialog); + Drupal.media.popups.overlayDisplace(dialog.parents(".ui-dialog")); + + return mediaIframe; +}; + +Drupal.media.popups.mediaStyleSelector.mediaBrowserOnLoad = function (e) { +}; + +Drupal.media.popups.mediaStyleSelector.getDefaults = function () { + return { + src: Drupal.settings.media.styleSelectorUrl, + onLoad: Drupal.media.popups.mediaStyleSelector.mediaBrowserOnLoad + }; +}; + + +/** + * Style chooser Popup. Creates a dialog for a user to choose a media style. + * + * @param mediaFile + * The mediaFile you are requesting this formatting form for. + * @todo: should this be fid? That's actually all we need now. + * + * @param Function + * onSubmit Function to be called when the user chooses a media + * style. Takes one parameter (Object formattedMedia). + * + * @param Object + * options Options for the mediaStyleChooser dialog. + */ +Drupal.media.popups.mediaFieldEditor = function (fid, onSelect, options) { + var defaults = Drupal.media.popups.mediaFieldEditor.getDefaults(); + // @todo: remove this awful hack :( + defaults.src = defaults.src.replace('-media_id-', fid); + options = $.extend({}, defaults, options); + // Create it as a modal window. + var mediaIframe = Drupal.media.popups.getPopupIframe(options.src, 'mediaFieldEditor'); + // Attach the onLoad event + // @TODO - This event is firing too early in IE on Windows 7, + // - so the height being calculated is too short for the content. + mediaIframe.bind('load', options, options.onLoad); + + /** + * Set up the button text + */ + var ok = 'OK'; + var notSelected = 'Very sorry, there was an unknown error embedding media.'; + + if (Drupal && Drupal.t) { + ok = Drupal.t(ok); + notSelected = Drupal.t(notSelected); + } + + // @todo: let some options come through here. Currently can't be changed. + var dialogOptions = Drupal.media.popups.getDialogOptions(); + + dialogOptions.buttons[ok] = function () { + var formattedMedia = this.contentWindow.Drupal.media.formatForm.getFormattedMedia(); + if (!formattedMedia) { + alert(notSelected); + return; + } + onSelect(formattedMedia); + $(this).dialog("close"); + }; + + var dialog = mediaIframe.dialog(dialogOptions); + + Drupal.media.popups.sizeDialog(dialog); + Drupal.media.popups.resizeDialog(dialog); + Drupal.media.popups.scrollDialog(dialog); + Drupal.media.popups.overlayDisplace(dialog); + + return mediaIframe; +}; + +Drupal.media.popups.mediaFieldEditor.mediaBrowserOnLoad = function (e) { + +}; + +Drupal.media.popups.mediaFieldEditor.getDefaults = function () { + return { + // @todo: do this for real + src: '/media/-media_id-/edit?render=media-popup', + onLoad: Drupal.media.popups.mediaFieldEditor.mediaBrowserOnLoad + }; +}; + + +/** + * Generic functions to both the media-browser and style selector + */ + +/** + * Returns the commonly used options for the dialog. + */ +Drupal.media.popups.getDialogOptions = function () { + return { + buttons: {}, + dialogClass: Drupal.settings.media.dialogOptions.dialogclass, + modal: Drupal.settings.media.dialogOptions.modal, + draggable: Drupal.settings.media.dialogOptions.draggable, + resizable: Drupal.settings.media.dialogOptions.resizable, + minWidth: Drupal.settings.media.dialogOptions.minwidth, + width: Drupal.settings.media.dialogOptions.width, + height: Drupal.settings.media.dialogOptions.height, + position: Drupal.settings.media.dialogOptions.position, + overlay: { + backgroundColor: Drupal.settings.media.dialogOptions.overlay.backgroundcolor, + opacity: Drupal.settings.media.dialogOptions.overlay.opacity + }, + zIndex: Drupal.settings.media.dialogOptions.zindex, + close: function (event, ui) { + $(event.target).remove(); + } + }; +}; + +/** + * Get an iframe to serve as the dialog's contents. Common to both plugins. + */ +Drupal.media.popups.getPopupIframe = function (src, id, options) { + var defaults = {width: '100%', scrolling: 'auto'}; + var options = $.extend({}, defaults, options); + + return $('<iframe class="media-modal-frame"/>') + .attr('src', src) + .attr('width', options.width) + .attr('id', id) + .attr('scrolling', options.scrolling); +}; + +Drupal.media.popups.overlayDisplace = function (dialog) { + if (parent.window.Drupal.overlay && jQuery.isFunction(parent.window.Drupal.overlay.getDisplacement)) { + var overlayDisplace = parent.window.Drupal.overlay.getDisplacement('top'); + if (dialog.offset().top < overlayDisplace) { + dialog.css('top', overlayDisplace); + } + } +} + +/** + * Size the dialog when it is first loaded and keep it centered when scrolling. + * + * @param jQuery dialogElement + * The element which has .dialog() attached to it. + */ +Drupal.media.popups.sizeDialog = function (dialogElement) { + if (!dialogElement.is(':visible')) { + return; + } + var windowWidth = $(window).width(); + var dialogWidth = windowWidth * 0.8; + var windowHeight = $(window).height(); + var dialogHeight = windowHeight * 0.8; + + dialogElement.dialog("option", "width", dialogWidth); + dialogElement.dialog("option", "height", dialogHeight); + dialogElement.dialog("option", "position", 'center'); + + $('.media-modal-frame').width('100%'); +} + +/** + * Resize the dialog when the window changes. + * + * @param jQuery dialogElement + * The element which has .dialog() attached to it. + */ +Drupal.media.popups.resizeDialog = function (dialogElement) { + $(window).resize(function() { + Drupal.media.popups.sizeDialog(dialogElement); + }); +} + +/** + * Keeps the dialog centered when the window is scrolled. + * + * @param jQuery dialogElement + * The element which has .dialog() attached to it. + */ +Drupal.media.popups.scrollDialog = function (dialogElement) { + // Keep the dialog window centered when scrolling. + $(window).scroll(function() { + if (!dialogElement.is(':visible')) { + return; + } + dialogElement.dialog("option", "position", 'center'); + }); +} + +})(jQuery); diff --git a/sites/all/modules/media/js/plugins/media.views.js b/sites/all/modules/media/js/plugins/media.views.js new file mode 100644 index 000000000..455288d65 --- /dev/null +++ b/sites/all/modules/media/js/plugins/media.views.js @@ -0,0 +1,173 @@ +/** + * @file + * Handles the JS for the views file browser. + * + * Note that this does not currently support multiple file selection + */ + +(function ($) { + +namespace('Drupal.media.browser.views'); +Drupal.behaviors.mediaViews = { + attach: function (context, settings) { + + // Make sure when pressing enter on text inputs, the form isn't submitted + $('.ctools-auto-submit-full-form .views-exposed-form input:text, input:text.ctools-auto-submit', context) + .filter(':not(.ctools-auto-submit-exclude)') + .bind('keydown keyup', function (e) { + if(e.keyCode === 13) { + e.stopImmediatePropagation(); + e.preventDefault(); + } + }); + // Disable the links on media items list + $('.view-content ul.media-list-thumbnails a').click(function() { + return false; + }); + + // We loop through the views listed in Drupal.settings.media.browser.views + // and set them up individually. + var views_ids = []; + for(var key in Drupal.settings.media.browser.views){ + views_ids.push(key); + } + + for (var i = 0; i < views_ids.length; i++) { + var views_id = views_ids[i]; + for (var j= 0; j < Drupal.settings.media.browser.views[views_id].length; j++) { + var views_display_id = Drupal.settings.media.browser.views[views_id][j], + view = $('.view-id-' + views_id + '.view-display-id-' + views_display_id); + if (view.length) { + Drupal.media.browser.views.setup(view); + } + } + } + + // Reset the state on tab-changes- bind on the 'select' event on the tabset + $('#media-browser-tabset').bind('tabsselect', function(event, ui) { + var view = $('.view', ui.panel); + if (view.length) { + Drupal.media.browser.views.select(view); + } + }); + + } +} + +/** + * Event-function that is called with a view, when the tab containing that + * view is selected. + */ +Drupal.media.browser.views.select = function(view) { + // Reset the list of selected files + Drupal.media.browser.selectMedia([]); + + // Reset all 'selected'-status. + $('.view-content .media-item', view).removeClass('selected'); +} + +/** + * Setup function. Called once for every Media Browser view. + * + * Sets up event-handlers for selecting items in the view. + */ +Drupal.media.browser.views.setup = function(view) { + // Ensure we only setup each view once.. + if ($(view).hasClass('media-browser-views-processed')) { + return; + } + + // Reset the list of selected files + Drupal.media.browser.selectMedia([]); + + // Catch double click to submit a single item. + $('.view-content .media-item', view).bind('dblclick', function () { + var fid = $(this).closest('.media-item[data-fid]').data('fid'), + selectedFiles = new Array(); + + // Remove all currently selected files + $('.view-content .media-item', view).removeClass('selected'); + + // Mark it as selected + $(this).addClass('selected'); + + // Because the files are added using drupal_add_js() and due to the fact + // that drupal_get_js() runs a drupal_array_merge_deep() which re-numbers + // numeric key values, we have to search in Drupal.settings.media.files + // for the matching file ID rather than referencing it directly. + for (index in Drupal.settings.media.files) { + if (Drupal.settings.media.files[index].fid == fid) { + selectedFiles.push(Drupal.settings.media.files[index]); + + // If multiple tabs contains the same file, it will be present in the + // files-array multiple times, so we break out early so we don't have + // it in the selectedFiles array multiple times. + // This would interfer with multi-selection, so... + break; + } + } + Drupal.media.browser.selectMediaAndSubmit(selectedFiles); + }); + + + // Catch the click on a media item + $('.view-content .media-item', view).bind('click', function () { + var fid = $(this).closest('.media-item[data-fid]').data('fid'), + selectedFiles = new Array(); + + // Remove all currently selected files + $('.view-content .media-item', view).removeClass('selected'); + + // Mark it as selected + $(this).addClass('selected'); + + // Multiselect! + if (Drupal.settings.media.browser.params.multiselect) { + // Loop through the already selected files + for (index in Drupal.media.browser.selectedMedia) { + var currentFid = Drupal.media.browser.selectedMedia[index].fid; + + // If the current file exists in the list of already selected + // files, we deselect instead of selecting + if (currentFid == fid) { + $(this).removeClass('selected'); + // If we change the fid, the later matching won't + // add it back again because it can't find it. + fid = NaN; + + // The previously selected file wasn't clicked, so we retain it + // as an active file + } + else { + // Add to list of already selected files + selectedFiles.push(Drupal.media.browser.selectedMedia[index]); + + // Mark it as selected + $('.view-content *[data-fid=' + currentFid + '].media-item', view).addClass('selected'); + } + } + } + + // Because the files are added using drupal_add_js() and due to the fact + // that drupal_get_js() runs a drupal_array_merge_deep() which re-numbers + // numeric key values, we have to search in Drupal.settings.media.files + // for the matching file ID rather than referencing it directly. + for (index in Drupal.settings.media.files) { + if (Drupal.settings.media.files[index].fid == fid) { + selectedFiles.push(Drupal.settings.media.files[index]); + + // If multiple tabs contains the same file, it will be present in the + // files-array multiple times, so we break out early so we don't have + // it in the selectedFiles array multiple times. + // This would interfer with multi-selection, so... + break; + } + } + Drupal.media.browser.selectMedia(selectedFiles); + }); + + // Add the processed class, so we dont accidentally process the same element twice.. + $(view).addClass('media-browser-views-processed'); +} + +}(jQuery)); diff --git a/sites/all/modules/media/js/util/ba-debug.min.js b/sites/all/modules/media/js/util/ba-debug.min.js new file mode 100644 index 000000000..1c50e7fb8 --- /dev/null +++ b/sites/all/modules/media/js/util/ba-debug.min.js @@ -0,0 +1,12 @@ +/* + * debug - v0.3 - 6/8/2009 + * http://benalman.com/projects/javascript-debug-console-log/ + * + * Copyright (c) 2009 "Cowboy" Ben Alman + * Licensed under the MIT license + * http://benalman.com/about/license/ + * + * With lots of help from Paul Irish! + * http://paulirish.com/ + */ +window.debug=(function(){var c=this,e=Array.prototype.slice,b=c.console,i={},f,g,j=9,d=["error","warn","info","debug","log"],m="assert clear count dir dirxml group groupEnd profile profileEnd time timeEnd trace".split(" "),k=m.length,a=[];while(--k>=0){(function(n){i[n]=function(){j!==0&&b&&b[n]&&b[n].apply(b,arguments)}})(m[k])}k=d.length;while(--k>=0){(function(n,o){i[o]=function(){var q=e.call(arguments),p=[o].concat(q);a.push(p);h(p);if(!b||!l(n)){return}b.firebug?b[o].apply(c,q):b[o]?b[o](q):b.log(q)}})(k,d[k])}function h(n){if(f&&(g||!b||!b.log)){f.apply(c,n)}}i.setLevel=function(n){j=typeof n==="number"?n:9};function l(n){return j>0?j>n:d.length+j<=n}i.setCallback=function(){var o=e.call(arguments),n=a.length,p=n;f=o.shift()||null;g=typeof o[0]==="boolean"?o.shift():false;p-=typeof o[0]==="number"?o.shift():n;while(p<n){h(a[p++])}};return i})();
\ No newline at end of file diff --git a/sites/all/modules/media/js/util/json2.js b/sites/all/modules/media/js/util/json2.js new file mode 100644 index 000000000..39d8f3706 --- /dev/null +++ b/sites/all/modules/media/js/util/json2.js @@ -0,0 +1,481 @@ +/* + http://www.JSON.org/json2.js + 2009-09-29 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/sites/all/modules/media/media-views-view-media-browser.tpl.php b/sites/all/modules/media/media-views-view-media-browser.tpl.php new file mode 100644 index 000000000..b37f0cf52 --- /dev/null +++ b/sites/all/modules/media/media-views-view-media-browser.tpl.php @@ -0,0 +1,23 @@ +<?php +/** + * @file media-views-view-media-browser.tpl.php + * View template to display a grid of media previews in the media browser. + * + * @see views-view-list.tpl.php + * @see template_preprocess_media_views_view_media_browser() + * @ingroup views_templates + */ +?> + +<?php print $wrapper_prefix; ?> + <div class="clearfix"> + <?php print $list_type_prefix; ?> + <?php foreach ($rows as $id => $row): ?> + <li id="media-item-<?php print $row->fid; ?>" class="<?php print $classes_array[$id]; ?>"> + <?php print $row->preview; ?> + </li> + <?php endforeach; ?> + <?php print $list_type_suffix; ?> + <div id="status"></div> + </div> +<?php print $wrapper_suffix; ?> diff --git a/sites/all/modules/media/media.api.php b/sites/all/modules/media/media.api.php new file mode 100644 index 000000000..ecde61b24 --- /dev/null +++ b/sites/all/modules/media/media.api.php @@ -0,0 +1,137 @@ +<?php + +/** + * @file + * Hooks provided by the Media module. + */ + +/** + * Parses a url or embedded code into a unique URI. + * + * @param string $url + * The original URL or embed code to parse. + * + * @return array + * The unique URI for the file, based on its stream wrapper, or NULL. + * + * @see hook_media_parse_alter() + * @see media_parse_to_file() + * @see media_add_from_url_validate() + */ +function hook_media_parse($url) { + // Only parse URLs from our website of choice: examplevideo.com + if (substr($url, 0, 27) == 'http://www.examplevideo.com') { + // Each video has a 5 digit ID, i.e. http://www.examplevideo.com/12345 + // Grab the ID and use it in our URI. + $id = substr($url, 28, 33); + return file_stream_wrapper_uri_normalize('examplevideo://video/' . $id); + } +} + +/** + * Alters the parsing of urls and embedded codes into unique URIs. + * + * @param string $success + * The unique URI for the file, based on its stream wrapper, or NULL. + * @param array $context + * A nested array of contextual information containing the following keys: + * - url: The original URL or embed code to parse. + * - module: The name of the module which is attempting to parse the url or + * embedded code into a unique URI. + * + * @see hook_media_parse() + * @see hook_media_browser_plugin_info() + * @see media_get_browser_plugin_info() + */ +function hook_media_parse_alter(&$success, $context) { + $url = $context['url']; + $url_info = parse_url($url); + + // Restrict users to only embedding secure links. + if ($url_info['scheme'] != 'https') { + $success = NULL; + } + + // Use a custom handler for detecting YouTube videos. + if ($context['module' == 'media_youtube']) { + $handler = new CustomYouTubeHandler($url); + $success = $handler->parse($url); + } +} + +/** + * Returns a list of plugins for the media browser. + * + * @return array + * A nested array of plugin information, keyed by plugin name. Each plugin + * info array may have the following keys: + * - title: (required) A name for the tab in the media browser. + * - class: (required) The class name of the handler. This class must + * implement a view() method, and may (should) extend the + * @link MediaBrowserPlugin MediaBrowserPlugin @endlink class. + * - weight: (optional) Integer to determine the tab order. Defaults to 0. + * - access callback: (optional) A callback for user access checks. + * - access arguments: (optional) An array of arguments for the user access + * check. + * + * Additional custom keys may be provided for use by the handler. + * + * @see hook_media_browser_plugin_info_alter() + * @see media_get_browser_plugin_info() + */ +function hook_media_browser_plugin_info() { + $info['media_upload'] = array( + 'title' => t('Upload'), + 'class' => 'MediaBrowserUpload', + 'weight' => -10, + 'access callback' => 'user_access', + 'access arguments' => array('create files'), + ); + + return $info; +} + +/** + * Alter the list of plugins for the media browser. + * + * @param array $info + * The associative array of media browser plugin definitions from + * hook_media_browser_plugin_info(). + * + * @see hook_media_browser_plugin_info() + * @see media_get_browser_plugin_info() + */ +function hook_media_browser_plugin_info_alter(&$info) { + $info['media_upload']['title'] = t('Upload 2.0'); + $info['media_upload']['class'] = 'MediaBrowserUploadImproved'; +} + +/** + * Alter the plugins before they are rendered. + * + * @param array $plugin_output + * The associative array of media browser plugin information from + * media_get_browser_plugin_info(). + * + * @see hook_media_browser_plugin_info() + * @see media_get_browser_plugin_info() + */ +function hook_media_browser_plugins_alter(&$plugin_output) { + $plugin_output['upload']['form']['upload']['#title'] = t('Upload 2.0'); + $plugin_output['media_internet']['form']['embed_code']['#size'] = 100; +} + +/** + * Alter a singleton of the params passed to the media browser. + * + * @param array $stored_params + * An array of parameters provided when a media_browser is launched. + * + * @see media_browser() + * @see media_set_browser_params() + */ +function hook_media_browser_params_alter(&$stored_params) { + $stored_params['view_mode'] = 'custom'; + $stored_params['types'][] = 'document'; + unset($stored_params['enabledPlugins'][0]); +} diff --git a/sites/all/modules/media/media.file_default_displays.inc b/sites/all/modules/media/media.file_default_displays.inc new file mode 100644 index 000000000..3e3e2c136 --- /dev/null +++ b/sites/all/modules/media/media.file_default_displays.inc @@ -0,0 +1,69 @@ +<?php + +/** + * @file + * Default display configuration for the default file types. + */ + +/** + * Implements hook_file_default_displays(). + */ +function media_file_default_displays() { + $file_displays = array(); + + // Audio previews should be displayed using a large filetype icon. + $file_display = new stdClass(); + $file_display->api_version = 1; + $file_display->name = 'audio__preview__file_field_media_large_icon'; + $file_display->weight = 49; + $file_display->status = TRUE; + $file_display->settings = array( + 'image_style' => 'media_thumbnail', + ); + $file_displays['audio__preview__file_field_media_large_icon'] = $file_display; + + // Document previews should be displayed using a large filetype icon. + $file_display = new stdClass(); + $file_display->api_version = 1; + $file_display->name = 'document__preview__file_field_media_large_icon'; + $file_display->weight = 49; + $file_display->status = TRUE; + $file_display->settings = array( + 'image_style' => 'media_thumbnail', + ); + $file_displays['document__preview__file_field_media_large_icon'] = $file_display; + + // Image previews should be displayed using a large filetype icon. + $file_display = new stdClass(); + $file_display->api_version = 1; + $file_display->name = 'image__preview__file_field_media_large_icon'; + $file_display->weight = 49; + $file_display->status = TRUE; + $file_display->settings = array( + 'image_style' => 'media_thumbnail', + ); + $file_displays['image__preview__file_field_media_large_icon'] = $file_display; + + // Video previews should be displayed using a large filetype icon. + $file_display = new stdClass(); + $file_display->api_version = 1; + $file_display->name = 'video__preview__file_field_media_large_icon'; + $file_display->weight = 49; + $file_display->status = TRUE; + $file_display->settings = array( + 'image_style' => 'media_thumbnail', + ); + $file_displays['video__preview__file_field_media_large_icon'] = $file_display; + + return $file_displays; +} + +/** + * Implements hook_file_default_displays_alter(). + */ +function media_file_default_displays_alter(&$file_displays) { + // Image previews should be displayed using the media image style. + if (isset($file_displays['image__preview__file_field_image'])) { + $file_displays['image__preview__file_field_image']->settings['image_style'] = 'media_thumbnail'; + } +} diff --git a/sites/all/modules/media/media.info b/sites/all/modules/media/media.info new file mode 100644 index 000000000..c7b8f8710 --- /dev/null +++ b/sites/all/modules/media/media.info @@ -0,0 +1,32 @@ +name = Media +description = Provides the core Media API +package = Media +core = 7.x + +dependencies[] = file_entity +dependencies[] = image +dependencies[] = views + +test_dependencies[] = token + +files[] = includes/MediaReadOnlyStreamWrapper.inc +files[] = includes/MediaBrowserPluginInterface.inc +files[] = includes/MediaBrowserPlugin.inc +files[] = includes/MediaBrowserUpload.inc +files[] = includes/MediaBrowserView.inc +files[] = includes/MediaEntityTranslationHandler.inc +files[] = includes/media_views_plugin_display_media_browser.inc +files[] = includes/media_views_plugin_style_media_browser.inc +files[] = tests/media.test + +configure = admin/config/media/browser + +; We have to add a fake version so Git checkouts do not fail Media dependencies +version = 7.x-2.x-dev + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/media.install b/sites/all/modules/media/media.install new file mode 100644 index 000000000..1e7ae31da --- /dev/null +++ b/sites/all/modules/media/media.install @@ -0,0 +1,1182 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Media module. + */ + +/** + * Implements hook_install(). + */ +function media_install() { + // Make sure that we set the icon base directory variable if it is not + // already set. + $base = variable_get('media_icon_base_directory', NULL); + if (!isset($base)) { + $default_base = 'public://media-icons'; + variable_set('media_icon_base_directory', $default_base); + } + try { + _media_install_copy_icons(); + } + catch (Exception $e) { + watchdog_exception('media', $e); + } +} + +/** + * Copy the media file icons to files directory for use with image styles. + */ +function _media_install_copy_icons() { + $destination = variable_get('media_icon_base_directory', 'public://media-icons') . '/' . variable_get('media_icon_set', 'default'); + if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY)) { + throw new Exception("Unable to create directory $destination."); + } + // @todo If we ever add another default icon set, this should copy all images from one directory up. + $source = drupal_get_path('module', 'media') . '/images/icons/' . variable_get('media_icon_set', 'default'); + $files = file_scan_directory($source, '/.*\.(png|jpg)$/'); + foreach ($files as $file) { + $result = file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_REPLACE); + if (!$result) { + throw new Exception("Unable to copy {$file->uri} to $destination."); + } + } +} + +/** + * Implements hook_uninstall(). + */ +function media_uninstall() { + // Remove variables. + variable_del('media_dialog_theme'); + variable_del('media_icon_base_directory'); + variable_del('media_icon_set'); + variable_del('media_show_deprecated_view_modes'); + + // Dialog options. + variable_del('media_dialogclass'); + variable_del('media_modal'); + variable_del('media_draggable'); + variable_del('media_resizable'); + variable_del('media_minwidth'); + variable_del('media_width'); + variable_del('media_height'); + variable_del('media_position'); + variable_del('media_zindex'); + variable_del('media_backgroundcolor'); + variable_del('media_opacity'); +} + +/** + * Implements hook_update_dependencies(). + */ +function media_update_dependencies() { + // media_update_7200() needs to convert old 'media' permissions to new 'file' + // permissions, so it must run before file_entity_7208 which updates existing + // 'file' permissions to be split per file type. + $dependencies['file_entity'][7208] = array( + 'media' => 7200, + ); + // This update function requires field_update_7002() to run before it since + // the field_bundle_settings variable has been split into separate variables + // per entity type and bundle. + $dependencies['media'][7016] = array( + 'field' => 7002, + 'rules' => 7205, + ); + // Those updates require {file_type} table created. + $dependencies['media'][7204] = array( + 'file_entity' => 7201, + ); + // Require {file_type}.mimetypes column before updating them. + $dependencies['media'][7208] = array( + 'file_entity' => 7210, + ); + $dependencies['media'][7212] = array( + 'file_entity' => 7210, + ); + return $dependencies; +} + +/** + * Implements hook_requirements(). + */ +function media_requirements($phase) { + $t = get_t(); + // Make sure that file_entity module is 2.x version. + // We can't add this check in .info file because drupal.org testbot can't + // handle it. See #1734648. + $requirements = array(); + + if ($phase == 'update') { + $info = system_get_info('module', 'file_entity'); + if (strpos($info['version'], '7.x-2') === FALSE) { + $requirements['file_entity'] = array( + 'title' => $t('File entity 2.x'), + 'value' => $t('Wrong version'), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('Media 2.x requires <a href="@url">File entity 2.x</a>. Please download the correct version and make sure you have deleted the file_entity folder inside the media module directory.', array('@url' => 'http://drupal.org/project/file_entity')), + ); + } + } + + return $requirements; +} + +/** + * Deprecated update function. + */ +function media_update_7000() { +} + +/** + * Deprecated update function. + */ +function media_update_7001() { +} + +/** + * Create the media_type table from the media_types variable. + */ +function media_update_7002() { + if (db_table_exists('media_type')) { + return; + } + + $schema['media_type'] = array( + 'description' => 'Stores the settings for media types.', + 'fields' => array( + 'name' => array( + 'description' => 'The machine name of the media type.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'label' => array( + 'description' => 'The label of the media type.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'base' => array( + 'description' => 'If this is a base type (i.e. cannot be deleted)', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + 'weight' => array( + 'description' => 'Weight of media type. Determines which one wins when claiming a piece of media (first wins)', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + 'size' => 'normal', + ), + 'type_callback' => array( + 'description' => 'Callback to determine if provided media is of this type.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + 'default' => '', + ), + 'type_callback_args' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + 'description' => 'A serialized array of name value pairs that will be passed to the callback function', + ), + ), + 'primary key' => array('name'), + ); + db_create_table('media_type', $schema['media_type']); + + drupal_load('module', 'media'); + $old_types = variable_get('media_types', array()); + foreach ($old_types as $type) { + // Was an error in the original creation. + if (isset($type->callbacks)) { + unset($type->callbacks); + } + $type->name = $type->machine_name; + unset($type->machine_name); + db_merge('media_type') + ->key(array('name' => $type->name)) + ->fields((array) $type) + ->execute(); + } + variable_del('media_types'); +} + +/** + * We now prefix media namespaced variables with media__, so fix old variables. + */ +function media_update_7003() { + drupal_load('module', 'media'); + foreach (media_variable_default() as $variable => $value) { + if (($test = variable_get('media_' . $variable, TRUE)) == variable_get('media_' . $variable, FALSE)) { + media_variable_set($variable, $test); + variable_del('media_' . $variable); + } + } +} + +/** + * Empty update function to trigger a menu rebuild. + */ +function media_update_7004() { +} + +/** + * Deprecated update function. + */ +function media_update_7005() { +} + +/** + * Rename the file table to file_managed in case head2head was used. + */ +function media_update_7006() { + if (db_table_exists('file') && !db_table_exists('file_managed')) { + db_rename_table('file', 'file_managed'); + } +} + +/** + * Deprecated update function. + */ +function media_update_7007() { +} + +/** + * Empty function. + */ +function media_update_7008() { +} + +/** + * Deprecated update function. + */ +function media_update_7009() { +} + +/** + * Deprecated update function. + */ +function media_update_7010() { +} + +/** + * Empty update function. + */ +function media_update_7011() { +} + +/** + * Empty update function. + */ +function media_update_7012() { +} + +/** + * Work around a core bug where text format cacheability is not updated. + * + * @see http://drupal.org/node/993230 + */ +function media_update_7013() { + $formats = filter_formats(); + foreach ($formats as $format) { + $format->filters = filter_list_format($format->format); + // filter_format_save() expects filters to be an array, however + // filter_list_format() gives us objects. + foreach ($format->filters as $key => $value) { + $format->filters[$key] = (array) $value; + } + filter_format_save($format); + } +} + +/** + * Rename the media__dialog_get_theme_name variable to media__dialog_theme. + */ +function media_update_7014() { + if ($old_value = variable_get('media__dialog_get_theme_name')) { + variable_del('media__dialog_get_theme_name'); + variable_set('media__dialog_theme', $old_value); + } +} + +/** + * Empty update function to trigger a registry rebuild. + */ +function media_update_7015() { +} + +/** + * Convert Media entities to File entities. + * + * This update function requires field_update_7002() to run before it since + * the field_bundle_settings variable has been split into separate variables + * per entity type and bundle. + * + * @see http://drupal.org/node/1418708 + * @see http://drupal.org/node/1211008 + */ +function media_update_7016() { + // Allow File Entity module to take over the {file_managed}.type field. It + // will create new indexes as it needs to, but it doesn't know about old ones, + // so delete them. + if (db_index_exists('file_managed', 'file_type')) { + db_drop_index('file_managed', 'file_type'); + } + module_enable(array('file_entity')); + + // Move all field instances from Media entity to File entity. + $instances = field_read_instances(array('entity_type' => 'media'), array('include_inactive' => TRUE, 'include_deleted' => TRUE)); + foreach ($instances as $instance) { + // Skip the old self-referencing file field. It will be deleted later in + // this function. + if ($instance['field_name'] === 'file') { + continue; + } + + // @todo Convert this to use _update_7000_field_read_fields() + $fields = field_read_fields(array('id' => $instance['field_id']), array('include_inactive' => TRUE, 'include_deleted' => TRUE)); + $field = $fields[$instance['field_id']]; + + // There is no API for updating the entity_type foreign key within field + // data storage. We can do a direct db_update() for when the default SQL + // storage back-end is being used, but must skip updating fields that use a + // different storage type. + if ($field['storage']['type'] !== 'field_sql_storage' || !module_exists('field_sql_storage') || !$field['storage']['active']) { + $messages[] = t('Cannot update field %id (%field_name) because it does not use the field_sql_storage storage type.', array( + '%id' => $field['id'], + '%field_name' => $field['field_name'], + )); + continue; + } + + // Update the data tables. + $table_name = _field_sql_storage_tablename($field); + $revision_name = _field_sql_storage_revision_tablename($field); + db_update($table_name) + ->fields(array('entity_type' => 'file')) + ->condition('entity_type', 'media') + ->condition('bundle', $instance['bundle']) + ->execute(); + db_update($revision_name) + ->fields(array('entity_type' => 'file')) + ->condition('entity_type', 'media') + ->condition('bundle', $instance['bundle']) + ->execute(); + + // Once all the data has been updated, update the {field_config_instance} + // record. + db_update('field_config_instance') + ->fields(array('entity_type' => 'file')) + ->condition('id', $instance['id']) + ->execute(); + } + + // Update the field_bundle_settings configuration variable: move media bundle + // settings to file bundles, and move settings of the old self-referencing + // file field to the new file pseudo-field. + foreach ($instances as $instance) { + if ($instance['field_name'] === 'file' && !$instance['deleted']) { + $file_settings = field_bundle_settings('file', $instance['bundle']); + $media_settings = field_bundle_settings('media', $instance['bundle']); + $file_settings = array_merge($file_settings, $media_settings); + if (isset($instance['widget']['weight'])) { + $file_settings['extra_fields']['form']['file']['weight'] = $instance['widget']['weight']; + } + if (isset($instance['display'])) { + foreach ($instance['display'] as $view_mode => $display) { + if (isset($display['weight'])) { + $file_settings['extra_fields']['display']['file'][$view_mode]['weight'] = $display['weight']; + } + if (isset($display['type'])) { + $file_settings['extra_fields']['display']['file'][$view_mode]['visible'] = ($display['type'] != 'hidden'); + } + } + } + field_bundle_settings('file', $instance['bundle'], $file_settings); + } + } + // Delete old media bundle settings. + db_delete('variable') + ->condition('name', db_like('field_bundle_settings_media__') . '%', 'LIKE') + ->execute(); + + // Copy field formatter settings of old self-referencing file field to file + // pseudo-field formatter settings. + $file_displays = variable_get('file_displays', array()); + foreach ($instances as $instance) { + if ($instance['field_name'] === 'file' && !$instance['deleted']) { + if (isset($instance['display'])) { + foreach ($instance['display'] as $view_mode => $display) { + if (isset($display['type']) && $display['type'] != 'hidden') { + $file_formatter = 'file_field_' . $display['type']; + $file_displays[$instance['bundle']][$view_mode][$file_formatter]['status'] = TRUE; + if (isset($display['settings'])) { + $file_displays[$instance['bundle']][$view_mode][$file_formatter]['settings'] = $display['settings']; + } + } + } + } + } + } + variable_set('file_displays', $file_displays); + + // Delete the old self-referencing file field instances. If all instances are + // deleted, field_delete_instance() will delete the field too. + foreach ($instances as $instance) { + if ($instance['field_name'] === 'file' && !$instance['deleted']) { + field_delete_instance($instance); + } + } + + field_cache_clear(); +} + +/** + * Move file display configuration. + * + * Move file display configurations from the 'file_displays' variable to the + * {file_display} table. + */ +function media_update_7017() { + // If the {file_display} table doesn't exist, then the File Entity module's + // update functions will automatically take care of migrating the + // configurations. However, if file_entity_update_7001() has already run + // prior to media_update_7016(), run it again in order to capture those + // configurations too. + if (db_table_exists('file_display') && function_exists('file_entity_update_7001')) { + module_load_include('install', 'file_entity', 'file_entity'); + file_entity_update_7001(); + } +} + +/** + * Empty update function to trigger a menu rebuild. + */ +function media_update_7018() { +} + +/** + * Update old view mode formaters. + * + * Update old per-view-mode media field formatters to the generic media + * formatter with a setting. + */ +function media_update_7019() { + $instances = array(); + $fields = field_read_fields(array('type' => 'media'), array('include_inactive' => TRUE)); + foreach ($fields as $field) { + $instances = array_merge($instances, field_read_instances(array('field_id' => $field['id']), array('include_inactive' => TRUE))); + } + foreach ($instances as $instance) { + $update_instance = FALSE; + foreach ($instance['display'] as $view_mode => $display) { + if (in_array($display['type'], array('media_link', 'media_preview', 'media_small', 'media_large', 'media_original'))) { + $update_instance = TRUE; + $instance['display'][$view_mode]['type'] = 'media'; + $instance['display'][$view_mode]['settings'] = array('file_view_mode' => $display['type']); + } + } + if ($update_instance) { + field_update_instance($instance); + } + } +} + +/** + * Delete the wysiwyg_allowed_types variable if it is the same as default. + */ +function media_update_7020() { + if (variable_get('media__wysiwyg_allowed_types') == array('image', 'video')) { + variable_del('media__wysiwyg_allowed_types'); + } +} + +/** + * Rerun media_update_7002() due to a typo that would prevent table creation. + */ +function media_update_7021() { + media_update_7002(); +} + +/** + * Replace 'view media' perm from all users having the role with 'view file'. + */ +function media_update_7200() { + $perms = user_permission_get_modules(); + if (!isset($perms['view files'])) { + throw new DrupalUpdateException('The File Entity module needs to be upgraded before continuing.'); + } + else { + $roles = user_roles(FALSE, 'view media'); + $permissions = array( + 'view media' => FALSE, + 'view files' => TRUE, + ); + foreach ($roles as $rid => $role) { + user_role_change_permissions($rid, $permissions); + } + $roles = user_roles(FALSE, 'edit media'); + $permissions = array( + 'edit media' => FALSE, + 'edit any files' => TRUE, + ); + if (function_exists('file_entity_list_permissions')) { + unset($permissions['edit any files']); + + foreach (file_entity_permissions_get_configured_types() as $type) { + $permissions += file_entity_list_permissions($type); + } + } + foreach ($roles as $rid => $role) { + user_role_change_permissions($rid, $permissions); + } + $roles = user_roles(FALSE, 'administer media'); + $permissions = array( + 'administer media' => FALSE, + 'administer files' => TRUE, + ); + foreach ($roles as $rid => $role) { + user_role_change_permissions($rid, $permissions); + } + } +} + +/** + * Handle existing media fields. + * + * Enable the new Media Field module if this site uses "media" fields. File + * fields are now preferred for storing media. + */ +function media_update_7201() { + $fields = field_info_fields(); + foreach ($fields as $field) { + if ($field['type'] == 'media') { + // This update function may run even if Media is not enabled. Don't enable + // Media Field if its dependencies aren't already enabled. + module_enable(array('mediafield'), FALSE); + + // Update entries in file_usage so that they are associated with Media + // Field rather than Media. + // @TODO This update function may conflict with + // http://drupal.org/node/1268116 + db_update('file_usage') + ->condition('module', 'media') + ->fields(array('module' => 'mediafield')) + ->execute(); + + return t('The "Media" field type has been moved to the new "Media Field" module. This site uses media fields, so the Media Field module has been enabled.'); + } + } + return t('The "Media" field type has been moved to the new "Media Field" module. File fields can be used to store media.'); +} + +/** + * Enable the Views module if it is not already enabled. + */ +function media_update_7202() { + module_enable(array('views')); + if (!module_exists('views')) { + throw new DrupalUpdateException('The <a href="https://drupal.org/project/views">Views module</a> must be downloaded and available for Media updates to proceed.'); + } +} + +/** + * Empty update function to trigger cache clear. + */ +function media_update_7203() { + // Do nothing. +} + +/** + * Update old Media view modes to the new File Entity ones. + */ +function media_update_7204() { + $view_mode_updates = array( + 'media_preview' => 'preview', + 'media_small' => 'teaser', + 'media_large' => 'full', + ); + + // Update the media__wysiwyg_default_view_mode variable. + $wysiwyg_default_view_mode = variable_get('media__wysiwyg_default_view_mode'); + if (isset($wysiwyg_default_view_mode) && isset($view_mode_updates[$wysiwyg_default_view_mode])) { + $wysiwyg_default_view_mode = $view_mode_updates[$wysiwyg_default_view_mode]; + variable_set('media__wysiwyg_default_view_mode', $wysiwyg_default_view_mode); + } + + // Update view mode references in the 'field_bundle_settings' variable. + $field_bundle_settings = variable_get('field_bundle_settings'); + if (!empty($field_bundle_settings['file'])) { + foreach ($field_bundle_settings['file'] as $file_type => $info) { + // Per-bundle information about the view modes. + foreach ($view_mode_updates as $old_view_mode => $new_view_mode) { + if (isset($info['view_modes'][$old_view_mode])) { + $field_bundle_settings['file'][$file_type]['view_modes'][$new_view_mode] = $info['view_modes'][$old_view_mode]; + unset($field_bundle_settings['file'][$file_type]['view_modes'][$old_view_mode]); + } + // The File Entity module defaults to not use custom settings for the + // new view modes, but the Media module used to default to using custom + // settings, so if this variable is not defined, use the prior default. + if (!isset($field_bundle_settings['file'][$file_type]['view_modes'][$new_view_mode]['custom_settings'])) { + $field_bundle_settings['file'][$file_type]['view_modes'][$new_view_mode]['custom_settings'] = TRUE; + } + } + + // Settings for the "extra fields" configured on the Manage Display page. + if (!empty($info['extra_fields']['display'])) { + foreach ($info['extra_fields']['display'] as $extra_field_name => $extra_field_info) { + foreach ($view_mode_updates as $old_view_mode => $new_view_mode) { + if (isset($extra_field_info[$old_view_mode])) { + $field_bundle_settings['file'][$file_type]['extra_fields']['display'][$extra_field_name][$new_view_mode] = $extra_field_info[$old_view_mode]; + unset($field_bundle_settings['file'][$file_type]['extra_fields']['display'][$extra_field_name][$old_view_mode]); + } + } + } + } + } + } + variable_set('field_bundle_settings', $field_bundle_settings); + + // Move settings for fields attached to files from the old view modes to the + // new ones. + $instances = field_read_instances(array('entity_type' => 'file')); + foreach ($instances as $instance) { + $updated = FALSE; + foreach ($view_mode_updates as $old_view_mode => $new_view_mode) { + if (isset($instance['display'][$old_view_mode])) { + $instance['display'][$new_view_mode] = $instance['display'][$old_view_mode]; + unset($instance['display'][$old_view_mode]); + $updated = TRUE; + } + } + if ($updated) { + field_update_instance($instance); + } + } + + // Move "Manage file display" settings from old view modes to new ones. + $file_display_names = db_query('SELECT name FROM {file_display}')->fetchCol(); + foreach ($file_display_names as $old_file_display_name) { + list($file_type, $view_mode, $formatter) = explode('__', $old_file_display_name, 3); + if (isset($view_mode_updates[$view_mode])) { + $view_mode = $view_mode_updates[$view_mode]; + $new_file_display_name = implode('__', array($file_type, $view_mode, $formatter)); + db_delete('file_display')->condition('name', $new_file_display_name)->execute(); + db_update('file_display')->fields(array('name' => $new_file_display_name))->condition('name', $old_file_display_name)->execute(); + } + } + + // Update file/image/media fields that use a formatter that reference an old + // file view modes to reference the new ones. + foreach (field_read_instances() as $instance) { + if (!empty($instance['display'])) { + $updated = FALSE; + foreach ($instance['display'] as $instance_view_mode => $display) { + if (isset($display['settings']['file_view_mode']) && isset($view_mode_updates[$display['settings']['file_view_mode']])) { + $instance['display'][$instance_view_mode]['settings']['file_view_mode'] = $view_mode_updates[$display['settings']['file_view_mode']]; + $updated = TRUE; + } + } + if ($updated) { + field_update_instance($instance); + } + } + } + + // Update formatter settings that reference the old view modes within saved + // Views. + if (db_table_exists('views_display')) { + $result = db_select('views_display', 'v')->fields('v', array('vid', 'id', 'display_options'))->execute(); + foreach ($result as $record) { + if (!empty($record->display_options)) { + $display_options = unserialize($record->display_options); + if (_media_update_7204_update_views_display_options($display_options, $view_mode_updates)) { + db_update('views_display') + ->fields(array('display_options' => serialize($display_options))) + ->condition('vid', $record->vid) + ->condition('id', $record->id) + ->execute(); + } + } + } + } + + // Update formatter settings that reference the old view modes within unsaved + // Views in the CTools object cache. Objects in the CTools cache are instances + // of classes, so the Views module must be enabled to unserialize it + // correctly. + if (db_table_exists('ctools_object_cache') && module_exists('views')) { + $result = db_select('ctools_object_cache', 'c')->fields('c', array('sid', 'name', 'obj', 'data'))->condition('obj', 'view')->execute(); + foreach ($result as $record) { + $view = unserialize($record->data); + if (!empty($view->display)) { + $updated = FALSE; + foreach ($view->display as $display_name => $display) { + if (!empty($display->display_options) && _media_update_7204_update_views_display_options($display->display_options, $view_mode_updates)) { + $updated = TRUE; + } + } + if ($updated) { + db_update('ctools_object_cache') + ->fields(array('data' => serialize($view))) + ->condition('sid', $record->sid) + ->condition('name', $record->name) + ->condition('obj', $record->obj) + ->execute(); + } + } + } + } + + // Clear caches that might contain stale Views displays. + if (module_exists('views')) { + cache_clear_all('*', 'cache_views', TRUE); + cache_clear_all('*', 'cache_views_data', TRUE); + } + if (module_exists('block')) { + cache_clear_all('*', 'cache_block', TRUE); + } + cache_clear_all('*', 'cache_page', TRUE); + + // We still have the old media_link and media_original view modes that must be + // supported for now. + // @TODO: Make this apply only to updates from Media 1.x. + // @see media_entity_info_alter() + variable_set('media__show_deprecated_view_modes', TRUE); +} + +/** + * Drop the unused {media_list_type} table. + */ +function media_update_7205() { + if (db_table_exists('media_list_type')) { + db_drop_table('media_list_type'); + return t('Dropped the unused {media_list_type} table.'); + } +} + +/** + * Move default file display configurations to the database. + */ +function media_update_7206() { + module_load_include('inc', 'file_entity', 'file_entity.file_api'); + module_load_include('inc', 'ctools', 'includes/export'); + $default_image_styles = array( + 'preview' => 'square_thumbnail', + 'teaser' => 'medium', + 'full' => 'large', + ); + + // Only needed by sites that updated from Media 1.x. + // @see media_entity_info_alter() + if (variable_get('media__show_deprecated_view_modes')) { + $default_image_styles['media_original'] = ''; + } + + // Clear out the ctools cache so that the old default implementations + // are removed. + ctools_export_load_object_reset('file_display'); + foreach ($default_image_styles as $view_mode => $image_style) { + $existing_displays = file_displays_load('image', $view_mode, TRUE); + // Only insert default config into the database if no existing + // configuration is found. + if (!isset($existing_displays['file_image'])) { + $display_name = 'image__' . $view_mode . '__file_image'; + $display = array( + 'api_version' => 1, + 'name' => $display_name, + 'status' => 1, + 'weight' => 5, + 'settings' => array('image_style' => $image_style), + 'export_type' => NULL, + ); + file_display_save((object) $display); + } + } +} + +/** + * Trigger cache clear. + * + * Empty update function to trigger cache clear after changing access callbacks + * to file_entity_access. + */ +function media_update_7207() { + // Do nothing. +} + +/** + * Drop the media_types table and migrate files to file_entity types. + */ +function media_update_7208() { + // Reset static cache to ensure our new file types are recognized + drupal_static_reset('ctools_export_load_object_table_exists'); + + if (!db_table_exists('media_type')) { + // No types to migrate. + return; + } + // @see http://drupal.org/node/1292382 + if (!function_exists('file_type_get_enabled_types')) { + throw new DrupalUpdateException('The File Entity module needs to be upgraded before continuing.'); + } + else { + $existing_types = db_select('media_type', 'mt') + ->orderBy('weight') + ->fields('mt') + ->execute() + // Will key by the name field. + ->fetchAllAssoc('name'); + foreach ($existing_types as &$type) { + $type->type_callback_args = unserialize($type->type_callback_args); + } + + include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc'; + $mapping = file_mimetype_mapping(); + // We do not migrate this type, since there is no way to handle its weight. + unset($existing_types['default']); + foreach ($existing_types as $type) { + $extensions = isset($type->type_callback_args['extensions']) ? $type->type_callback_args['extensions'] : array(); + $mimetypes = isset($type->type_callback_args['mimetypes']) ? $type->type_callback_args['mimetypes'] : array(); + // Add mimetypes by extensions. + foreach ($extensions as $extension) { + if (isset($mapping['extensions'][$extension])) { + $type->mimetypes[] = $mapping['mimetypes'][$mapping['extensions'][$extension]]; + } + } + // Add rest mimetypes. + foreach ($mimetypes as $mimetype) { + // Mimetype is a regex pattern. + foreach ($mapping['mimetypes'] as $mapping_mimetype) { + if (preg_match($mimetype, $mapping_mimetype) && !in_array($mapping_mimetype, $type->mimetypes)) { + $type->mimetypes[] = $mapping_mimetype; + } + } + } + $type->streams = isset($type->type_callback_args['streams']) ? $type->type_callback_args['streams'] : array(); + $type->type = $type->name; + // Merge existing type with new ones. + if ($new_type = file_type_load($type->name)) { + $new_type->mimetypes = array_merge($type->mimetypes, $new_type->mimetypes); + $new_type->streams = array_merge($type->streams, $new_type->streams); + } + else { + $new_type = $type; + } + file_type_save($new_type); + } + db_drop_table('media_type'); + + // Special treatment for old media application type to new file_entity + // document one. Add some more mimetypes to document. + $document_type = file_type_load('document'); + if (!$document_type) { + return; + } + foreach ($mapping['mimetypes'] as $mimetype) { + $is_document = strpos($mimetype, 'document') !== FALSE || strpos($mimetype, 'application/vnd.ms-') !== FALSE; + if ($is_document && !in_array($mimetype, $document_type->mimetypes)) { + $document_type->mimetypes[] = $mimetype; + } + } + file_type_save($document_type); + } +} + +/** + * Enable the hidden media_migrate_file_types module to provide a UI to update + * {file_managed}.type with the new file types provided by file_entity. + */ +function media_update_7209() { + drupal_load('module', 'media_migrate_file_types'); + + if (_media_migrate_file_types_get_migratable_file_types()) { + module_enable(array('media_migrate_file_types')); + } +} + +/** + * Delete deceprated media__type_icon_directory variable. + */ +function media_update_7210() { + variable_del('media__type_icon_directory'); +} + +/** + * Save a square_thumbnail image style in the database for legacy support if one + * does not already exist. + */ +function media_update_7211() { + $default_style = array( + 'name' => 'square_thumbnail' + ); + + // Clear the image cache to remove any old image styles that only exist in + // code. + cache_clear_all('*', 'cache_image', TRUE); + + // Check if the square_thumbnail image style exists. + // The style will only exist if the user has customized it, otherwise it would + // have been removed by clearing the image style cache. + $existing_style = image_style_load('square_thumbnail'); + + // Save a square_thumbnail image style in the database for legacy support. + // This is only necessary if a square_thumbnail image style doesn't already + // exist. + if (empty($existing_style)) { + $style = image_style_save($default_style); + + $effect = array( + 'name' => 'image_scale_and_crop', + 'data' => array( + 'width' => 180, + 'height' => 180, + 'weight' => 0, + ), + 'isid' => $style['isid'], + ); + + image_effect_save($effect); + } + + return t('Saved a square_thumbnail image style in the database for legacy support if one did not already exist.'); +} + +/** + * Utility function for update 7204. Updates display options within Views. + */ +function _media_update_7204_update_views_display_options(&$display_options, $view_mode_updates) { + $updated = FALSE; + + // Update fields that use a formatter with a file_view_mode formatter setting. + if (!empty($display_options['fields'])) { + foreach ($display_options['fields'] as $field_name => $field_display) { + if (isset($field_display['settings']['file_view_mode']) && isset($view_mode_updates[$field_display['settings']['file_view_mode']])) { + $display_options['fields'][$field_name]['settings']['file_view_mode'] = $view_mode_updates[$field_display['settings']['file_view_mode']]; + $updated = TRUE; + } + } + } + + // Update Views that display files directly using a row plugin with a view + // mode setting. + if (isset($display_options['row_plugin']) && $display_options['row_plugin'] === 'file' && isset($display_options['row_options']['view_mode']) && isset($view_mode_updates[$display_options['row_options']['view_mode']])) { + $display_options['row_options']['view_mode'] = $view_mode_updates[$display_options['row_options']['view_mode']]; + $updated = TRUE; + } + return $updated; +} + +/** + * Re-create application file type for legacy reasons. + */ +function media_update_7212() { + module_load_include('inc', 'file_entity', 'file_entity.file_api'); + if (!file_type_load('application')) { + $application = (object) array( + 'api_version' => 1, + 'type' => 'application', + 'label' => t('Application'), + 'description' => t('Multipurpose type - kept to support older sites.'), + 'mimetypes' => array(), + 'streams' => array( + 'public', + ), + ); + + file_type_save($application); + $application = file_type_load('application'); + file_type_disable($application); + } +} + +/** + * Remove the obsolete file_extensions variable. + */ +function media_update_7213() { + $media_file_extensions = explode(' ', variable_get('media__file_extensions')); + $file_entity_file_extensions = explode(' ', variable_get('file_entity_default_allowed_extensions', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp mp3 mov mp4 m4a m4v mpeg avi ogg oga ogv weba webp webm')); + + // Preserve any custom file extensions. + if (array_diff($media_file_extensions, $file_entity_file_extensions)) { + $combined_file_extensions = array_unique(array_merge($file_entity_file_extensions, $media_file_extensions)); + variable_set('file_entity_default_allowed_extensions', implode(' ' , $combined_file_extensions)); + } + + variable_del('media__file_extensions'); +} + +/** + * Drop the legacy {media_filter_usage} table. + */ +function media_update_7214() { + if (db_table_exists('media_filter_usage')) { + db_drop_table('media_filter_usage'); + } +} + +/** + * Skipped to run media_update_7217(). + */ +function media_update_7216() { + // Do nothing. +} + +/** + * Copy file type icons to public files directory. + */ +function media_update_7217() { + // Remove any trailing slashes from the icon base directory variable. + $dir = variable_get('media__icon_base_directory'); + if (!empty($dir)) { + $dir = rtrim($dir, '/'); + variable_set('media__icon_base_directory', $dir); + } + + try { + _media_install_copy_icons(); + } + catch (Exception $e) { + throw new DrupalUpdateException($e->getMessage()); + } +} + +/** + * Drop the legacy {cache_media_xml} table. + */ +function media_update_7218() { + if (db_table_exists('cache_media_xml')) { + db_drop_table('cache_media_xml'); + } + + variable_del('media__xml_cache_expire'); +} + +/** + * Enable the Media WYSIWYG submodule. + */ +function media_update_7219() { + if (module_exists('wysiwyg')) { + module_enable(array('media_wysiwyg')); + } +} + +/** + * Delete the deprecated media__file_list_size variable. + */ +function media_update_7220() { + variable_del('media__file_list_size'); +} + +/** + * Enable the Media Bulk Upload submodule. + */ +function media_update_7221() { + if (module_exists('multiform') && module_exists('plupload')) { + module_enable(array('media_bulk_upload')); + } +} + +/** + * Delete the deprecated media__display_types_migration_mess variable. + */ +function media_update_7222() { + variable_del('media__display_types_migration_mess'); +} + +/** + * Delete legacy variables. + */ +function media_update_7223() { + variable_del('media__max_filesize'); + variable_del('media__debug'); + variable_del('media__xml_cache_expire'); + variable_del('media__show_file_type_rebuild_nag'); + variable_del('media__field_select_media_text'); + variable_del('media__field_remove_media_text'); + variable_del('media__browser_library_empty_message'); + variable_del('media__browser_pager_limit'); + variable_del('media__browser_viewtype_default'); +} + +/** + * Rename variables, removing variable namespace. + */ +function media_update_7224() { + // Create an array of variables sans 'media' prefix. + $variables = array('wysiwyg_title', 'wysiwyg_icon_title', 'wysiwyg_default_view_mode', 'wysiwyg_upload_directory', 'wysiwyg_allowed_types', 'wysiwyg_allowed_attributes', 'wysiwyg_browser_plugins', 'dialog_theme', 'import_batch_size', 'fromurl_supported_schemes', 'icon_base_directory', 'icon_set', 'show_deprecated_view_modes'); + + foreach ($variables as $variable) { + // Find the value of the old variable. + $value = variable_get('media__' . $variable); + + // Port the value of the variable if it was set. + if (!is_null($value)) { + variable_set('media_' . $variable, $value); + } + + // Remove the old variable. + variable_del('media__' . $variable); + } +} + +/** + * Migrate variables to appropriate submodules. + */ +function media_update_7225() { + $data = array( + 'media_wysiwyg' => array( + 'wysiwyg_title', + 'wysiwyg_icon_title', + 'wysiwyg_default_view_mode', + 'wysiwyg_upload_directory', + 'wysiwyg_allowed_types', + 'wysiwyg_allowed_attributes', + 'wysiwyg_browser_plugins', + ), + 'media_internet' => array( + 'fromurl_supported_schemes', + ), + 'media_bulk_upload' => array( + 'import_batch_size', + ), + ); + + foreach ($data as $module => $variables) { + foreach ($variables as $variable) { + // Only port variables to submodules if the submodule exists. + if (module_exists($module)) { + // Find the value of the old variable. + $value = variable_get('media_' . $variable); + + // Port the value of the variable if it was set. + if (!is_null($value)) { + variable_set($module . '_' . $variable, $value); + } + } + + // Remove the old variable. + variable_del('media_' . $variable); + } + } +} + +/** + * Grant existing user access to new media browser permission. + */ +function media_update_7226() { + $roles = user_roles(FALSE, 'create files'); + + foreach ($roles as $rid => $role) { + user_role_grant_permissions($rid, array('access media browser')); + } +} diff --git a/sites/all/modules/media/media.media.inc b/sites/all/modules/media/media.media.inc new file mode 100644 index 000000000..2700265f8 --- /dev/null +++ b/sites/all/modules/media/media.media.inc @@ -0,0 +1,110 @@ +<?php + +/** + * @file + * Media module integration for the Media module. + */ + +/** + * Implements hook_media_browser_plugin_info(). + */ +function media_media_browser_plugin_info() { + $info['upload'] = array( + 'title' => t('Upload'), + 'weight' => -10, + 'class' => 'MediaBrowserUpload', + ); + + // Add a plugin for each View display using the 'media_browser' display type. + $view_weight = 10; + foreach (views_get_enabled_views() as $view) { + foreach ($view->display as $display) { + if ($display->display_plugin == 'media_browser') { + $title = $display->display_title; + if (!empty($display->display_options['title'])) { + $title = $display->display_options['title']; + } + $info["{$view->name}--{$display->id}"] = array( + 'title' => $title, + // @TODO make this configurable. + 'weight' => $view_weight++, + 'class' => 'MediaBrowserView', + 'view_name' => $view->name, + 'view_display_id' => $display->id, + ); + } + } + } + + return $info; +} + +/** + * Implements hook_query_TAG_alter(). + * + * @todo: Potentially move this into media.module in a future version of Media. + */ +function media_query_media_browser_alter($query) { + // Ensure that the query is against the file_managed table. + $tables = $query->getTables(); + + if (empty($tables['file_managed'])) { + throw new Exception(t('Media browser being queried without the file_managed table.')); + } + + $alias = $tables['file_managed']['alias']; + + // How do we validate these? I don't know. + // I think PDO should protect them, but I'm not 100% certain. + $params = media_get_browser_params(); + + // Gather any file restrictions. + $types = !empty($params['types']) ? $params['types'] : array(); + $schemes = !empty($params['schemes']) ? $params['schemes'] : array(); + $extensions = !empty($params['file_extensions']) ? explode(' ', $params['file_extensions']) : array(); + + $or = db_or(); + + // Filter out files with restricted types. + if (!empty($types)) { + $query->condition($alias . '.type', $types, 'IN'); + } + + // Filter out files with restricted schemes. + if (!empty($schemes)) { + $local_or = db_or(); + $local_and = db_and(); + + // Gather all of the local stream wrappers. + $local_stream_wrappers = media_get_local_stream_wrappers(); + + foreach ($schemes as $scheme) { + // Only local files have extensions. + // Filter out files with restricted extensions. + if (!empty($extensions) && isset($local_stream_wrappers[$scheme])) { + $mimetypes = array(); + foreach ($extensions as $extension) { + $mimetype = media_get_extension_mimetype($extension); + if ($mimetype) { + $mimetypes[] = $mimetype; + } + } + $local_and->condition($alias . '.uri', db_like($scheme . '://') . '%', 'LIKE'); + if (count($mimetypes)) { + $local_and->condition($alias . '.filemime', $mimetypes, 'IN'); + } + $local_or->condition($local_and); + $local_and = db_and(); + } + else { + $local_or->condition($alias . '.uri', db_like($scheme . '://') . '%', 'LIKE'); + } + } + + $or->condition($local_or); + } + + if ($or->count()) { + $query->condition($or); + } +} diff --git a/sites/all/modules/media/media.module b/sites/all/modules/media/media.module new file mode 100644 index 000000000..1f97bcb7a --- /dev/null +++ b/sites/all/modules/media/media.module @@ -0,0 +1,1402 @@ +<?php + +/** + * @file + * Media API + * + * The core Media API. + * See http://drupal.org/project/media for more details. + */ + +// Code relating to using media as a field. +require_once dirname(__FILE__) . '/includes/media.fields.inc'; + +/** + * Implements hook_hook_info(). + */ +function media_hook_info() { + $hooks = array( + 'media_parse', + 'media_browser_plugin_info', + 'media_browser_plugin_info_alter', + 'media_browser_plugins_alter', + 'media_browser_params_alter', + 'query_media_browser_alter', + ); + + return array_fill_keys($hooks, array('group' => 'media')); +} + +/** + * Implements hook_help(). + */ +function media_help($path, $arg) { + switch ($path) { + case 'admin/help#media': + $output = ''; + $output .= '<h3>' . t('About') . '</h3>'; + $output .= '<p>' . t('The Media module is a File Browser to the Internet, media provides a framework for managing files and multimedia assets, regardless of whether they are hosted on your own site or a 3rd party site. It replaces the Drupal core upload field with a unified User Interface where editors and administrators can upload, manage, and reuse files and multimedia assets. Media module also provides rich integration with WYSIWYG module to let content creators access media assets in rich text editor. Javascript is required to use the Media module. For more information check <a href="@media_faq">Media Module page</a>', array('@media_faq' => 'http://drupal.org/project/media')) . '.</p>'; + $output .= '<h3>' . t('Uses') . '</h3>'; + $output .= '<dl>'; + $output .= '<dt>' . t('Media Repository') . '</dt>'; + $output .= '<dd>' . t('Media module allows you to maintain a <a href="@mediarepo">media asset repository</a> where in you can add, remove, reuse your media assets. You can add the media file using upload form or from a url and also do bulk operations on the media assets.', array('@mediarepo' => url('admin/content/media'))) . '</dd>'; + $output .= '<dt>' . t('Attaching media assets to content types') . '</dt>'; + $output .= '<dd>' . t('Media assets can be attached to content types as fields. To add a media field to a <a href="@content-type">content type</a>, go to the content type\'s <em>manage fields</em> page, and add a new field of type <em>Multimedia Asset</em>.', array('@content-type' => url('admin/structure/types'))) . '</dd>'; + $output .= '<dt>' . t('Using media assets in WYSIWYG') . '</dt>'; + $output .= '<dd>' . t('Media module provides rich integration with WYSIWYG editors, using Media Browser plugin you can select media asset from library to add to the rich text editor moreover you can add media asset from the media browser itself using either upload method or add from url method. To configure media with WYSIWYG you need two steps of configuration:'); + $output .= '<ul><li>' . t('Enable WYSIWYG plugin on your desired <a href="@wysiwyg-profile">WYSIWYG profile</a>. Please note that you will need to have <a href="@wysiwyg">WYSIWYG</a> module enabled.', array('@wysiwyg-profile' => url('admin/config/content/wysiwyg'), '@wysiwyg' => 'http://drupal.org/project/wysiwyg')) . '</li>'; + $output .= '<li>' . t('Enable the <em>Convert Media tags to markup</em> filter on the <a href="@input-format">Input format</a> you are using with the WYSIWYG profile.', array('@input-format' => url('admin/config/content/formats'))) . '</li></ul></dd>'; + return $output; + } +} + +/** + * Implements hook_entity_info_alter(). + */ +function media_entity_info_alter(&$entity_info) { + // For sites that updated from Media 1.x, continue to provide these deprecated + // view modes. + // @see http://drupal.org/node/1051090 + if (variable_get('media_show_deprecated_view_modes', FALSE)) { + $entity_info['file']['view modes']['media_link'] = array( + 'label' => t('Link'), + 'custom settings' => TRUE, + ); + $entity_info['file']['view modes']['media_original'] = array( + 'label' => t('Original'), + 'custom settings' => TRUE, + ); + } + + if (module_exists('entity_translation')) { + $entity_info['file']['translation']['entity_translation']['class'] = 'MediaEntityTranslationHandler'; + } +} + +/** + * Implements hook_menu(). + */ +function media_menu() { + // For managing different types of media and the fields associated with them. + $items['admin/config/media/browser'] = array( + 'title' => 'Media browser settings', + 'description' => 'Configure the behavior and display of the media browser.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_admin_config_browser'), + 'access arguments' => array('administer media browser'), + 'file' => 'includes/media.admin.inc', + ); + + // Administrative screens for managing media. + $items['admin/content/file/thumbnails'] = array( + 'title' => 'Thumbnails', + 'description' => 'Manage files used on your site.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('file_entity_admin_file'), + 'access arguments' => array('administer files'), + 'type' => MENU_LOCAL_TASK, + 'file' => 'file_entity.admin.inc', + 'file path' => drupal_get_path('module', 'file_entity'), + 'weight' => 10, + ); + + $items['media/ajax'] = array( + 'page callback' => 'media_ajax_upload', + 'delivery callback' => 'ajax_deliver', + 'access arguments' => array('access content'), + 'theme callback' => 'ajax_base_page_theme', + 'type' => MENU_CALLBACK, + ); + + $items['media/browser'] = array( + 'title' => 'Media browser', + 'description' => 'Media Browser for picking media and uploading new media', + 'page callback' => 'media_browser', + 'access arguments' => array('access media browser'), + 'type' => MENU_CALLBACK, + 'file' => 'includes/media.browser.inc', + 'theme callback' => 'media_dialog_get_theme_name', + ); + + // A testbed to try out the media browser with different launch commands. + $items['media/browser/testbed'] = array( + 'title' => 'Media Browser test', + 'description' => 'Make it easier to test media browser', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_browser_testbed'), + 'access arguments' => array('administer files'), + 'type' => MENU_CALLBACK, + 'file' => 'includes/media.browser.inc', + ); + + // We could re-use the file/%file/edit path for the modal callback, but + // it is just easier to use our own namespace here. + $items['media/%file/edit/%ctools_js'] = array( + 'title' => 'Edit', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_file_edit_modal', 1, 3), + 'access callback' => 'file_entity_access', + 'access arguments' => array('update', 1), + 'theme callback' => 'ajax_base_page_theme', + 'file' => 'includes/media.pages.inc', + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implements hook_menu_local_tasks_alter(). + */ +function media_menu_local_tasks_alter(&$data, $router_item, $root_path) { + // Add action link to 'file/add' on 'admin/content/file/thumbnails' page. + if ($root_path == 'admin/content/file/thumbnails') { + $item = menu_get_item('file/add'); + if (!empty($item['access'])) { + $data['actions']['output'][] = array( + '#theme' => 'menu_local_action', + '#link' => $item, + '#weight' => $item['weight'], + ); + } + } +} + +/** + * Implements hook_admin_paths(). + */ +function media_admin_paths() { + $paths['media/*/edit/*'] = TRUE; + $paths['media/*/format-form'] = TRUE; + + // If the media browser theme is set to the admin theme, ensure it gets set + // as an admin path as well. + $dialog_theme = variable_get('media_dialog_theme', ''); + if (empty($dialog_theme) || $dialog_theme == variable_get('admin_theme')) { + $paths['media/browser'] = TRUE; + $paths['media/browser/*'] = TRUE; + } + + return $paths; +} + +/** + * Implements hook_permission(). + */ +function media_permission() { + return array( + 'administer media browser' => array( + 'title' => t('Administer media browser'), + 'description' => t('Access media browser settings.'), + ), + 'access media browser' => array( + 'title' => t('Use the media browser'), + ), + ); +} + +/** + * Implements hook_theme(). + */ +function media_theme() { + return array( + // media.module. + 'media_element' => array( + 'render element' => 'element', + ), + + // media.field.inc. + 'media_widget' => array( + 'render element' => 'element', + ), + 'media_widget_multiple' => array( + 'render element' => 'element', + ), + 'media_upload_help' => array( + 'variables' => array('description' => NULL), + ), + + // media.theme.inc. + 'media_thumbnail' => array( + 'render element' => 'element', + 'file' => 'includes/media.theme.inc', + ), + 'media_formatter_large_icon' => array( + 'variables' => array('file' => NULL, 'attributes' => array(), 'style_name' => 'media_thumbnail'), + 'file' => 'includes/media.theme.inc', + ), + 'media_dialog_page' => array( + 'render element' => 'page', + 'template' => 'templates/media-dialog-page', + 'file' => 'includes/media.theme.inc', + ), + ); +} + +/** + * Menu callback; Shared Ajax callback for media attachment 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 media_ajax_upload() { + $form_parents = func_get_args(); + $form_build_id = (string) array_pop($form_parents); + + if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) { + // Invalid request. + drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error'); + $commands = array(); + $commands[] = ajax_command_replace(NULL, theme('status_messages')); + return array('#type' => 'ajax', '#commands' => $commands); + } + + list($form, $form_state, $form_id, $form_build_id, $commands) = ajax_get_form(); + + if (!$form) { + // Invalid form_build_id. + drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error'); + $commands = array(); + $commands[] = ajax_command_replace(NULL, theme('status_messages')); + return array('#type' => 'ajax', '#commands' => $commands); + } + + // Get the current element and count the number of files. + $current_element = $form; + foreach ($form_parents as $parent) { + $current_element = $current_element[$parent]; + } + $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0; + + // Process user input. $form and $form_state are modified in the process. + drupal_process_form($form['#form_id'], $form, $form_state); + + // Retrieve the element to be rendered. + foreach ($form_parents as $parent) { + $form = $form[$parent]; + } + + // Add the special Ajax class if a new file was added. + if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) { + $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content'; + } + // Otherwise just add the new content class on a placeholder. + else { + $form['#suffix'] .= '<span class="ajax-new-content"></span>'; + } + + $output = theme('status_messages') . drupal_render($form); + $js = drupal_add_js(); + $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']); + + $commands[] = ajax_command_replace(NULL, $output, $settings); + return array('#type' => 'ajax', '#commands' => $commands); +} + +/** + * Implements hook_image_default_styles(). + */ +function media_image_default_styles() { + $styles = array(); + $styles['media_thumbnail'] = array( + 'label' => 'Media thumbnail (100x100)', + 'effects' => array( + array( + 'name' => 'image_scale_and_crop', + 'data' => array('width' => 100, 'height' => 100), + 'weight' => 0, + ), + ), + ); + return $styles; +} + +/** + * Implements hook_page_alter(). + * + * This is used to use our alternate template when ?render=media-popup is passed + * in the URL. + */ +function media_page_alter(&$page) { + if (isset($_GET['render']) && $_GET['render'] == 'media-popup') { + $page['#theme'] = 'media_dialog_page'; + + // Disable administration modules from adding output to the popup. + // @see http://drupal.org/node/914786 + module_invoke_all('suppress', TRUE); + + foreach (element_children($page) as $key) { + if ($key != 'content') { + unset($page[$key]); + } + } + } +} + +/** + * Implements hook_form_FIELD_UI_FIELD_EDIT_FORM_alter(). + * + * @todo: Respect field settings in 7.x-2.x and handle them in the media widget + * UI. + */ +function media_form_field_ui_field_edit_form_alter(&$form, &$form_state) { + // On file fields that use the media widget we need remove specific fields. + if ($form['#field']['type'] == 'file' && $form['instance']['widget']['type']['#value'] == 'media_generic') { + $form['instance']['settings']['file_extensions']['#title'] = t('Allowed file extensions for uploaded files'); + $form['instance']['settings']['file_extensions']['#maxlength'] = 255; + } + + // On image fields using the media widget we remove the alt/title fields. + if ($form['#field']['type'] == 'image' && $form['instance']['widget']['type']['#value'] == 'media_generic') { + $form['instance']['settings']['alt_field']['#access'] = FALSE; + $form['instance']['settings']['title_field']['#access'] = FALSE; + $form['instance']['settings']['file_extensions']['#title'] = t('Allowed file extensions for uploaded files'); + // Do not increase maxlength of file extensions for image fields, since + // presumably they will not need a long list of extensions. + } + + // Add a validation function to any field instance which uses the media widget + // to ensure that the upload destination scheme is one of the allowed schemes + // if any defined by settings. + if ($form['instance']['widget']['type']['#value'] == 'media_generic' && isset($form['#field']['settings']['uri_scheme'])) { + $form['#validate'][] = 'media_field_instance_validate'; + } + + if ($form['#instance']['entity_type'] == 'file') { + $form['instance']['settings']['wysiwyg_override'] = array( + '#type' => 'checkbox', + '#title' => t('Override in WYSIWYG'), + '#description' => t('If checked, then this field may be overridden in the WYSIWYG editor.'), + '#default_value' => isset($form['#instance']['settings']['wysiwyg_override']) ? $form['#instance']['settings']['wysiwyg_override'] : TRUE, + ); + } +} + +/** + * Validation handler; ensure that the upload destination scheme is one of the + * allowed schemes. + */ +function media_field_instance_validate($form, &$form_state) { + $allowed_schemes = array_filter($form_state['values']['instance']['widget']['settings']['allowed_schemes']); + $upload_destination = $form_state['values']['field']['settings']['uri_scheme']; + + if (!empty($allowed_schemes) && !in_array($upload_destination, $allowed_schemes)) { + form_set_error('allowed_schemes', t('The upload destination must be one of the allowed schemes.')); + } +} + +/** + * Implements hook_form_alter(). + */ +function media_form_alter(&$form, &$form_state, $form_id) { + // If we're in the media browser, set the #media_browser key to true + // so that if an ajax request gets sent to a different path, the form + // still uses the media_browser_form_submit callback. + if (current_path() == 'media/browser') { + if ($form_id == 'views_exposed_form') { + $form['render'] = array('#type' => 'hidden', '#value' => 'media-popup'); + $form['#action'] = '/media/browser'; + } else { + $form_state['#media_browser'] = TRUE; + } + } + + // If the #media_browser key isset and is true we are using the browser + // popup, so add the media_browser submit handler. + if (!empty($form_state['#media_browser'])) { + $form['#submit'][] = 'media_browser_form_submit'; + } +} + +/** + * Submit handler; direction form submissions in the media browser. + */ +function media_browser_form_submit($form, &$form_state) { + $url = NULL; + $parameters = array(); + + // Single upload. + if (!empty($form_state['file'])) { + $file = $form_state['file']; + $url = 'media/browser'; + $parameters = array('query' => array('render' => 'media-popup', 'fid' => $file->fid)); + } + + // If $url is set, we had some sort of upload, so redirect the form. + if (!empty($url)) { + $form_state['redirect'] = array($url, $parameters); + } +} + +/** + * Implements hook_library(). + */ +function media_library() { + $path = drupal_get_path('module', 'media'); + $info = system_get_info('module', 'media'); + + $common = array( + 'website' => 'http://drupal.org/project/media', + 'version' => !empty($info['version']) ? $info['version'] : '7.x-2.x', + ); + + // Contains libraries common to other media modules. + $libraries['media_base'] = array( + 'title' => 'Media base', + 'js' => array( + $path . '/js/media.core.js' => array('group' => JS_LIBRARY, 'weight' => -5), + $path . '/js/util/json2.js' => array('group' => JS_LIBRARY), + $path . '/js/util/ba-debug.min.js' => array('group' => JS_LIBRARY), + ), + 'css' => array( + $path . '/css/media.css', + ), + ); + + // Includes resources needed to launch the media browser. Should be included + // on pages where the media browser needs to be launched from. + $libraries['media_browser'] = array( + 'title' => 'Media Browser popup libraries', + 'js' => array( + $path . '/js/media.popups.js' => array('group' => JS_DEFAULT), + ), + 'dependencies' => array( + array('system', 'ui.resizable'), + array('system', 'ui.draggable'), + array('system', 'ui.dialog'), + array('media', 'media_base'), + ), + ); + + // Resources needed in the media browser itself. + $libraries['media_browser_page'] = array( + 'title' => 'Media browser', + 'js' => array( + $path . '/js/media.browser.js' => array('group' => JS_DEFAULT), + ), + 'dependencies' => array( + array('system', 'ui.tabs'), + array('system', 'ui.draggable'), + array('system', 'ui.dialog'), + array('media', 'media_base'), + ), + ); + + // Settings for the dialog etc. + $settings = array( + 'browserUrl' => url('media/browser', array( + 'query' => array( + 'render' => 'media-popup' + )) + ), + 'styleSelectorUrl' => url('media/-media_id-/format-form', array( + 'query' => array( + 'render' => 'media-popup' + )) + ), + 'dialogOptions' => array( + 'dialogclass' => variable_get('media_dialogclass', 'media-wrapper'), + 'modal' => (boolean)variable_get('media_modal', TRUE), + 'draggable' => (boolean)variable_get('media_draggable', FALSE), + 'resizable' => (boolean)variable_get('media_resizable', FALSE), + 'minwidth' => (int)variable_get('media_minwidth', 500), + 'width' => (int)variable_get('media_width', 670), + 'height' => (int)variable_get('media_height', 280), + 'position' => variable_get('media_position', 'center'), + 'overlay' => array( + 'backgroundcolor' => variable_get('media_backgroundcolor', '#000000'), + 'opacity' => (float)variable_get('media_opacity', 0.4), + ), + 'zindex' => (int)variable_get('media_zindex', 10000), + ), + ); + + $libraries['media_browser_settings'] = array( + 'title' => 'Media browser settings', + 'js' => array( + 0 => array( + 'data' => array( + 'media' => $settings, + ), + 'type' => 'setting', + ), + ), + ); + + foreach ($libraries as &$library) { + $library += $common; + } + return $libraries; +} + +/** + * Theme callback used to identify when we are in a popup dialog. + * + * Generally the default theme will look terrible in the media browser. This + * will default to the administration theme, unless set otherwise. + */ +function media_dialog_get_theme_name() { + return variable_get('media_dialog_theme', variable_get('admin_theme')); +} + +/** + * This will parse a url or embedded code into a unique URI. + * + * The function will call all modules implementing hook_media_parse($url), + * which should return either a string containing a parsed URI or NULL. + * + * @NOTE The implementing modules may throw an error, which will not be caught + * here; it's up to the calling function to catch any thrown errors. + * + * @NOTE In emfield, we originally also accepted an array of regex patterns + * to match against. However, that module used a registration for providers, + * and simply stored the match in the database keyed to the provider object. + * However, other than the stream wrappers, there is currently no formal + * registration for media handling. Additionally, few, if any, stream wrappers + * will choose to store a straight match from the parsed URL directly into + * the URI. Thus, we leave both the matching and the final URI result to the + * implementing module in this implementation. + * + * An alternative might be to do the regex pattern matching here, and pass a + * successful match back to the implementing module. However, that would + * require either an overloaded function or a new hook, which seems like more + * overhead than it's worth at this point. + * + * @TODO Once hook_module_implements_alter() is in core (see the issue at + * http://drupal.org/node/692950) we may want to implement media_media_parse() + * to ensure we were passed a valid URL, rather than an unsupported or + * malformed embed code that wasn't caught earlier. It will needed to be + * weighted so it's called after all other streams have a go, as the fallback, + * and will need to throw an error. + * + * @param string $url + * The original URL or embed code to parse. + * + * @return string + * The unique URI for the file, based on its stream wrapper, or NULL. + * + * @see media_parse_to_file() + * @see media_add_from_url_validate() + */ +function media_parse_to_uri($url) { + // Trim any whitespace before parsing. + $url = trim($url); + foreach (module_implements('media_parse') as $module) { + $success = module_invoke($module, 'media_parse', $url); + $context = array( + 'url' => $url, + 'module' => $module, + ); + drupal_alter('media_parse', $success, $context); + if (isset($success)) { + return $success; + } + } +} + +/** + * Parse a URL or embed code and return a file object. + * + * If a remote stream doesn't claim the parsed URL in media_parse_to_uri(), + * then we'll copy the file locally. + * + * @NOTE The implementing modules may throw an error, which will not be caught + * here; it's up to the calling function to catch any thrown errors. + * + * @see media_parse_to_uri() + * @see media_add_from_url_submit() + */ +function media_parse_to_file($url) { + try { + $uri = media_parse_to_uri($url); + } + catch (Exception $e) { + // Pass the error along. + throw $e; + return; + } + + if (isset($uri)) { + // Attempt to load an existing file from the unique URI. + $select = db_select('file_managed', 'f') + ->extend('PagerDefault') + ->fields('f', array('fid')) + ->condition('uri', $uri); + + $fid = $select->execute()->fetchCol(); + if (!empty($fid)) { + $file = file_load(array_pop($fid)); + return $file; + } + } + + if (isset($uri)) { + // The URL was successfully parsed to a URI, but does not yet have an + // associated file: save it! + $file = file_uri_to_object($uri); + file_save($file); + } + else { + // The URL wasn't parsed. We'll try to save a remote file. + // Copy to temporary first. + $source_uri = file_stream_wrapper_uri_normalize('temporary://' . basename($url)); + if (!@copy(@$url, $source_uri)) { + throw new Exception('Unable to add file ' . $url); + return; + } + $source_file = file_uri_to_object($source_uri); + $scheme = variable_get('file_default_scheme', 'public') . '://'; + $uri = file_stream_wrapper_uri_normalize($scheme . $source_file->filename); + // Now to its new home. + $file = file_move($source_file, $uri, FILE_EXISTS_RENAME); + } + + return $file; +} + +/** + * Utility function to recursively run check_plain on an array. + * + * @todo There is probably something in core I am not aware of that does this. + */ +function media_recursive_check_plain(&$value, $key) { + $value = check_plain($value); +} + +/** + * Implements hook_element_info(). + */ +function media_element_info() { + $types['media'] = array( + '#input' => TRUE, + '#process' => array('media_element_process'), + '#value_callback' => 'media_file_value', + '#element_validate' => array('media_element_validate'), + '#pre_render' => array('media_element_pre_render'), + '#theme' => 'media_element', + '#theme_wrappers' => array('form_element'), + '#size' => 22, + '#extended' => FALSE, + '#media_options' => array( + 'global' => array( + // Example: array('image', 'audio'); + 'types' => array(), + // Example: array('http', 'ftp', 'flickr'); + 'schemes' => array(), + ), + ), + '#attached' => array( + 'library' => array( + array('media', 'media_browser'), + ), + ), + ); + + $setting = array(); + $setting['media']['global'] = $types['media']['#media_options']; + + $types['media']['#attached']['js'][] = array( + 'type' => 'setting', + 'data' => $setting, + ); + + return $types; +} + +/** + * Process callback for the media form element. + */ +function media_element_process($element, &$form_state, $form) { + ctools_include('modal'); + ctools_include('ajax'); + ctools_modal_add_js(); + + // Append the '-upload' to the #id so the field label's 'for' attribute + // corresponds with the textfield element. + $original_id = $element['#id']; + $element['#id'] .= '-upload'; + $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0; + + // Set some default element properties. + $element['#file'] = $fid ? file_load($fid) : FALSE; + $element['#tree'] = TRUE; + + $ajax_settings = array( + 'path' => 'media/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'], + 'wrapper' => $original_id . '-ajax-wrapper', + 'effect' => 'fade', + ); + + // Set up the buttons first since we need to check if they were clicked. + $element['attach_button'] = array( + '#name' => implode('_', $element['#parents']) . '_attach_button', + '#type' => 'submit', + '#value' => t('Attach'), + '#validate' => array(), + '#submit' => array('media_file_submit'), + '#limit_validation_errors' => array($element['#parents']), + '#ajax' => $ajax_settings, + '#attributes' => array('class' => array('attach')), + '#weight' => -1, + ); + + $element['preview'] = array( + 'content' => array(), + '#prefix' => '<div class="preview">', + '#suffix' => '</div>', + '#ajax' => $ajax_settings, + '#weight' => -10, + ); + + // Substitute the JS preview for a true file thumbnail once the file is + // attached. + if ($fid && $element['#file']) { + $element['preview']['content'] = media_get_thumbnail_preview($element['#file']); + } + + // The file ID field itself. + $element['upload'] = array( + '#name' => 'media[' . implode('_', $element['#parents']) . ']', + '#type' => 'textfield', + '#title' => t('Enter the ID of an existing file'), + '#title_display' => 'invisible', + '#field_prefix' => t('File ID'), + '#size' => $element['#size'], + '#theme_wrappers' => array(), + '#attributes' => array('class' => array('upload')), + '#weight' => -9, + ); + + $element['browse_button'] = array( + '#type' => 'link', + '#href' => '', + '#title' => t('Browse'), + '#attributes' => array('class' => array('button', 'browse', 'element-hidden')), + '#options' => array('fragment' => FALSE, 'external' => TRUE), + '#weight' => -8, + ); + + // Force the progress indicator for the remove button to be either 'none' or + // 'throbber', even if the upload button is using something else. + $ajax_settings['progress']['type'] = 'throbber'; + $ajax_settings['progress']['message'] = NULL; + $ajax_settings['effect'] = 'none'; + $element['edit'] = array( + '#type' => 'link', + '#href' => 'media/' . $fid . '/edit/nojs', + '#title' => t('Edit'), + '#attributes' => array( + 'class' => array( + // Required for CTools modal to work. + 'ctools-use-modal', 'use-ajax', + 'ctools-modal-media-file-edit', 'button', 'edit', + ), + ), + '#weight' => 20, + '#access' => $element['#file'] ? file_entity_access('update', $element['#file']) : FALSE, + ); + $element['remove_button'] = array( + '#name' => implode('_', $element['#parents']) . '_remove_button', + '#type' => 'submit', + '#value' => t('Remove'), + '#validate' => array(), + '#submit' => array('media_file_submit'), + '#limit_validation_errors' => array($element['#parents']), + '#ajax' => $ajax_settings, + '#attributes' => array('class' => array('remove')), + '#weight' => 0, + ); + + $element['fid'] = array( + '#type' => 'hidden', + '#value' => $fid, + '#attributes' => array('class' => array('fid')), + ); + + // Media browser attach code. + $element['#attached']['js'][] = drupal_get_path('module', 'media') . '/js/media.js'; + + // Add the media options to the page as JavaScript settings. + $element['browse_button']['#attached']['js'] = array( + array( + 'type' => 'setting', + 'data' => array('media' => array('elements' => array('#' . $element['#id'] => $element['#media_options']))) + ) + ); + + $element['#attached']['library'][] = array('media', 'media_browser'); + $element['#attached']['library'][] = array('media', 'media_browser_settings'); + + // Prefix and suffix used for Ajax replacement. + $element['#prefix'] = '<div id="' . $original_id . '-ajax-wrapper">'; + $element['#suffix'] = '</div>'; + + return $element; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function media_form_file_entity_edit_alter(&$form, &$form_state) { + // Make adjustments to the file edit form when used in a CTools modal. + if (!empty($form_state['ajax'])) { + // Remove the preview and the delete button. + $form['preview']['#access'] = FALSE; + $form['actions']['delete']['#access'] = FALSE; + + // Convert the cancel link to a button which triggers a modal close. + $form['actions']['cancel']['#attributes']['class'][] = 'button'; + $form['actions']['cancel']['#attributes']['class'][] = 'button-no'; + $form['actions']['cancel']['#attributes']['class'][] = 'ctools-close-modal'; + } +} + +/** + * The #value_callback for a media type element. + */ +function media_file_value(&$element, $input = FALSE, $form_state = NULL) { + $fid = 0; + + // Find the current value of this field from the form state. + $form_state_fid = $form_state['values']; + foreach ($element['#parents'] as $parent) { + $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0; + } + + if ($element['#extended'] && isset($form_state_fid['fid'])) { + $fid = $form_state_fid['fid']; + } + elseif (is_numeric($form_state_fid)) { + $fid = $form_state_fid; + } + + // Process any input and attach files. + if ($input !== FALSE) { + $return = $input; + + // Attachments take priority over all other values. + if ($file = media_attach_file($element)) { + $fid = $file->fid; + } + else { + // Check for #filefield_value_callback values. + // Because FAPI does not allow multiple #value_callback values like it + // does for #element_validate and #process, this fills the missing + // functionality to allow File fields to be extended through FAPI. + if (isset($element['#file_value_callbacks'])) { + foreach ($element['#file_value_callbacks'] as $callback) { + $callback($element, $input, $form_state); + } + } + // Load file if the FID has changed to confirm it exists. + if (isset($input['fid']) && $file = file_load($input['fid'])) { + $fid = $file->fid; + } + } + } + + // If there is no input, set the default value. + else { + if ($element['#extended']) { + $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0; + $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0); + } + else { + $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0; + $return = array('fid' => 0); + } + + // Confirm that the file exists when used as a default value. + if ($default_fid && $file = file_load($default_fid)) { + $fid = $file->fid; + } + } + + $return['fid'] = $fid; + + return $return; +} + +/** + * Validate media form elements. + * + * The file type is validated during the upload process, but this is necessary + * necessary in order to respect the #required property. + */ +function media_element_validate(&$element, &$form_state) { + $clicked_button = end($form_state['triggering_element']['#parents']); + + // Check required property based on the FID. + if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('attach_button', 'remove_button'))) { + form_error($element['browse_button'], t('!name field is required.', array('!name' => $element['#title']))); + } + + // Consolidate the array value of this field to a single FID. + if (!$element['#extended']) { + form_set_value($element, $element['fid']['#value'], $form_state); + } +} + +/** + * Form submission handler for attach / remove buttons of media elements. + * + * @see media_element_process() + */ +function media_file_submit($form, &$form_state) { + // Determine whether it was the attach or remove button that was clicked, and + // set $element to the managed_file element that contains that button. + $parents = $form_state['triggering_element']['#array_parents']; + $button_key = array_pop($parents); + $element = drupal_array_get_nested_value($form, $parents); + + // No action is needed here for the attach button, because all media + // attachments on the form are processed by media_file_value() regardless of + // which button was clicked. Action is needed here for the remove button, + // because we only remove a file in response to its remove button being + // clicked. + if ($button_key == 'remove_button') { + // If it's a temporary file we can safely remove it immediately, otherwise + // it's up to the implementing module to clean up files that are in use. + if ($element['#file'] && $element['#file']->status == 0) { + file_delete($element['#file']); + } + // Update both $form_state['values'] and $form_state['input'] to reflect + // that the file has been removed, so that the form is rebuilt correctly. + // $form_state['values'] must be updated in case additional submit handlers + // run, and for form building functions that run during the rebuild, such as + // when the media element is part of a field widget. + // $form_state['input'] must be updated so that media_file_value() has + // correct information during the rebuild. + $values_element = $element['#extended'] ? $element['fid'] : $element; + form_set_value($values_element, NULL, $form_state); + drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL); + } + + // Set the form to rebuild so that $form is correctly updated in response to + // processing the file removal. Since this function did not change $form_state + // if the upload button was clicked, a rebuild isn't necessary in that + // situation and setting $form_state['redirect'] to FALSE would suffice. + // However, we choose to always rebuild, to keep the form processing workflow + // consistent between the two buttons. + $form_state['rebuild'] = TRUE; +} + +/** + * Attaches any files that have been referenced by a media element. + * + * @param $element + * The FAPI element whose files are being attached. + * + * @return + * The file object representing the file that was attached, or FALSE if no + * file was attached. + */ +function media_attach_file($element) { + $upload_name = implode('_', $element['#parents']); + if (empty($_POST['media'][$upload_name])) { + return FALSE; + } + + if (!$file = file_load($_POST['media'][$upload_name])) { + watchdog('file', 'The file upload failed. %upload', array('%upload' => $upload_name)); + form_set_error($upload_name, t('The file in the !name field was unable to be uploaded.', array('!name' => $element['#title']))); + return FALSE; + } + + return $file; +} + +/** + * Returns HTML for a managed file element. + * + * @param $variables + * An associative array containing: + * - element: A render element representing the file. + * + * @ingroup themeable + */ +function theme_media_element($variables) { + $element = $variables['element']; + + $attributes = array(); + if (isset($element['#id'])) { + $attributes['id'] = $element['#id']; + } + if (!empty($element['#attributes']['class'])) { + $attributes['class'] = (array) $element['#attributes']['class']; + } + $attributes['class'][] = 'form-media'; + + // This wrapper is required to apply JS behaviors and CSS styling. + $output = ''; + $output .= '<div' . drupal_attributes($attributes) . '>'; + $output .= drupal_render_children($element); + $output .= '</div>'; + return $output; +} + +/** + * #pre_render callback to hide display of the browse/attach or remove controls. + * + * Browse/attach controls are hidden when a file is already attached. + * Remove controls are hidden when there is no file attached. Controls are + * hidden here instead of in media_element_process(), because #access for these + * buttons depends on the media element's #value. See the documentation of + * form_builder() for more detailed information about the relationship between + * #process, #value, and #access. + * + * Because #access is set here, it affects display only and does not prevent + * JavaScript or other untrusted code from submitting the form as though access + * were enabled. The form processing functions for these elements should not + * assume that the buttons can't be "clicked" just because they are not + * displayed. + * + * @see media_element_process() + * @see form_builder() + */ +function media_element_pre_render($element) { + // If we already have a file, we don't want to show the browse and attach + // controls. + if (!empty($element['#value']['fid'])) { + $element['upload']['#access'] = FALSE; + $element['browse_button']['#access'] = FALSE; + $element['attach_button']['#access'] = FALSE; + } + // If we don't already have a file, there is nothing to remove. + else { + $element['remove_button']['#access'] = FALSE; + } + return $element; +} + +/** + * Generates a thumbnail preview of a file. + * + * Provides default fallback images if an image of the file cannot be generated. + * + * @param object $file + * A Drupal file object. + * @param boolean $link + * (optional) Boolean indicating whether the thumbnail should be linked to the + * file. Defaults to FALSE. + * @param string $view_mode + * (optional) The view mode to use when rendering the thumbnail. Defaults to + * 'preview'. + * + * @return array + * Renderable array suitable for drupal_render() with the necessary classes + * and CSS to support a media thumbnail. + */ +function media_get_thumbnail_preview($file, $link = FALSE, $view_mode = 'preview') { + // If a file has an invalid type, allow file_view_file() to work. + if (!file_type_is_enabled($file->type)) { + $file->type = file_get_type($file); + } + + $preview = file_view_file($file, $view_mode); + $preview['#show_names'] = TRUE; + $preview['#add_link'] = $link; + $preview['#theme_wrappers'][] = 'media_thumbnail'; + $preview['#attached']['css'][] = drupal_get_path('module', 'media') . '/css/media.css'; + + return $preview; +} + +/** + * Check that the media is one of the selected types. + * + * @param object $file + * A Drupal file object. + * @param array $types + * An array of media type names + * + * @return array + * If the file type is not allowed, it will contain an error message. + * + * @see hook_file_validate() + */ +function media_file_validate_types($file, $types) { + $errors = array(); + if (!in_array(file_get_type($file), $types)) { + $errors[] = t('Only the following types of files are allowed to be uploaded: %types-allowed', array('%types-allowed' => implode(', ', $types))); + } + + return $errors; +} + +/** + * Implements hook_file_displays_alter(). + */ +function media_file_displays_alter(&$displays, $file, $view_mode) { + if ($view_mode == 'preview' && empty($displays)) { + // We re in the media browser and this file has no formatters enabled. + // Instead of letting it go through theme_file_link(), pass it through + // theme_media_formatter_large_icon() to get our cool file icon instead. + $displays['file_field_media_large_icon'] = array( + 'weight' => 0, + 'status' => 1, + 'settings' => NULL, + ); + } + + // Override the fields of the file when requested by the WYSIWYG. + if (isset($file->override) && isset($file->override['fields'])) { + $instance = field_info_instances('file', $file->type); + foreach ($file->override['fields'] as $field_name => $value) { + if (!isset($instance[$field_name]['settings']) || !isset($instance[$field_name]['settings']['wysiwyg_override']) || $instance[$field_name]['settings']['wysiwyg_override']) { + $file->{$field_name} = $value;} + } + } + + // Alt and title are special. + // @see file_entity_file_load + $alt = variable_get('file_entity_alt', '[file:field_file_image_alt_text]'); + $title = variable_get('file_entity_title', '[file:field_file_image_title_text]'); + + $replace_options = array( + 'clear' => TRUE, + 'sanitize' => FALSE, + ); + + // Load alt and title text from fields. + if (!empty($alt)) { + $file->alt = token_replace($alt, array('file' => $file), $replace_options); + } + if (!empty($title)) { + $file->title = token_replace($title, array('file' => $file), $replace_options); + } +} + +/** + * For sanity in grammar. + * + * @see media_set_browser_params() + */ +function media_get_browser_params() { + return media_set_browser_params(); +} + +/** + * Provides a singleton of the params passed to the media browser. + * + * This is useful in situations like form alters because callers can pass + * id="wysiywg_form" or whatever they want, and a form alter could pick this up. + * We may want to change the hook_media_browser_plugin_view() implementations to + * use this function instead of being passed params for consistency. + * + * It also offers a chance for some meddler to meddle with them. + * + * @see media_browser() + */ +function media_set_browser_params() { + $params = &drupal_static(__FUNCTION__, array()); + + if (empty($params)) { + // Build out browser settings. Permissions- and security-related behaviors + // should not rely on these parameters, since they come from the HTTP query. + // @TODO make sure we treat parameters as user input. + $params = drupal_get_query_parameters() + array( + 'types' => array(), + 'multiselect' => FALSE, + ); + + // Transform text 'true' and 'false' to actual booleans. + foreach ($params as $k => $v) { + if ($v === 'true') { + $params[$k] = TRUE; + } + elseif ($v === 'false') { + $params[$k] = FALSE; + } + } + + array_walk_recursive($params, 'media_recursive_check_plain'); + + // Allow modules to alter the parameters. + drupal_alter('media_browser_params', $params); + } + + return $params; +} + +/** + * Implements hook_ctools_plugin_api(). + * + * Lets CTools know which plugin APIs are implemented by Media module. + */ +function media_ctools_plugin_api($module, $api) { + if ($module == 'file_entity' && $api == 'file_default_displays') { + return array( + 'version' => 1, + ); + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * This alter enhances the default admin/content/file page, addding JS and CSS. + * It also makes modifications to the thumbnail view by replacing the existing + * checkboxes and table with thumbnails. + */ +function media_form_file_entity_admin_file_alter(&$form, $form_state) { + if (!empty($form_state['values']['operation'])) { + // The form is being rebuilt because an operation requiring confirmation + // We don't want to be messing with it in this case. + return; + } + + // Add the "Add file" local action, and notify users if they have files + // selected and they try to switch between the "Thumbnail" and "List" local + // tasks. + $path = drupal_get_path('module', 'media'); + $form['#attributes']['class'][] = 'file-entity-admin-file-form'; + $form['#attached']['js'][] = $path . '/js/media.admin.js'; + $form['#attached']['css'][] = $path . '/css/media.css'; + + // By default, this form contains a table select element called "files". For + // the 'thumbnails' tab, Media generates a thumbnail for each file and + // replaces the tableselect with a grid of thumbnails. + if (arg(3) == 'thumbnails') { + if (empty($form['admin']['files']['#options'])) { + // Display empty text if there are no files. + $form['admin']['files'] = array( + '#markup' => '<p>' . $form['admin']['files']['#empty'] . '</p>', + ); + } + else { + $files = file_load_multiple(array_keys($form['admin']['files']['#options'])); + + $form['admin']['files'] = array( + '#tree' => TRUE, + '#prefix' => '<div class="media-display-thumbnails media-clear clearfix"><ul id="media-browser-library-list" class="media-list-thumbnails">', + '#suffix' => '</ul></div>', + ); + + foreach ($files as $file) { + $preview = media_get_thumbnail_preview($file, TRUE); + $form['admin']['files'][$file->fid] = array( + '#type' => 'checkbox', + '#title' => '', + '#prefix' => '<li>' . drupal_render($preview), + '#suffix' => '</li>', + ); + } + } + } +} + +/** + * Implements hook_views_api(). + */ +function media_views_api() { + return array( + 'api' => 3, + 'path' => drupal_get_path('module', 'media'), + ); +} + +/** + * Implements hook_views_default_views(). + */ +function media_views_default_views() { + return media_load_all_exports('media', 'views', 'view.inc', 'view'); +} + +/** + * Fetches an array of exportables from files. + * + * @param string $module + * The module invoking this request. (Can be called by other modules.) + * @param string $directory + * The subdirectory in the custom module. + * @param string $extension + * The file extension. + * @param string $name + * The name of the variable found in each file. Defaults to the same as + * $extension. + * + * @return array + * Array of $name objects. + */ +function media_load_all_exports($module, $directory, $extension, $name = NULL) { + if (!$name) { + $name = $extension; + } + + $return = array(); + // Find all the files in the directory with the correct extension. + $files = file_scan_directory(drupal_get_path('module', $module) . "/$directory", "/.$extension/"); + foreach ($files as $path => $file) { + require $path; + if (isset($$name)) { + $return[$$name->name] = $$name; + } + } + + return $return; +} + +/** + * Returns metadata describing Media browser plugins. + * + * @return + * An associative array of plugin information, keyed by plugin. + * + * @see hook_media_browser_plugin_info() + * @see hook_media_browser_plugin_info_alter() + */ +function media_get_browser_plugin_info() { + $info = &drupal_static(__FUNCTION__); + + if (!isset($info)) { + $info = module_invoke_all('media_browser_plugin_info'); + drupal_alter('media_browser_plugin_info', $info); + } + + return $info; +} + +/** + * Gets the MIME type mapped to a given extension. + * + * @param string $extension + * A file extension. + * + * @return string + * The MIME type associated with the extension or FALSE if the extension does + * not have an associated MIME type. + * + * @see file_mimetype_mapping() + */ +function media_get_extension_mimetype($extension) { + include_once DRUPAL_ROOT . '/includes/file.mimetypes.inc'; + $mimetype_mappings = file_mimetype_mapping(); + + if (isset($mimetype_mappings['extensions'][$extension])) { + $id = $mimetype_mappings['extensions'][$extension]; + return $mimetype_mappings['mimetypes'][$id]; + } + else { + return FALSE; + } +} + +/** + * Helper function to get a list of local stream wrappers. + */ +function media_get_local_stream_wrappers() { + return file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_NORMAL); +} + +/** + * Helper function to get a list of remote stream wrappers. + */ +function media_get_remote_stream_wrappers() { + $wrappers = file_get_stream_wrappers(); + $wrappers = array_diff_key($wrappers, file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_NORMAL)); + $wrappers = array_diff_key($wrappers, file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL_HIDDEN)); + return $wrappers; +} diff --git a/sites/all/modules/media/media.views.inc b/sites/all/modules/media/media.views.inc new file mode 100644 index 000000000..aed55fa48 --- /dev/null +++ b/sites/all/modules/media/media.views.inc @@ -0,0 +1,133 @@ +<?php + +/** + * @file + * Provide Views data and handlers for media.module + */ + +/** + * Implements hook_views_plugins(). + * + * Generate a list of which base-tables to enabled the plugins for. + */ +function media_views_plugins() { + $plugins = array(); + + // Always allow the actual file-table + $base = array('file_managed'); + + if (module_exists('search_api')) { + // If the Search API module exists, also allow indices of the file-entity + // that has the fid field indexed. + $indices = search_api_index_load_multiple(NULL); + foreach ($indices as $machine_name => $index) { + if ($index->item_type == 'file' && isset($index->options['fields']['fid'])) { + $base[] = 'search_api_index_' . $machine_name; + } + } + } + + // Display plugin. + $plugins['display']['media_browser'] = array( + 'title' => t('Media browser tab'), + 'help' => t('Display as a tab in the media browser.'), + 'handler' => 'media_views_plugin_display_media_browser', + 'theme' => 'views_view', + 'base' => $base, + 'use ajax' => TRUE, + 'use pager' => TRUE, + 'accept attachments' => TRUE, + ); + + // Style plugin. + $plugins['style']['media_browser'] = array( + 'title' => t('Media browser'), + 'help' => t('Displays rows as an HTML list.'), + 'handler' => 'media_views_plugin_style_media_browser', + 'theme' => 'media_views_view_media_browser', + 'base' => $base, + 'uses row plugin' => FALSE, + 'uses row class' => FALSE, + 'uses options' => FALSE, + 'uses fields' => FALSE, + 'type' => 'normal', + 'help topic' => 'style-media-browser', + ); + return $plugins; +} + +/** + * Display the view as a media browser. + */ +function template_preprocess_media_views_view_media_browser(&$vars) { + module_load_include('inc', 'media', 'includes/media.browser'); + // Load file objects for each View result. + $fids = array(); + foreach ($vars['rows'] as $index => $row) { + // The Search API module returns the row in a slightly different format, + // so convert it to the format that the normal file_managed table returns. + if (!empty($row->entity->fid)) { + $vars['rows'][$index]->fid = $row->entity->fid; + } + $fids[$index] = $row->fid; + } + $files = file_load_multiple($fids); + + // Render the preview for each file. + $params = media_get_browser_params(); + $view_mode = isset($params['view_mode']) ? $params['view_mode'] : 'preview'; + + foreach ($vars['rows'] as $index => $row) { + $file = $files[$row->fid]; + // Add url/preview to the file object. + media_browser_build_media_item($file, $view_mode); + $vars['rows'][$index] = $file; + $vars['rows'][$index]->preview = $file->preview; + } + + // Add the files to JS so that they are accessible inside the browser. + drupal_add_js(array('media' => array('files' => array_values($files))), 'setting'); + + // Add the browser parameters to the settings and that this display exists. + drupal_add_js(array( + 'media' => array( + 'browser' => array( + 'params' => media_get_browser_params(), + 'views' => array( + $vars['view']->name => array( + $vars['view']->current_display, + ), + ), + ), + ), + ), 'setting'); + + // Add classes and wrappers from the style plugin. + $handler = $vars['view']->style_plugin; + + $class = explode(' ', $handler->options['class']); + $class = array_map('drupal_clean_css_identifier', $class); + + $wrapper_class = explode(' ', $handler->options['wrapper_class']); + $wrapper_class = array_map('drupal_clean_css_identifier', $wrapper_class); + + $vars['class'] = implode(' ', $class); + $vars['wrapper_class'] = implode(' ', $wrapper_class); + $vars['wrapper_prefix'] = '<div class="' . implode(' ', $wrapper_class) . '">'; + $vars['wrapper_suffix'] = '</div>'; + $vars['list_type_prefix'] = '<' . $handler->options['type'] . ' id="media-browser-library-list" class="' . implode(' ', $class) . '">'; + $vars['list_type_suffix'] = '</' . $handler->options['type'] . '>'; + + // Run theming variables through a standard Views preprocess function. + template_preprocess_views_view_unformatted($vars); + + // Add media browser javascript and CSS. + drupal_add_js(drupal_get_path('module', 'media') . '/js/plugins/media.views.js'); +} + +/** + * Implements hook_views_invalidate_cache(). + */ +function media_views_invalidate_cache() { + drupal_static_reset('media_get_browser_plugin_info'); +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/includes/MediaBrowserBulkUpload.inc b/sites/all/modules/media/modules/media_bulk_upload/includes/MediaBrowserBulkUpload.inc new file mode 100644 index 000000000..9dbc98332 --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/includes/MediaBrowserBulkUpload.inc @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Definition of MediaBrowserBulkUpload. + */ + +/** + * Media browser plugin for showing the bulk upload form. + * + * @deprecated + */ +class MediaBrowserBulkUpload extends MediaBrowserUpload { + /** + * Overrides MediaBrowserPlugin::view(). + */ + public function view() { + module_load_include('inc', 'file_entity', 'file_entity.pages'); + + $build = array(); + if ($this->params['multiselect']) { + $build['form'] = drupal_get_form('file_entity_add_upload_multiple', $this->params); + } + else { + $build['form'] = drupal_get_form('file_entity_add_upload', $this->params); + } + + return $build; + } +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.admin.inc b/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.admin.inc new file mode 100644 index 000000000..bc0b77f6b --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.admin.inc @@ -0,0 +1,155 @@ +<?php + +/** + * @file + * This file contains the admin functions for the Media Bulk Upload module. + */ + +/** + * Form callback for mass import. + */ +function media_bulk_upload_import($form, &$form_state) { + if (!isset($form_state['storage']['files'])) { + $form_state['storage']['step'] = 'choose'; + $form_state['storage']['next_step'] = 'preview'; + $form['directory'] = array( + '#type' => 'textfield', + '#title' => t('Directory'), + '#description' => t('Enter the absolute directory on the web server to look for files. Subdirectories inside this directory will not be scanned.'), + '#required' => TRUE, + ); + + $form['pattern'] = array( + '#type' => 'textarea', + '#title' => t('Pattern'), + '#description' => t("Only files matching these patterns will be imported. Enter one pattern per line. The '*' character is a wildcard. Example patterns are %png_example to import all PNG files.", array('%png_example' => '*.png')), + '#default_value' => '*', + '#required' => TRUE, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Preview'), + ); + $form['actions']['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#href' => isset($_GET['destination']) ? $_GET['destination'] : 'admin/content/file', + ); + } + else { + $form['preview'] = array( + '#markup' => theme('item_list', array('items' => $form_state['storage']['files'])), + ); + + $form = confirm_form($form, t('Import these files?'), 'admin/content/file/import'); + } + return $form; + +} + +/** + * Validate handler for media_import(). + */ +function media_bulk_upload_import_validate($form, &$form_state) { + if ($form_state['values']['op'] != t('Confirm')) { + $directory = $form_state['values']['directory']; + $pattern = $form_state['values']['pattern']; + if (!is_dir($directory)) { + form_set_error('directory', t('The provided directory does not exist.')); + } + if (!is_readable($directory)) { + form_set_error('directory', t('The provided directory is not readable.')); + } + + $pattern_quoted = preg_quote($pattern, '/'); + $pattern_quoted = preg_replace('/(\r\n?|\n)/', '|', $pattern_quoted); + $pattern_quoted = strtr($pattern_quoted, array( + '\\|' => '|', + '\\*' => '.*', + '\\?' => '.?', + )); + $files = file_scan_directory($directory, '/^(' . $pattern_quoted . ')$/', array('recurse' => FALSE)); + $files = array_keys($files); + if (empty($files)) { + form_set_error('pattern', t('No files were found in %directory matching the regular expression %pattern', array('%directory' => $directory, '%pattern' => $pattern_quoted))); + } + $form_state['storage']['files'] = $files; + } +} + +/** + * Submit handler for media_import(). + */ +function media_bulk_upload_import_submit($form, &$form_state) { + if ($form_state['values']['op'] == t('Confirm')) { + $files = $form_state['storage']['files']; + $batch = array( + 'title' => t('Importing'), + 'operations' => array( + array('media_bulk_upload_import_batch_import_files', array($files)), + ), + 'finished' => 'media_bulk_upload_import_batch_import_complete', + 'file' => drupal_get_path('module', 'media_bulk_upload') . '/includes/media_bulk_upload.admin.inc', + ); + batch_set($batch); + return; + + } + $form_state['rebuild'] = TRUE; +} + +/** + * BatchAPI callback op for media import. + */ +function media_bulk_upload_import_batch_import_files($files, &$context) { + if (!isset($context['sandbox']['files'])) { + // This runs the first time the batch runs. + // This is stupid, but otherwise, I don't think it will work... + $context['results'] = array('success' => array(), 'errors' => array()); + $context['sandbox']['max'] = count($files); + $context['sandbox']['files'] = $files; + } + $files =& $context['sandbox']['files']; + + // Take a cut of files. Let's do 10 at a time. + $import_batch_size = variable_get('media_bulk_upload_import_batch_size', 20); + $length = (count($files) > $import_batch_size) ? $import_batch_size : count($files); + $to_process = array_splice($files, 0, $length); + $image_in_message = ''; + + foreach ($to_process as $file) { + try { + $file_obj = media_parse_to_file($file); + $context['results']['success'][] = $file; + if (!$image_in_message) { + // @todo Is this load step really necessary? When there's time, test + // this, and either remove it, or comment why it's needed. + $loaded_file = file_load($file_obj->fid); + $image_in_message = file_view_file($loaded_file, 'preview'); + } + } + catch (Exception $e) { + $context['results']['errors'][] = $file . " Reason: " . $e->getMessage(); + } + } + + $context['message'] = "Importing " . theme('item_list', array('items' => $to_process)); + // Show the image that is being imported. + $context['message'] .= drupal_render($image_in_message); + + $context['finished'] = ($context['sandbox']['max'] - count($files)) / $context['sandbox']['max']; +} + +/** + * BatchAPI complete callback for media import. + */ +function media_bulk_upload_import_batch_import_complete($success, $results, $operations) { + if ($results['errors']) { + drupal_set_message(theme('item_list', array('items' => $results['errors'])), 'error'); + } + if ($results['success']) { + drupal_set_message(theme('item_list', array('items' => $results['success']))); + } +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.pages.inc b/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.pages.inc new file mode 100644 index 000000000..41287ebb0 --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/includes/media_bulk_upload.pages.inc @@ -0,0 +1,95 @@ +<?php + +/** + * @file + * Common pages for the Media Bulk Upload module. + */ + +/** + * Menu callback; Edit multiple files on the same page using multiform module. + * + * @todo When http://drupal.org/node/1227706 is fixed, filter the $files + * array using file_access($file, 'edit'). + * + * @see media_bulk_upload_file_operation_edit_multiple() + */ +function media_bulk_upload_file_page_edit_multiple($files) { + if (empty($files)) { + return MENU_ACCESS_DENIED; + } + + $forms = array(); + foreach ($files as $file) { + // To maintain unique form_ids, append the file id. + $forms[] = array('media_edit_' . $file->fid, $file); + } + + $form = call_user_func_array('multiform_get_form', $forms); + $form['#attributes']['class'][] = 'media-bulk-upload-multiedit-form'; + + // Improve the display of each file form. + foreach (element_children($form['multiform']) as $key) { + $fid = $form['multiform'][$key]['fid']['#value']; + $file = $files[$fid]; + + // Add the filename to each 'subform'. + $title = t('<em>Edit @type</em> @title', array('@type' => $file->type, '@title' => $file->filename)); + $form['multiform'][$key]['#prefix'] = '<h2>' . $title . '</h2>'; + + // Remove the 'replace file' functionality. + $form['multiform'][$key]['replace_upload']['#access'] = FALSE; + + // Remove any actions. + $form['multiform'][$key]['actions']['#access'] = FALSE; + + // Hide additional settings under a collapsible fieldset. + $form['multiform'][$key]['settings'] = array( + '#type' => 'fieldset', + '#title' => t('Additional settings'), + '#weight' => 99, + '#collapsible' => TRUE, + '#collapsed' => TRUE, + // FAPI #collapsed and #collapsible not available in a render array. + '#attached' => array( + 'js' => array( + 'misc/form.js', + 'misc/collapse.js', + ), + ), + '#attributes' => array( + 'class' => array('collapsible', 'collapsed'), + ), + ); + + $form['multiform'][$key]['settings']['additional_settings'] = $form['multiform'][$key]['additional_settings']; + unset($form['multiform'][$key]['additional_settings']); + } + + if (isset($form['buttons']['Delete'])) { + $form['buttons']['Delete']['#access'] = FALSE; + } + + // Add a cancel button at the bottom of the form. + $form['buttons']['cancel'] = array( + '#type' => 'link', + '#title' => t('Cancel'), + '#weight' => 50, + ); + if (isset($_GET['destination'])) { + $form['buttons']['cancel']['#href'] = $_GET['destination']; + } + else if (user_access('administer files')) { + $form['buttons']['cancel']['#href'] = 'admin/content/file'; + } + else { + $form['buttons']['cancel']['#href'] = '<front>'; + } + + // Override the page title since each file form sets a title. + drupal_set_title(t('Edit multiple files')); + + // Allow other modules to alter the form. + drupal_alter('media_bulk_upload_edit_multiple_form', $form); + + return $form; +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.info b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.info new file mode 100644 index 000000000..9763f529f --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.info @@ -0,0 +1,21 @@ +name = Media Bulk Upload +description = Adds support for uploading multiple files at a time. +package = Media +core = 7.x + +dependencies[] = media +dependencies[] = multiform +dependencies[] = plupload + +test_dependencies[] = multiform +test_dependencies[] = plupload + +files[] = includes/MediaBrowserBulkUpload.inc +files[] = tests/media_bulk_upload.test + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.install b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.install new file mode 100644 index 000000000..a7b0741c7 --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.install @@ -0,0 +1,14 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Media Bulk Upload module. + */ + +/** + * Implements hook_uninstall(). + */ +function media_bulk_upload_uninstall() { + // Remove variables. + variable_del('media_bulk_upload_import_batch_size'); +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.module b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.module new file mode 100644 index 000000000..9bd0d7af4 --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/media_bulk_upload.module @@ -0,0 +1,203 @@ +<?php + +/** + * @file + * Primarily Drupal hooks. + */ + +/** + * Implements hook_menu(). + */ +function media_bulk_upload_menu() { + $items['admin/content/file/import'] = array( + 'title' => 'Import files', + 'description' => 'Import files into your media library.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_bulk_upload_import'), + 'access arguments' => array('import media'), + 'type' => MENU_LOCAL_ACTION, + 'file' => 'includes/media_bulk_upload.admin.inc', + 'weight' => 10, + ); + $items['admin/content/file/thumbnails/import'] = $items['admin/content/file/import']; + + // @todo Investigate passing file IDs in query string rather than a menu + // argument and then deprecate media_multi_load(). + $items['admin/content/file/edit-multiple/%media_bulk_upload_multi'] = array( + 'title' => 'Edit multiple files', + 'page callback' => 'media_bulk_upload_file_page_edit_multiple', + 'page arguments' => array(4), + 'access callback' => '_media_bulk_upload_file_entity_access_recursive', + 'access arguments' => array(4, 'update'), + 'file' => 'includes/media_bulk_upload.pages.inc', + ); + + return $items; +} + +/** + * Implements hook_permission(). + */ +function media_bulk_upload_permission() { + return array( + 'import media' => array( + 'title' => t('Import media files from the local filesystem'), + 'description' => t('Simple file importer'), + 'restrict access' => TRUE, + ), + ); +} + +/** + * Implements hook_media_browser_plugin_info_alter(). + */ +function media_bulk_upload_media_browser_plugin_info_alter(&$info) { + $info['upload']['class'] = 'MediaBrowserBulkUpload'; +} + +/** + * Implements hook_file_operations(). + */ +function media_bulk_upload_file_operations() { + return array( + 'edit_multiple' => array( + 'label' => t('Edit selected files'), + 'callback' => 'media_bulk_upload_file_operation_edit_multiple', + ), + ); +} + +/** + * Implements hook_form_alter(). + */ +function media_bulk_upload_form_alter(&$form, &$form_state, $form_id) { + // If we're in the media browser, set the #media_browser key to true + // so that if an ajax request gets sent to a different path, the form + // still uses the media_browser_form_submit callback. + if (current_path() == 'media/browser' && $form_id != 'views_exposed_form') { + $form_state['#media_browser'] = TRUE; + } + + // If the #media_browser key isset and is true we are using the browser + // popup, so add the media_browser submit handler. + if (!empty($form_state['#media_browser'])) { + $form['#submit'][] = 'media_bulk_upload_browser_form_submit'; + } +} + +/** + * Submit handler; direction form submissions in the media browser. + */ +function media_bulk_upload_browser_form_submit($form, &$form_state) { + $url = NULL; + $parameters = array(); + + // Multi upload. + if (!empty($form_state['files'])) { + $files = $form_state['files']; + $url = 'media/browser'; + $parameters = array('query' => array('render' => 'media-popup', 'fid' => array_keys($files))); + } + + // If $url is set, we had some sort of upload, so redirect the form. + if (!empty($url)) { + $form_state['redirect'] = array($url, $parameters); + } +} + +/** + * Return a URL for editing an files. + * + * Works with an array of fids or a single fid. + * + * @param mixed $fids + * An array of file IDs or a single file ID. + */ +function media_bulk_upload_file_edit_url($fids) { + if (!is_array($fids)) { + $fids = array($fids); + } + + if (count($fids) > 1) { + return 'admin/content/file/edit-multiple/' . implode(' ', $fids); + } + else { + return 'file/' . reset($fids) . '/edit'; + } +} + +/** + * Callback for the edit operation. + * + * Redirects the user to the edit multiple files page. + * + * @param array $fids + * An array of file IDs. + * + * @see media_file_page_edit_multiple() + */ +function media_bulk_upload_file_operation_edit_multiple($fids) { + // The thumbnail browser returns TRUE/FALSE for each item, so use array keys. + $fids = array_keys(array_filter($fids)); + drupal_goto(media_bulk_upload_file_edit_url($fids), array('query' => drupal_get_destination())); +} + +/** + * Implements hook_forms(). + */ +function media_bulk_upload_forms($form_id, $args) { + $forms = array(); + // To support the multiedit form, each form has to have a unique ID. + // So we name all the forms media_edit_N where the first requested form is + // media_edit_0, 2nd is media_edit_1, etc. + module_load_include('inc', 'file_entity', 'file_entity.pages'); + if ($form_id != 'media_edit' && (strpos($form_id, 'media_edit') === 0)) { + $forms[$form_id] = array( + 'callback' => 'file_entity_edit', + 'wrapper_callback' => 'media_bulk_upload_prepare_edit_form', + ); + } + return $forms; +} + +function media_bulk_upload_prepare_edit_form($form, &$form_state) { + form_load_include($form_state, 'inc', 'file_entity', 'file_entity.pages'); +} + +/** + * Access callback for the media-multi form. + * + * @param $files + * An array of files being editing on the multiform. + * @param $op + * A string containing the operation requested, such as 'update'. + * @return + * TRUE if the current user has access to edit all of the files, otherwise FALSE. + */ +function _media_bulk_upload_file_entity_access_recursive($files, $op) { + // Check that the current user can access each file. + if (!empty($files)) { + foreach ($files as $file) { + if (!file_entity_access($op, $file)) { + return FALSE; + } + } + return TRUE; + } + return FALSE; +} + +/** + * Load callback for %media_multi placeholder in menu paths. + * + * @param string $fids + * Separated by space (e.g., "3 6 12 99"). This often appears as "+" within + * URLs (e.g., "3+6+12+99"), but Drupal automatically decodes paths when + * intializing $_GET['q']. + * + * @return array + * An array of corresponding file entities. + */ +function media_bulk_upload_multi_load($fids) { + return file_load_multiple(explode(' ', $fids)); +} diff --git a/sites/all/modules/media/modules/media_bulk_upload/tests/media_bulk_upload.test b/sites/all/modules/media/modules/media_bulk_upload/tests/media_bulk_upload.test new file mode 100644 index 000000000..6bd21987c --- /dev/null +++ b/sites/all/modules/media/modules/media_bulk_upload/tests/media_bulk_upload.test @@ -0,0 +1,105 @@ +<?php + +/** + * @file + * Tests for media_bulk_upload.module. + */ + +/** + * Provides methods specifically for testing Media Bulk Upload module's bulk file uploading capabilities. + */ +class MediaBulkUploadTestHelper extends DrupalWebTestCase { + function setUp() { + // Since this is a base class for many test cases, support the same + // flexibility that DrupalWebTestCase::setUp() has for the modules to be + // passed in as either an array or a variable number of string arguments. + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'media_bulk_upload'; + parent::setUp($modules); + } + + /** + * Retrieves a sample file of the specified type. + */ + function getTestFile($type_name, $size = NULL) { + // Get a file to upload. + $file = current($this->drupalGetTestFiles($type_name, $size)); + + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + + return $file; + } + + /** + * Get a file from the database based on its filename. + * + * @param $filename + * A file filename, usually generated by $this->randomName(). + * @param $reset + * (optional) Whether to reset the internal file_load() cache. + * + * @return + * A file object matching $filename. + */ + function getFileByFilename($filename, $reset = FALSE) { + $files = file_load_multiple(array(), array('filename' => $filename), $reset); + // Load the first file returned from the database. + $returned_file = reset($files); + return $returned_file; + } +} + +/** + * Test bulk file editing. + */ +class MediaBulkUploadEditTestCase extends MediaBulkUploadTestHelper { + public static function getInfo() { + return array( + 'name' => 'Bulk file editing', + 'description' => 'Test file editing with multiple files.', + 'group' => 'Media Bulk Upload', + 'dependencies' => array('multiform', 'plupload'), + ); + } + + function setUp() { + parent::setUp(); + + $web_user = $this->drupalCreateUser(array('create files', 'edit any document files', 'edit any image files')); + $this->drupalLogin($web_user); + } + + /** + * Tests editing with multiple files. + */ + function testBulkFileEditing() { + $files = array(); + + // Create multiple files for testing. + foreach (array('image', 'text') as $type_name) { + $test_file = $this->getTestFile($type_name); + $file = file_save($test_file); + $files[$file->fid] = $file; + } + + // Visit the bulk file edit page and verify that it performs as expected. + $path = media_bulk_upload_file_edit_url(array_keys($files)); + $this->drupalGet($path); + + foreach ($files as $file) { + // Verify that a filename for each file is present on the page. + $title = t('<em>Edit @type</em> @title', array('@type' => $file->type, '@title' => $file->filename)); + $this->assertRaw('<h2>' . $title . '</h2>', 'The file has the correct filename.'); + + // Verify that the 'replace file' functionality is disabled. + $this->assertNoField('multiform[media_edit_' . $file->fid . '_' . ($file->fid - 1) . '][files][replace_upload]', 'Replace file field found.'); + + // Verify that the action buttons have been removed. + $this->assertNoLinkByHref('file/' . $file->fid); + } + } +} diff --git a/sites/all/modules/media/modules/media_internet/includes/MediaBrowserInternet.inc b/sites/all/modules/media/modules/media_internet/includes/MediaBrowserInternet.inc new file mode 100644 index 000000000..b719dc9ac --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/includes/MediaBrowserInternet.inc @@ -0,0 +1,32 @@ +<?php + +/** + * @file + * Definition of MediaBrowserInternet. + */ + +/** + * Media browser plugin for Media Internet sources. + */ +class MediaBrowserInternet extends MediaBrowserPlugin { + /** + * Implements MediaBrowserPluginInterface::access(). + */ + public function access($account = NULL) { + return media_internet_access($account); + } + + /** + * Implements MediaBrowserPlugin::view(). + */ + public function view() { + module_load_include('inc', 'file_entity', 'file_entity.pages'); + + $build = array(); + $params = $this->params; + $params['internet_media'] = TRUE; + $build['form'] = drupal_get_form('media_internet_add_upload', $params); + + return $build; + } +} diff --git a/sites/all/modules/media/modules/media_internet/includes/MediaInternetBaseHandler.inc b/sites/all/modules/media/modules/media_internet/includes/MediaInternetBaseHandler.inc new file mode 100644 index 000000000..abd71d51e --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/includes/MediaInternetBaseHandler.inc @@ -0,0 +1,77 @@ +<?php + +/** + * @file + * Definition of MediaInternetBaseHandler. + */ + +/** + * A base class for managing the addition of Internet media. + * + * Classes extending this class manage the addition of Internet media. To + * achieve this, the class should parse user-submitted embed code, claim it + * when appropriate and save it as a managed file. + */ +abstract class MediaInternetBaseHandler { + + /** + * The constructor for the MediaInternetBaseHandler class. This method is also called + * from the classes that extend this class and override this method. + */ + public function __construct($embedCode) { + $this->embedCode = $embedCode; + } + + /** + * Determines if this handler should claim the item. + * + * @param string $embed_code + * A string of user-submitted embed code. + * + * @return boolean + * Pass TRUE to claim the item. + */ + abstract public function claim($embed_code); + + /** + * Returns a file object which can be used for validation. + * + * @return StdClass + */ + abstract public function getFileObject(); + + /** + * If required, implementors can validate the embedCode. + */ + public function validate() { + } + + /** + * Before the file has been saved, implementors may do additional operations. + * + * @param object $file_obj + */ + public function preSave(&$file_obj) { + } + + /** + * Saves a file to the file_managed table (with file_save). + * + * @return StdClass + */ + public function save() { + $file_obj = $this->getFileObject(); + $this->preSave($file_obj); + file_save($file_obj); + $this->postSave($file_obj); + return $file_obj; + } + + /** + * After the file has been saved, implementors may do additional operations. + * + * @param object $file_obj + */ + public function postSave(&$file_obj) { + } +} diff --git a/sites/all/modules/media/modules/media_internet/includes/MediaInternetFileHandler.inc b/sites/all/modules/media/modules/media_internet/includes/MediaInternetFileHandler.inc new file mode 100644 index 000000000..c5b65cf5b --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/includes/MediaInternetFileHandler.inc @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Definition of MediaInternetFileHandler. + */ + +/** + * A class for managing the addition of Internet files. + */ +class MediaInternetFileHandler extends MediaInternetBaseHandler { + + public $fileObject; + + public function preSave(&$file_obj) { + // Coppies the remote file locally. + $remote_uri = $file_obj->uri; + //@TODO: we should follow redirection here an save the final filename, not just the basename. + $local_filename = basename($remote_uri); + $local_filename = file_munge_filename($local_filename, variable_get('file_entity_default_allowed_extensions', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp mp3 mov mp4 m4a m4v mpeg avi ogg oga ogv weba webp webm'), FALSE); + $local_uri = file_stream_wrapper_uri_normalize('temporary://' . $local_filename); + if (!@copy($remote_uri, $local_uri)) { + throw new Exception('Unable to add file ' . $remote_uri); + return; + } + // Make the current fileObject point to the local_uri, not the remote one. + $file_obj = file_uri_to_object($local_uri); + } + + public function postSave(&$file_obj) { + $scheme = variable_get('file_default_scheme', 'public') . '://'; + module_load_include('inc', 'file_entity', 'file_entity.pages'); + $destination_uri = file_entity_upload_destination_uri(array()); + $uri = file_stream_wrapper_uri_normalize($destination_uri . '/' . $file_obj->filename); + // Now to its new home. + $file_obj = file_move($file_obj, $uri, FILE_EXISTS_RENAME); + } + + public function getFileObject() { + if (!$this->fileObject) { + $this->fileObject = file_uri_to_object($this->embedCode); + } + return $this->fileObject; + } + + public function claim($embedCode) { + // Claim only valid URLs using a supported scheme. + if (!valid_url($embedCode, TRUE) || !in_array(file_uri_scheme($embedCode), variable_get('media_fromurl_supported_schemes', array('http', 'https', 'ftp', 'smb', 'ftps')))) { + return FALSE; + } + + // This handler is intended for regular files, so don't claim URLs + // containing query strings or fragments. + if (preg_match('/[\?\#]/', $embedCode)) { + return FALSE; + } + + // Since this handler copies the remote file to the local web server, do not + // claim a URL with an extension disallowed for media uploads. + $regex = '/\.(' . preg_replace('/ +/', '|', preg_quote(variable_get('file_entity_default_allowed_extensions', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp mp3 mov mp4 m4a m4v mpeg avi ogg oga ogv weba webp webm'))) . ')$/i'; + if (!preg_match($regex, basename($embedCode))) { + return FALSE; + } + + return TRUE; + } +} diff --git a/sites/all/modules/media/modules/media_internet/includes/MediaInternetNoHandlerException.inc b/sites/all/modules/media/modules/media_internet/includes/MediaInternetNoHandlerException.inc new file mode 100644 index 000000000..5a199cc37 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/includes/MediaInternetNoHandlerException.inc @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of MediaInternetNoHandlerException. + */ + +/** + * A custom exception class for handling embed code with no handler. + */ +class MediaInternetNoHandlerException extends Exception { + +} diff --git a/sites/all/modules/media/modules/media_internet/includes/MediaInternetValidationException.inc b/sites/all/modules/media/modules/media_internet/includes/MediaInternetValidationException.inc new file mode 100644 index 000000000..36322b3ed --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/includes/MediaInternetValidationException.inc @@ -0,0 +1,13 @@ +<?php + +/** + * @file + * Definition of MediaInternetValidationException. + */ + +/** + * A custom exception class for handling file validation errors. + */ +class MediaInternetValidationException extends Exception { + +} diff --git a/sites/all/modules/media/modules/media_internet/media_internet.api.php b/sites/all/modules/media/modules/media_internet/media_internet.api.php new file mode 100644 index 000000000..4a6afc5a3 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/media_internet.api.php @@ -0,0 +1,47 @@ +<?php + +/** + * @file + * Hooks provided by the media_internet module. + */ + +/** + * Returns a list of Internet media providers for URL/embed code testing. + * + * @return array + * A nested array of provider information, keyed by class name. This class + * must implement a claim() method and may (should) extend the + * @link MediaInternetBaseHandler MediaInternetBaseHandler @endlink class. + * Each provider info array may have the following keys: + * - title: (required) A name to be used when listing the currently supported + * providers on the web tab of the media browser. + * - hidden: (optional) Boolean to prevent the provider title from being + * listed on the web tab of the media browser. + * - weight: (optional) Integer to determine the tab order. Defaults to 0. + * + * @see hook_media_internet_providers_alter() + * @see media_internet_get_providers() + */ +function hook_media_internet_providers() { + return array( + 'MyModuleYouTubeHandler' => array( + 'title' => t('YouTube'), + 'hidden' => TRUE, + ), + ); +} + +/** + * Alter the list of Internet media providers. + * + * @param array $providers + * The associative array of Internet media provider definitions from + * hook_media_internet_providers(). + * + * @see hook_media_internet_providers() + * @see media_internet_get_providers() + */ +function hook_media_internet_providers_alter(&$providers) { + $providers['MyModuleYouTubeHandler']['title'] = t('Google video hosting'); + $providers['MyModuleYouTubeHandler']['weight'] = 42; +} diff --git a/sites/all/modules/media/modules/media_internet/media_internet.info b/sites/all/modules/media/modules/media_internet/media_internet.info new file mode 100644 index 000000000..56ee57965 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/media_internet.info @@ -0,0 +1,20 @@ +name = Media Internet Sources +description = Provides an API for accessing media on various internet services +package = Media +core = 7.x + +dependencies[] = media + +files[] = includes/MediaBrowserInternet.inc +files[] = includes/MediaInternetBaseHandler.inc +files[] = includes/MediaInternetFileHandler.inc +files[] = includes/MediaInternetNoHandlerException.inc +files[] = includes/MediaInternetValidationException.inc +files[] = tests/media_internet.test + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_internet/media_internet.install b/sites/all/modules/media/modules/media_internet/media_internet.install new file mode 100644 index 000000000..03264d416 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/media_internet.install @@ -0,0 +1,21 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Media Internet module. + */ + +/** + * Implements hook_uninstall(). + */ +function media_internet_uninstall() { + // Remove variables. + variable_del('media_internet_fromurl_supported_schemes'); +} + +/** + * Rebuild the registry in order to accommodate moved classes. + */ +function media_internet_update_7000() { + registry_rebuild(); +} diff --git a/sites/all/modules/media/modules/media_internet/media_internet.media.inc b/sites/all/modules/media/modules/media_internet/media_internet.media.inc new file mode 100644 index 000000000..33f953964 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/media_internet.media.inc @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Media module integration for the Media internet module. + */ + +/** + * Implements hook_media_browser_plugin_info(). + */ +function media_internet_media_browser_plugin_info() { + $info['media_internet'] = array( + 'title' => t('Web'), + 'class' => 'MediaBrowserInternet', + ); + + return $info; +} + +/** + * Implements hook_media_internet_providers(). + * + * Provides a very basic handler which copies files from remote sources to the + * local files directory. + */ +function media_internet_media_internet_providers() { + return array( + 'MediaInternetFileHandler' => array( + 'title' => 'Files', + 'hidden' => TRUE, + // Make it go last. + 'weight' => 10000, + ), + ); +} diff --git a/sites/all/modules/media/modules/media_internet/media_internet.module b/sites/all/modules/media/modules/media_internet/media_internet.module new file mode 100644 index 000000000..d94b47926 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/media_internet.module @@ -0,0 +1,322 @@ +<?php + +/** + * Implements hook_hook_info(). + */ +function media_internet_hook_info() { + $hooks = array( + 'media_internet_providers', + ); + + return array_fill_keys($hooks, array('group' => 'media')); +} + +/** + * Implements hook_menu(). + */ +function media_internet_menu() { + $items['file/add/web'] = array( + 'title' => 'Web', + 'description' => 'Add internet files to your media library.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_internet_add_upload'), + 'access callback' => 'media_internet_access', + 'type' => MENU_LOCAL_TASK, + 'file' => 'file_entity.pages.inc', + 'file path' => drupal_get_path('module', 'file_entity'), + ); + + return $items; +} + +/** + * Access callback for the media_internet media browser plugin. + */ +function media_internet_access($account = NULL) { + return user_access('administer files', $account) || user_access('add media from remote sources', $account); +} + +/** + * Implement hook_permission(). + */ +function media_internet_permission() { + return array( + 'add media from remote sources' => array( + 'title' => t('Add media from remote services'), + 'description' => t('Add media from remote sources such as other websites, YouTube, etc'), + ), + ); +} + +/** + * Implements hook_theme(). + */ +function media_internet_theme() { + return array( + // media_internet.pages.inc. + 'media_internet_embed_help' => array( + 'variables' => array('description' => NULL, 'supported_providers' => NULL), + ), + ); +} + +/** + * Gets the list of Internet media providers. + * + * Each 'Provider' has a title and a class which can handle saving remote files. + * Providers are each given a turn at parsing a user-submitted URL or embed code + * and, if they recognize that it belongs to a service or protocol they support, + * they store a representation of it as a file object in file_managed. + * + * @return array + * An associative array of provider information keyed by provider name. + */ +function media_internet_get_providers() { + $providers = &drupal_static(__FUNCTION__); + + if (!isset($providers)) { + foreach (module_implements('media_internet_providers') as $module) { + foreach (module_invoke($module, 'media_internet_providers') as $class => $info) { + $providers[$class] = $info; + + // Store the name of the module which declared the provider. + $providers[$class]['module'] = $module; + + // Assign a default value to providers which don't specify a weight. + if (!isset($providers[$class]['weight'])) { + $providers[$class]['weight'] = 0; + } + } + } + + // Allow modules to alter the list of providers. + drupal_alter('media_internet_providers', $providers); + + // Sort the providers by weight. + uasort($providers, 'drupal_sort_weight'); + } + + return $providers; +} + +/** + * Finds the appropriate provider for a given URL or embed_string + * + * Each provider has a claim() method which it uses to tell media_internet + * that it should handle this input. We cycle through all providers to find + * the right one. + * + * @todo: Make this into a normal hook or something because we have to instantiate + * each class to test and that's not right. + */ +function media_internet_get_provider($embed_string) { + foreach (media_internet_get_providers() as $class_name => $nothing) { + $p = new $class_name($embed_string); + if ($p->claim($embed_string)) { + return $p; + } + } + throw new MediaInternetNoHandlerException(t('Unable to handle the provided embed string or URL.')); +} + +/** + * Returns HTML for help text based on supported internet media providers. + * + * @param $variables + * An associative array containing: + * - description: The normal description for this field, specified by the + * user. + * - supported_providers: A string of supported providers. + * + * @ingroup themeable + */ +function theme_media_internet_embed_help($variables) { + $description = $variables['description']; + $supported_providers = $variables['supported_providers']; + + $descriptions = array(); + + if (strlen($description)) { + $descriptions[] = $description; + } + if (!empty($supported_providers)) { + $descriptions[] = t('Supported internet media providers: !providers.', array('!providers' => '<strong>' . $supported_providers . '</strong>')); + } + + return implode('<br />', $descriptions); +} + +/** + * Implements hook_forms(). + */ +function media_internet_forms($form_id, $args) { + $forms = array(); + + // Create a copy of the upload wizard form for internet media. + if ($form_id == 'media_internet_add_upload') { + $forms[$form_id] = array( + 'callback' => 'file_entity_add_upload', + ); + } + + return $forms; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function media_internet_form_file_entity_add_upload_alter(&$form, &$form_state, $form_id) { + $step = $form['#step']; + $options = $form['#options']; + + // Swap the upload field for an embed field when on the first step of the web + // tab. + if ($form_id == 'media_internet_add_upload' && $step == 1) { + unset($form['upload']); + + $form['embed_code'] = array( + '#type' => 'textfield', + '#title' => t('File URL'), + '#description' => t('Enter a URL to a file.'), + '#attributes' => array('class' => array('media-add-from-url')), + // There is no standard specifying a maximum length for a URL. Internet + // Explorer supports up to 2083 (http://support.microsoft.com/kb/208427) + // so we assume publicly available media URLs are within this limit. + '#maxlength' => 2083, + '#required' => TRUE, + '#default_value' => isset($form_state['storage']['embed_code']) ? $form_state['storage']['embed_code'] : NULL, + ); + + // Create an array to hold potential Internet media providers. + $providers = array(); + + // Determine if there are any visible providers. + foreach (media_internet_get_providers() as $key => $provider) { + if (empty($provider['hidden']) || $provider['hidden'] != TRUE) { + $providers[] = check_plain($provider['title']); + } + } + + $form['#providers'] = $providers; + + // Notify the user of any available providers. + if ($providers) { + // If any providers are enabled it is assumed that some kind of embed is supported. + $form['embed_code']['#title'] = t('File URL or media resource'); + $form['embed_code']['#description'] = t('Enter a URL to a file or media resource. Many media providers also support identifying media via the embed code used to embed the media into external websites.'); + + $form['embed_code']['#description'] = theme('media_internet_embed_help', array('description' => $form['embed_code']['#description'], 'supported_providers' => implode(', ', $providers))); + } + + $form['#validators'] = array(); + + if (!empty($options['types'])) { + $form['#validators']['media_file_validate_types'] = array($options['types']); + } + + // Add validation and submission handlers to the form and ensure that they + // run first. + array_unshift($form['#validate'], 'media_internet_add_validate'); + array_unshift($form['#submit'], 'media_internet_add_submit'); + } +} + +/** + * Allow stream wrappers to have their chance at validation. + * + * Any module that implements hook_media_parse will have an + * opportunity to validate this. + * + * @see media_parse_to_uri() + */ +function media_internet_add_validate($form, &$form_state) { + // Supporting providers can now claim this input. It might be a URL, but it + // might be an embed code as well. + $embed_code = $form_state['values']['embed_code']; + + try { + $provider = media_internet_get_provider($embed_code); + $provider->validate(); + } + catch (MediaInternetNoHandlerException $e) { + form_set_error('embed_code', $e->getMessage()); + return; + } + catch (MediaInternetValidationException $e) { + form_set_error('embed_code', $e->getMessage()); + return; + } + + $validators = $form['#validators']; + $file = $provider->getFileObject(); + + if ($validators) { + try { + $file = $provider->getFileObject(); + } + catch (Exception $e) { + form_set_error('embed_code', $e->getMessage()); + return; + } + + // Check for errors. @see media_add_upload_validate calls file_save_upload(). + // this code is ripped from file_save_upload because we just want the validation part. + // Call the validation functions specified by this function's caller. + $errors = file_validate($file, $validators); + + if (!empty($errors)) { + $message = t('%url could not be added.', array('%url' => $embed_code)); + if (count($errors) > 1) { + $message .= theme('item_list', array('items' => $errors)); + } + else { + $message .= ' ' . array_pop($errors); + } + form_set_error('embed_code', $message); + return FALSE; + } + } + + // @TODO: Validate that if we have no $uri that this is a valid file to + // save. For instance, we may only be interested in images, and it would + // be helpful to let the user know they passed the HTML page containing + // the image accidentally. That would also save us from saving the file + // in the submit step. + + // This is kinda a hack of the same. + + // This should use the file_validate routines that the upload form users. + // We need to fix the media_parse_to_file routine to allow for a validation. +} + +/** + * Upload a file from a URL. + * + * This will copy a file from a remote location and store it locally. + * + * @see media_parse_to_uri() + * @see media_parse_to_file() + */ +function media_internet_add_submit($form, &$form_state) { + $embed_code = $form_state['values']['embed_code']; + + try { + // Save the remote file + $provider = media_internet_get_provider($embed_code); + // Providers decide if they need to save locally or somewhere else. + // This method returns a file object + $file = $provider->save(); + } + catch (Exception $e) { + form_set_error('embed_code', $e->getMessage()); + return; + } + + if (!$file->fid) { + form_set_error('embed_code', t('The file %file could not be saved. An unknown error has occurred.', array('%file' => $embed_code))); + return; + } + else { + $form_state['storage']['upload'] = $file->fid; + } +} diff --git a/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestHandler.inc b/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestHandler.inc new file mode 100644 index 000000000..764279caf --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestHandler.inc @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Extends the MediaInternetBaseHandler class to handle videos from an imaginary example.com. + */ + +/** + * Implementation of MediaInternetBaseHandler. + * + * @see hook_media_internet_providers(). + */ +class MediaInternetTestHandler extends MediaInternetBaseHandler { + public function parse($embedCode) { + // http://example.com/video/* + $patterns = array( + '@example\.com/video/(\d+)@i', + ); + + foreach ($patterns as $pattern) { + preg_match($pattern, $embedCode, $matches); + if (isset($matches[1])) { + return file_stream_wrapper_uri_normalize('mediainternettest://video/' . $matches[1]); + } + } + } + + public function claim($embedCode) { + if ($this->parse($embedCode)) { + return TRUE; + } + } + + public function getFileObject() { + $uri = $this->parse($this->embedCode); + $file = file_uri_to_object($uri, TRUE); + + // Override the default filename for testing purposes. + if (empty($file->fid)) { + $file->filename = 'Drupal'; + } + + return $file; + } +} diff --git a/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestStreamWrapper.inc b/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestStreamWrapper.inc new file mode 100644 index 000000000..202538389 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/tests/includes/MediaInternetTestStreamWrapper.inc @@ -0,0 +1,24 @@ +<?php + +/** + * @file + * Extends the MediaReadOnlyStreamWrapper class to handle videos from an imaginary example.com. + */ + +/** + * Create an instance like this: + * $media_internet_test = new MediaInternetTestStreamWrapper('mediainternettest://video/[video-code]'); + */ +class MediaInternetTestStreamWrapper extends MediaReadOnlyStreamWrapper { + protected $base_url = 'http://example.com'; + + static function getMimeType($uri, $mapping = NULL) { + return 'video/mediainternettest'; + } + + function interpolateUrl() { + if ($parameters = $this->get_parameters()) { + return $this->base_url . '/' . $parameters['video']; + } + } +} diff --git a/sites/all/modules/media/modules/media_internet/tests/media_internet.test b/sites/all/modules/media/modules/media_internet/tests/media_internet.test new file mode 100644 index 000000000..04b3243b6 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/tests/media_internet.test @@ -0,0 +1,394 @@ +<?php + +/** + * @file + * Tests for media_internet.module. + */ + +/** + * Provides methods specifically for testing Media Internet module's remote media handling. + */ +class MediaInternetTestHelper extends DrupalWebTestCase { + function setUp() { + // Since this is a base class for many test cases, support the same + // flexibility that DrupalWebTestCase::setUp() has for the modules to be + // passed in as either an array or a variable number of string arguments. + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'media_internet'; + parent::setUp($modules); + } + + /** + * Retrieves a sample file of the specified type. + */ + function getTestFile($type_name, $size = NULL) { + // Get a file to upload. + $file = current($this->drupalGetTestFiles($type_name, $size)); + + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + + return $file; + } + + /** + * Retrieves the fid of the last inserted file. + */ + function getLastFileId() { + return (int) db_query('SELECT MAX(fid) FROM {file_managed}')->fetchField(); + } + + /** + * Get a file from the database based on its filename. + * + * @param $filename + * A file filename, usually generated by $this->randomName(). + * @param $reset + * (optional) Whether to reset the internal file_load() cache. + * + * @return + * A file object matching $filename. + */ + function getFileByFilename($filename, $reset = FALSE) { + $files = file_load_multiple(array(), array('filename' => $filename), $reset); + // Load the first file returned from the database. + $returned_file = reset($files); + return $returned_file; + } + + protected function createFileType($overrides = array()) { + $type = new stdClass(); + $type->type = 'test'; + $type->label = "Test"; + $type->description = ''; + $type->mimetypes = array('image/jpeg', 'image/gif', 'image/png', 'image/tiff'); + + foreach ($overrides as $k => $v) { + $type->$k = $v; + } + + file_type_save($type); + return $type; + } +} + +/** + * Tests the media browser 'Web' tab. + */ +class MediaInternetBrowserWebTabTestCase extends MediaInternetTestHelper { + public static function getInfo() { + return array( + 'name' => 'Media browser web tab test', + 'description' => 'Tests the media browser web tab.', + 'group' => 'Media Internet', + ); + } + + function setUp() { + parent::setUp(); + + $web_user = $this->drupalCreateUser(array('access media browser', 'add media from remote sources')); + $this->drupalLogin($web_user); + } + + /** + * Tests that the views sorting works on the media browser 'Library' tab. + */ + function testMediaBrowserWebTab() { + // Load only the 'Library' tab of the media browser. + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_internet' => 'media_internet', + ), + ), + ); + + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + + // Check that the web tab is available and has an 'embed code' field. + $this->assertRaw(t('Web'), t('The web tab was found.')); + $this->assertFieldByName('embed_code', '', t('Embed code form field found.')); + } +} + +/** + * Test the default MediaInternetFileHandler provider. + */ +class MediaInternetRemoteFileTestCase extends MediaInternetTestHelper { + public static function getInfo() { + return array( + 'name' => 'Remote media file handler provider', + 'description' => 'Test the default remote file handler provider.', + 'group' => 'Media Internet', + ); + } + + function setUp() { + parent::setUp(); + + // Disable the private file system which is automatically enabled by + // DrupalTestCase so we can test the upload wizard correctly. + variable_del('file_private_path'); + + $web_user = $this->drupalCreateUser(array('create files', 'add media from remote sources')); + $this->drupalLogin($web_user); + } + + /** + * Tests the default remote file handler. + */ + function testRemoteFileHandling() { + // Step 1: Add a basic document file by providing a URL to the file. + $edit = array(); + $edit['embed_code'] = file_create_url('README.txt'); + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => 'Document', '%name' => $file->filename)), t('Document file uploaded.')); + } +} + +/** + * Tests custom media provider APIs. + */ +class MediaInternetProviderTestCase extends MediaInternetTestHelper { + public static function getInfo() { + return array( + 'name' => 'Custom media provider test', + 'description' => 'Tests the custom media provider APIs.', + 'group' => 'Media Internet', + ); + } + + function setUp() { + parent::setUp('media_internet_test'); + + // Disable the private file system which is automatically enabled by + // DrupalTestCase so we can test the upload wizard correctly. + variable_del('file_private_path'); + + // Enable media_internet_test.module's hook_media_internet_providers() + // implementation. + variable_set('media_internet_test_media_internet_providers', TRUE); + + $web_user = $this->drupalCreateUser(array('create files', 'view own private files', 'add media from remote sources')); + $this->drupalLogin($web_user); + } + + /** + * Test the basic file upload wizard functionality. + */ + function testMediaInternetCustomProviderWizardBasic() { + $this->drupalGet('file/add/web'); + $this->assertResponse(200); + + // Check that the provider is listed as supported. + $this->assertRaw(t('Supported internet media providers: !providers.', array('!providers' => '<strong>' . 'Media Internet Test' . '</strong>')), t('The example media provider is enabled.')); + + // Enable media_internet_test.module's + // hook_media_browser_plugin_info_alter_alter() implementation and ensure it + // is working as designed. + variable_set('media_internet_test_media_internet_providers_alter', TRUE); + + $this->drupalGet('file/add/web'); + $this->assertRaw(t('Supported internet media providers: !providers.', array('!providers' => '<strong>' . 'Altered provider title' . '</strong>')), t('The example media provider was successfully altered.')); + + // Step 1: Upload a basic video file. + $edit = array(); + $edit['embed_code'] = 'http://www.example.com/video/123'; + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => 'Video', '%name' => $file->filename)), t('Video file uploaded.')); + } + + /** + * Test the file upload wizard type step. + */ + function testMediaInternetCustomProviderWizardTypes() { + // Create multiple file types with the same mime types. + $this->createFileType(array('type' => 'video1', 'label' => 'Video 1', 'mimetypes' => array('video/mediainternettest'))); + $this->createFileType(array('type' => 'video2', 'label' => 'Video 2', 'mimetypes' => array('video/mediainternettest'))); + + // Step 1: Upload a basic video file. + $edit = array(); + $edit['embed_code'] = 'http://www.example.com/video/123'; + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Step 2: File type selection. + $edit = array(); + $edit['type'] = 'video2'; + $this->drupalPost(NULL, $edit, t('Next')); + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => 'Video 2', '%name' => $file->filename)), t('Video 2 file uploaded.')); + } + + /** + * Test the file upload wizard scheme step. + */ + function testMediaInternetCustomProviderWizardSchemes() { + // Enable the private file system. + variable_set('file_private_path', $this->private_files_directory); + + // Step 1: Upload a basic video file. + $edit = array(); + $edit['embed_code'] = 'http://www.example.com/video/123'; + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Step 3: Users should not be able to select a scheme for files with + // read-only stream wrappers. + $this->assertNoFieldByName('scheme'); + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => 'Video', '%name' => $file->filename)), t('Video file uploaded.')); + } + + /** + * Test the file upload wizard field step. + */ + function testMediaInternetCustomProviderWizardFields() { + $filename = $this->randomName(); + + // Add a text field to the video file type. + $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'text'); + field_create_field($field); + $instance = array( + 'field_name' => $field_name, + 'entity_type' => 'file', + 'bundle' => 'video', + 'label' => $this->randomName() . '_label', + ); + field_create_instance($instance); + + // Step 1: Upload a basic video file. + $edit = array(); + $edit['embed_code'] = 'http://www.example.com/video/123'; + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Step 4: Attached fields. + $edit = array(); + $edit['filename'] = $filename; + $edit[$field_name . '[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName(); + $this->drupalPost(NULL, $edit, t('Save')); + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => 'Video', '%name' => $filename)), t('Video file uploaded.')); + } + + /** + * Test skipping each of the file upload wizard steps. + */ + function testMediaInternetCustomProviderWizardStepSkipping() { + $filename = $this->randomName(); + + // Ensure that the file is affected by every step. + variable_set('file_private_path', $this->private_files_directory); + + $this->createFileType(array('type' => 'video1', 'label' => 'Video 1', 'mimetypes' => array('video/mediainternettest'))); + $this->createFileType(array('type' => 'video2', 'label' => 'Video 2', 'mimetypes' => array('video/mediainternettest'))); + + $field_name = drupal_strtolower($this->randomName() . '_field_name'); + $field = array('field_name' => $field_name, 'type' => 'text'); + field_create_field($field); + $instance = array( + 'field_name' => $field_name, + 'entity_type' => 'file', + 'bundle' => 'video2', + 'label' => $this->randomName() . '_label', + ); + field_create_instance($instance); + + // Test skipping each upload wizard step. + foreach (array('types', 'schemes', 'fields') as $step) { + // Step to skip. + switch ($step) { + case 'types': + variable_set('file_entity_file_upload_wizard_skip_file_type', TRUE); + break; + case 'schemes': + variable_set('file_entity_file_upload_wizard_skip_scheme', TRUE); + break; + case 'fields': + variable_set('file_entity_file_upload_wizard_skip_fields', TRUE); + break; + } + + // Step 1: Upload a basic video file. + $edit = array(); + $edit['embed_code'] = 'http://www.example.com/video/123'; + $this->drupalPost('file/add/web', $edit, t('Next')); + + // Step 2: File type selection. + if ($step != 'types') { + $edit = array(); + $edit['type'] = 'video2'; + $this->drupalPost(NULL, $edit, t('Next')); + } + + // Step 3: Users should not be able to select a scheme for files with + // read-only stream wrappers. + $this->assertNoFieldByName('scheme'); + + // Step 4: Attached fields. + if ($step != 'fields') { + // Skipping file type selection essentially skips this step as well + // because the file will not be assigned a type so no fields will be + // available. + if ($step != 'types') { + $edit = array(); + $edit['filename'] = $filename; + $edit[$field_name . '[' . LANGUAGE_NONE . '][0][value]'] = $this->randomName(); + $this->drupalPost(NULL, $edit, t('Save')); + } + } + + // Check that the file exists in the database. + $fid = $this->getLastFileId(); + $file = file_load($fid); + $this->assertTrue($file, t('File found in database.')); + + // Determine the file's file type. + $type = file_type_load($file->type); + + // Check that the video file has been uploaded. + $this->assertRaw(t('!type %name was uploaded.', array('!type' => $type->label, '%name' => $file->filename)), t('Video file uploaded.')); + + // Reset 'skip' variables. + variable_del('file_entity_file_upload_wizard_skip_file_type'); + variable_del('file_entity_file_upload_wizard_skip_scheme'); + variable_del('file_entity_file_upload_wizard_skip_fields'); + } + } +} diff --git a/sites/all/modules/media/modules/media_internet/tests/media_internet_test.info b/sites/all/modules/media/modules/media_internet/tests/media_internet_test.info new file mode 100644 index 000000000..09f831c40 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/tests/media_internet_test.info @@ -0,0 +1,15 @@ +name = Media Internet Test +description = Provides hooks for testing Media Internet module functionality. +package = Media +core = 7.x +hidden = TRUE + +files[] = includes/MediaInternetTestStreamWrapper.inc +files[] = includes/MediaInternetTestHandler.inc + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_internet/tests/media_internet_test.module b/sites/all/modules/media/modules/media_internet/tests/media_internet_test.module new file mode 100644 index 000000000..cfcd69a26 --- /dev/null +++ b/sites/all/modules/media/modules/media_internet/tests/media_internet_test.module @@ -0,0 +1,67 @@ +<?php + +/** + * @file + * Provides Media Internet module hook implementations for testing purposes. + */ + +/** + * Implements hook_media_internet_providers(). + */ +function media_internet_test_media_internet_providers() { + // Allow tests to enable or disable this hook. + if (!variable_get('media_internet_test_media_internet_providers', FALSE)) { + return array(); + } + + return array( + 'MediaInternetTestHandler' => array( + 'title' => t('Media Internet Test'), + ), + ); +} + +/** + * Implements hook_media_browser_plugin_info_alter_alter(). + */ +function media_internet_test_media_internet_providers_alter(&$providers) { + // Allow tests to enable or disable this hook. + if (!variable_get('media_internet_test_media_internet_providers_alter', FALSE)) { + return; + } + + $providers['MediaInternetTestHandler']['title'] = t('Altered provider title'); +} + +/** + * Implements hook_stream_wrappers(). + */ +function media_internet_test_stream_wrappers() { + return array( + 'mediainternettest' => array( + 'name' => t('Media Internet Test'), + 'class' => 'MediaInternetTestStreamWrapper', + 'description' => t('Media Internet Test.'), + 'type' => STREAM_WRAPPERS_READ_VISIBLE, + ), + ); +} + +/** + * Implements hook_media_parse(). + * + * @todo This hook should be deprecated. Refactor Media module to not call it + * any more, since media_internet should be able to automatically route to the + * appropriate handler. + */ +function media_internet_test_media_parse($embed_code) { + $handler = new MediaInternetTestHandler($embed_code); + return $handler->parse($embed_code); +} + + /** + * Implements hook_file_mimetype_mapping_alter(). + */ +function media_internet_test_file_mimetype_mapping_alter(&$mapping) { + $mapping['mimetypes'][] = 'video/mediainternettest'; +} diff --git a/sites/all/modules/media/modules/media_migrate_file_types/includes/media_migrate_file_types.pages.inc b/sites/all/modules/media/modules/media_migrate_file_types/includes/media_migrate_file_types.pages.inc new file mode 100644 index 000000000..78131e88e --- /dev/null +++ b/sites/all/modules/media/modules/media_migrate_file_types/includes/media_migrate_file_types.pages.inc @@ -0,0 +1,203 @@ +<?php + +/** + * @file + * Common pages for the Media Migrate File Types module. + */ + +/** + * File type migration page. + * + * Allows site administrator to execute migration of old/disabled/deleted + * file types to new ones. + */ +function media_migrate_file_types_upgrade_file_types($form, &$form_state) { + $migratable_types = _media_migrate_file_types_get_migratable_file_types(); + + // Silently return if there are no file types that need migration. + if (empty($migratable_types)) { + return array( + 'message' => array( + '#markup' => t('There are no file types that need migration. The Media Medigrate File Types module can now be safely <a href="@modules">disabled</a>.', array('@modules' => url('admin/modules'))), + ), + ); + } + + $form['message'] = array( + 'message' => array( + '#markup' => t('This page allows you to migrate deprecated and/or disabled file types to new ones. It will migrate files from old type to new one and optionally migrate fields and delete old type.'), + ), + ); + + $form['migrate_fields'] = array( + '#type' => 'checkbox', + '#title' => t('Migrate fields'), + '#default_value' => TRUE, + '#description' => t('Migrate fields and their values from old file types to new ones.'), + ); + $form['delete_old_type'] = array( + '#type' => 'checkbox', + '#title' => t('Delete old type'), + '#default_value' => FALSE, + '#description' => t('Delete old file type if migration was successful and delete operation is possible (type is not exported in code).'), + ); + $form['migrate_mimes'] = array( + '#type' => 'checkbox', + '#title' => t('Migrate type mime-type'), + '#default_value' => TRUE, + '#description' => t('Move mime-type from old type to new one.'), + ); + + $form['upgradable_types'] = array( + '#type' => 'fieldset', + '#title' => t('Upgradable file types'), + ); + + $options = array('- ' . t('Do not upgrade') . ' -'); + foreach (file_type_get_enabled_types() as $type) { + $options[$type->type] = $type->label; + } + + foreach ($migratable_types as $machine_name) { + $type = file_type_load($machine_name); + if (!$type) { + $type = new stdClass; + $type->label = $type->type = $machine_name; + } + $form['upgradable_types'][$machine_name] = array( + '#type' => 'select', + '#title' => $type->label, + '#options' => $options, + '#description' => t( + 'Select file type which you want to migrate @type to. Select %no_upgrade if type should stay as it is.', + array('@type' => $type->label, '%no_upgrade' => '- ' . t('Do not upgrade') . ' -')), + ); + } + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Start migration'), + ); + + return $form; +} + +/** + * File type migration page submit handler. + */ +function media_migrate_file_types_upgrade_file_types_submit($form, &$form_state) { + $migratable_types = _media_migrate_file_types_get_migratable_file_types(); + $migrate = FALSE; + foreach ($migratable_types as $type) { + if ($form_state['values'][$type]) { + $migrate = TRUE; + break; + } + } + + // Return silently if no types were selected for migration. + if (!$migrate) { + return; + } + + // Use confirmation page/form. + $query = $form_state['values']; + unset($query['op']); + unset($query['submit']); + unset($query['form_id']); + unset($query['form_token']); + unset($query['form_build_id']); + + $form_state['redirect'] = array( + 'admin/structure/file-types/upgrade/confirm', + array('query' => $query), + ); +} + +/** + * File types migration confirmation page. + */ +function media_migrate_file_types_upgrade_file_types_confirm($form, &$form_state) { + return confirm_form( + $form, + t('Do you really want to migrate selected file types?'), + 'admin/structure/file-types/upgrade', + NULL, + t('Migrate') + ); +} + +/** + * File types migration confirmation page submit. Executes actual migration. + */ +function media_migrate_file_types_upgrade_file_types_confirm_submit($form, &$form_state) { + $migratable_types = _media_migrate_file_types_get_migratable_file_types(); + foreach ($migratable_types as $type) { + if ($_GET[$type] && $bundle_new = file_type_load($_GET[$type])) { + // Old bundle might be deleted so let's fake some values. + $bundle_old = file_type_load($type); + if (empty($bundle_old)) { + $bundle_old = new stdClass; + $bundle_old->type = $type; + $bundle_old->mimetypes = array(); + $bundle_old->export_type = 2; + } + + // Migrate fields to new bundle. + if ($_GET['migrate_fields']) { + $old_fields = db_select('field_config_instance', 'fc')->fields('fc', array('field_name'))->condition('entity_type', 'file')->condition('bundle', $bundle_old->type)->execute()->fetchCol(); + $new_fields = db_select('field_config_instance', 'fc')->fields('fc', array('field_name'))->condition('entity_type', 'file')->condition('bundle', $bundle_new->type)->execute()->fetchCol(); + $fields_to_move = array_diff($old_fields, $new_fields); + $fields_to_drop = array_diff($old_fields, $fields_to_move); + + if (!empty($fields_to_move)) { + db_update('field_config_instance') + ->fields(array('bundle' => $bundle_new->type)) + ->condition('entity_type', 'file') + ->condition('bundle', $bundle_old->type) + ->condition('field_name', $fields_to_move, 'IN') + ->execute(); + } + + if (!empty($fields_to_drop)) { + db_delete('field_config_instance') + ->condition('entity_type', 'file') + ->condition('bundle', $bundle_old->type) + ->condition('field_name', $fields_to_drop, 'IN') + ->execute(); + } + + field_cache_clear(); + module_invoke_all('field_attach_rename_bundle', 'file', $bundle_old->type, $bundle_new->type); + } + + // Migrate mimetypes to new bundle. + if ($_GET['migrate_mimes']) { + $changed = FALSE; + foreach ($bundle_old->mimetypes as $mime) { + if (!file_entity_match_mimetypes($bundle_new->mimetypes, $mime)) { + $bundle_new->mimetypes[] = $mime; + $changed = TRUE; + } + } + + if ($changed) { + file_type_save($bundle_new); + } + } + + // Delete old bundle. + if ($_GET['delete_old_type'] && $bundle_old->export_type == 1) { + file_type_delete($bundle_old); + } + + // Migrate files. + db_update('file_managed') + ->fields(array('type' => $bundle_new->type)) + ->condition('type', $bundle_old->type) + ->execute(); + } + } + + $form_state['redirect'] = 'admin/structure/file-types'; +} diff --git a/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.info b/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.info new file mode 100644 index 000000000..b872ec514 --- /dev/null +++ b/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.info @@ -0,0 +1,16 @@ +name = Media Migrate File Types +description = Provides a UI for updating legacy media types with the new file types provided by File Entity. +package = Media +core = 7.x +hidden = TRUE + +dependencies[] = media + +configure = admin/structure/file-types/upgrade + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.module b/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.module new file mode 100644 index 000000000..c9c6d46c5 --- /dev/null +++ b/sites/all/modules/media/modules/media_migrate_file_types/media_migrate_file_types.module @@ -0,0 +1,73 @@ +<?php + +/** + * @file + * Primarily Drupal hooks. + */ + +/* + * Implements hook_system_info_alter() + */ +function media_migrate_file_types_system_info_alter(&$info, $file, $type) { + if ($type == 'module' && $file->name == 'media_migrate_file_types') { + $info['hidden'] = FALSE; + } +} + +/** + * Implements hook_menu(). + */ +function media_migrate_file_types_menu() { + $items['admin/structure/file-types/upgrade'] = array( + 'title' => 'Upgrade types', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_migrate_file_types_upgrade_file_types'), + 'access arguments' => array('administer file types'), + 'file' => 'includes/media_migrate_file_types.pages.inc', + 'type' => MENU_CALLBACK, + ); + $items['admin/structure/file-types/upgrade/confirm'] = array( + 'title' => 'Upgrade types', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_migrate_file_types_upgrade_file_types_confirm'), + 'access arguments' => array('administer file types'), + 'file' => 'includes/media_migrate_file_types.pages.inc', + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implements hook_help(). + */ +function media_migrate_file_types_help($path, $arg) { + switch ($path) { + case 'admin/structure/file-types': + if (_media_migrate_file_types_get_migratable_file_types()) { + drupal_set_message(t('There are disabled/deleted file types that can be migrated to their new alternatives. Visit <a href="!url">migration page</a> to get more information.', array('!url' => url('admin/structure/file-types/upgrade')))); + } + break; + } +} + +/** + * Checks if there are any files that belong to disabled or deleted file + * types. + * + * @return Array of file types (machine names) that are candidates for + * migration. + */ +function _media_migrate_file_types_get_migratable_file_types() { + $query = db_select('file_managed', 'f') + ->fields('f', array('type')) + ->distinct(); + $types = $query->execute()->fetchCol(); + + $enabled_types = array(); + foreach (file_type_get_enabled_types() as $type) { + $enabled_types[] = $type->type; + } + + return array_diff($types, $enabled_types); +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/css/media_wysiwyg.css b/sites/all/modules/media/modules/media_wysiwyg/css/media_wysiwyg.css new file mode 100644 index 000000000..7b6f0190c --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/css/media_wysiwyg.css @@ -0,0 +1,20 @@ +/** + * @file + * Styles for the format form. + * + * The display and layout of the Media browser assumes Drupal's Seven theme as + * the theme active when this is displayed. + */ + +#media-wysiwyg-format-form { + margin: 20px; +} + +#media-wysiwyg-format-form .media-item { + float: left; + margin-right: 10px; +} + +#media-wysiwyg-format-form .form-item-format label { + display: inline; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/images/wysiwyg-media.gif b/sites/all/modules/media/modules/media_wysiwyg/images/wysiwyg-media.gif Binary files differnew file mode 100644 index 000000000..495e71d37 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/images/wysiwyg-media.gif diff --git a/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.file_usage.inc b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.file_usage.inc new file mode 100644 index 000000000..cbc1e841b --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.file_usage.inc @@ -0,0 +1,179 @@ +<?php + +/** + * @file + * Functions related to the tracking the file usage of embedded media. + */ + +/** + * Implements hook_field_attach_insert(). + * + * Track file usage for media files included in formatted text. Note that this + * is heavy-handed, and should be replaced when Drupal's filter system is + * context-aware. + */ +function media_wysiwyg_field_attach_insert($entity_type, $entity) { + _media_wysiwyg_filter_add_file_usage_from_fields($entity_type, $entity); +} + +/** + * Implements hook_field_attach_update(). + * + * @see media_field_attach_insert(). + */ +function media_wysiwyg_field_attach_update($entity_type, $entity) { + _media_wysiwyg_filter_add_file_usage_from_fields($entity_type, $entity); +} + +/** + * Add file usage from file references in an entity's text fields. + */ +function _media_wysiwyg_filter_add_file_usage_from_fields($entity_type, $entity) { + // Track the total usage for files from all fields combined. + $entity_files = media_wysiwyg_entity_field_count_files($entity_type, $entity); + + list($entity_id, $entity_vid, $entity_bundle) = entity_extract_ids($entity_type, $entity); + + // When an entity has revisions and then is saved again NOT as new version the + // previous revision of the entity has be loaded to get the last known good + // count of files. The saved data is compared against the last version + // so that a correct file count can be created for that (the current) version + // id. This code may assume some things about entities that are only true for + // node objects. This should be reviewed. + // @TODO this conditional can probably be condensed + if (empty($entity->revision) && empty($entity->old_vid) && empty($entity->is_new) && ! empty($entity->original)) { + $old_files = media_wysiwyg_entity_field_count_files($entity_type, $entity->original); + foreach ($old_files as $fid => $old_file_count) { + // Were there more files on the node just prior to saving? + if (empty($entity_files[$fid])) { + $entity_files[$fid] = 0; + } + if ($old_file_count > $entity_files[$fid]) { + $deprecate = $old_file_count - $entity_files[$fid]; + // Now deprecate this usage + $file = file_load($fid); + if ($file) { + file_usage_delete($file, 'media', $entity_type, $entity_id, $deprecate); + } + // Usage is deleted, nothing more to do with this file + unset($entity_files[$fid]); + } + // There are the same number of files, nothing to do + elseif ($entity_files[$fid] == $old_file_count) { + unset($entity_files[$fid]); + } + // There are more files now, adjust the difference for the greater number. + // file_usage incrementing will happen below. + else { + // We just need to adjust what the file count will account for the new + // images that have been added since the increment process below will + // just add these additional ones in + $entity_files[$fid] = $entity_files[$fid] - $old_file_count; + } + } + } + + // Each entity revision counts for file usage. If versions are not enabled + // the file_usage table will have no entries for this because of the delete + // query above. + foreach ($entity_files as $fid => $entity_count) { + if ($file = file_load($fid)) { + file_usage_add($file, 'media', $entity_type, $entity_id, $entity_count); + } + } +} + +/** + * Parse file references from an entity's text fields and return them as an array. + */ +function media_wysiwyg_filter_parse_from_fields($entity_type, $entity) { + $file_references = array(); + + foreach (media_wysiwyg_filter_fields_with_text_filtering($entity_type, $entity) as $field_name) { + if ($field_items = field_get_items($entity_type, $entity, $field_name)) { + foreach ($field_items as $field_item) { + preg_match_all(MEDIA_WYSIWYG_TOKEN_REGEX, $field_item['value'], $matches); + foreach ($matches[0] as $tag) { + $tag = str_replace(array('[[', ']]'), '', $tag); + $tag_info = drupal_json_decode($tag); + if (isset($tag_info['fid']) && $tag_info['type'] == 'media') { + $file_references[] = $tag_info; + } + } + + preg_match_all(MEDIA_WYSIWYG_TOKEN_REGEX, $field_item['value'], $matches_alt); + foreach ($matches_alt[0] as $tag) { + $tag = urldecode($tag); + $tag_info = drupal_json_decode($tag); + if (isset($tag_info['fid']) && $tag_info['type'] == 'media') { + $file_references[] = $tag_info; + } + } + } + } + } + + return $file_references; +} + +/** + * Utility function to get the file count in this entity + * + * @param type $entity + * @param type $entity_type + * @return int + */ +function media_wysiwyg_entity_field_count_files($entity_type, $entity) { + $entity_files = array(); + foreach (media_wysiwyg_filter_parse_from_fields($entity_type, $entity) as $file_reference) { + if (empty($entity_files[$file_reference['fid']])) { + $entity_files[$file_reference['fid']] = 1; + } + else { + $entity_files[$file_reference['fid']]++; + } + } + return $entity_files; +} + +/** + * Implements hook_entity_delete(). + */ +function media_wysiwyg_entity_delete($entity, $type) { + list($entity_id) = entity_extract_ids($type, $entity); + + db_delete('file_usage') + ->condition('module', 'media') + ->condition('type', $type) + ->condition('id', $entity_id) + ->execute(); +} + +/** + * Implements hook_field_attach_delete_revision(). + * + * @param type $entity_type + * @param type $entity + */ +function media_wysiwyg_field_attach_delete_revision($entity_type, $entity) { + list($entity_id) = entity_extract_ids($entity_type, $entity); + $files = media_wysiwyg_entity_field_count_files($entity_type, $entity); + foreach ($files as $fid => $count) { + if ($file = file_load($fid)) { + file_usage_delete($file, 'media', $entity_type , $entity_id, $count); + } + } +} + +/** + * Implements hook_entity_dependencies(). + */ +function media_wysiwyg_entity_dependencies($entity, $entity_type) { + // Go through all the entity's text fields and add a dependency on any files + // that are referenced there. + $dependencies = array(); + foreach (media_wysiwyg_filter_parse_from_fields($entity_type, $entity) as $file_reference) { + $dependencies[] = array('type' => 'file', 'id' => $file_reference['fid']); + } + return $dependencies; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.filter.inc b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.filter.inc new file mode 100644 index 000000000..73d6b5ff4 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.filter.inc @@ -0,0 +1,372 @@ +<?php + +/** + * @file + * Functions related to the WYSIWYG editor and the media input filter. + */ + +define('MEDIA_WYSIWYG_TOKEN_REGEX', '/\[\[.*?\]\]/s'); + +/** + * Filter callback for media markup filter. + * + * @TODO check for security probably pass text through filter_xss + */ +function media_wysiwyg_filter($text) { + $text = preg_replace_callback(MEDIA_WYSIWYG_TOKEN_REGEX, 'media_wysiwyg_token_to_markup', $text); + return $text; +} + +/** + * Parses the contents of a CSS declaration block. + * + * @param string $declarations + * One or more CSS declarations delimited by a semicolon. The same as a CSS + * declaration block (see http://www.w3.org/TR/CSS21/syndata.html#rule-sets), + * but without the opening and closing curly braces. Also the same as the + * value of an inline HTML style attribute. + * + * @return array + * A keyed array. The keys are CSS property names, and the values are CSS + * property values. + */ +function media_wysiwyg_parse_css_declarations($declarations) { + $properties = array(); + foreach (array_map('trim', explode(";", $declarations)) as $declaration) { + if ($declaration != '') { + list($name, $value) = array_map('trim', explode(':', $declaration, 2)); + $properties[strtolower($name)] = $value; + } + } + return $properties; +} + +/** + * Replace callback to convert a media file tag into HTML markup. + * + * @param string $match + * Takes a match of tag code + * @param bool $wysiwyg + * Set to TRUE if called from within the WYSIWYG text area editor. + * + * @return string + * The HTML markup representation of the tag, or an empty string on failure. + * + * @see media_wysiwyg_get_file_without_label() + * @see hook_media_wysiwyg_token_to_markup_alter() + */ +function media_wysiwyg_token_to_markup($match, $wysiwyg = FALSE) { + static $recursion_stop; + $settings = array(); + $match = str_replace("[[", "", $match); + $match = str_replace("]]", "", $match); + $tag = $match[0]; + + try { + if (!is_string($tag)) { + throw new Exception('Unable to find matching tag'); + } + + $tag_info = drupal_json_decode($tag); + + if (!isset($tag_info['fid'])) { + throw new Exception('No file Id'); + } + + // Ensure the 'link_text' key is always defined. + if (!isset($tag_info['link_text'])) { + $tag_info['link_text'] = NULL; + } + + // Ensure a valid view mode is being requested. + if (!isset($tag_info['view_mode'])) { + $tag_info['view_mode'] = variable_get('media_wysiwyg_wysiwyg_default_view_mode', 'full'); + } + elseif ($tag_info['view_mode'] != 'default') { + $file_entity_info = entity_get_info('file'); + if (!in_array($tag_info['view_mode'], array_keys($file_entity_info['view modes']))) { + // Media 1.x defined some old view modes that have been superseded by + // more semantically named ones in File Entity. The media_update_7203() + // function updates field settings that reference the old view modes, + // but it's impractical to update all text content, so adjust + // accordingly here. + static $view_mode_updates = array( + 'media_preview' => 'preview', + 'media_small' => 'teaser', + 'media_large' => 'full', + ); + if (isset($view_mode_updates[$tag_info['view_mode']])) { + $tag_info['view_mode'] = $view_mode_updates[$tag_info['view_mode']]; + } + else { + throw new Exception('Invalid view mode'); + } + } + } + + $file = file_load($tag_info['fid']); + if (!$file) { + throw new Exception('Could not load media object'); + } + // Check if we've got a recursion. Happens because a file_load() may + // triggers file_entity_is_page() which then again triggers a file load. + if (isset($recursion_stop[$file->fid])) { + return ''; + } + $recursion_stop[$file->fid] = TRUE; + + $tag_info['file'] = $file; + + // The class attributes is a string, but drupal requires it to be + // an array, so we fix it here. + if (!empty($tag_info['attributes']['class'])) { + $tag_info['attributes']['class'] = explode(" ", $tag_info['attributes']['class']); + } + + // Grab the potentially overrided fields from the file. + $fields = media_wysiwyg_filter_field_parser($tag_info); + foreach ($fields as $key => $value) { + $file->{$key} = $value; + } + + $attributes = is_array($tag_info['attributes']) ? $tag_info['attributes'] : array(); + $attribute_whitelist = variable_get('media_wysiwyg_wysiwyg_allowed_attributes', _media_wysiwyg_wysiwyg_allowed_attributes_default()); + $settings['attributes'] = array_intersect_key($attributes, array_flip($attribute_whitelist)); + $settings['fields'] = $fields; + + if (!empty($tag_info['attributes']) && is_array($tag_info['attributes'])) { + $settings['attributes'] = array_intersect_key($tag_info['attributes'], array_flip($attribute_whitelist)); + $settings['fields'] = $fields; + + // Many media formatters will want to apply width and height independently + // of the style attribute or the corresponding HTML attributes, so pull + // these two out into top-level settings. Different WYSIWYG editors have + // different behavior with respect to whether they store user-specified + // dimensions in the HTML attributes or the style attribute - check both. + // Per http://www.w3.org/TR/html5/the-map-element.html#attr-dim-width, the + // HTML attributes are merely hints: CSS takes precedence. + if (isset($settings['attributes']['style'])) { + $css_properties = media_wysiwyg_parse_css_declarations($settings['attributes']['style']); + foreach (array('width', 'height') as $dimension) { + if (isset($css_properties[$dimension]) && substr($css_properties[$dimension], -2) == 'px') { + $settings[$dimension] = substr($css_properties[$dimension], 0, -2); + } + elseif (isset($settings['attributes'][$dimension])) { + $settings[$dimension] = $settings['attributes'][$dimension]; + } + } + } + } + } + catch (Exception $e) { + watchdog('media', 'Unable to render media from %tag. Error: %error', array('%tag' => $tag, '%error' => $e->getMessage())); + return ''; + } + + // If the tag has link text stored with it, override the filename with it for + // the rest of this function, so that if the file is themed as a link, the + // desired text will be used (see, for example, theme_file_link()). + // @todo: Try to find a less hacky way to do this. + if (isset($tag_info['link_text'])) { + // The link text will have characters such as "&" encoded for HTML, but the + // filename itself needs the raw value when it is used to build the link, + // in order to avoid double encoding. + $file->filename = decode_entities($tag_info['link_text']); + } + + if ($wysiwyg) { + $settings['wysiwyg'] = $wysiwyg; + // If sending markup to a WYSIWYG, we need to pass the file information so + // that an inline macro can be generated when the WYSIWYG is detached. + // The WYSIWYG plugin is expecting this information in the + // Drupal.settings.mediaDataMap variable. + $element = media_wysiwyg_get_file_without_label($file, $tag_info['view_mode'], $settings); + $data = array( + 'type' => 'media', + 'fid' => $file->fid, + 'view_mode' => $tag_info['view_mode'], + 'link_text' => $tag_info['link_text'], + ); + drupal_add_js(array('mediaDataMap' => array($file->fid => $data)), 'setting'); + $element['#attributes']['data-fid'] = $file->fid; + $element['#attributes']['data-media-element'] = '1'; + $element['#attributes']['class'][] = 'media-element'; + } + else { + // Display the field elements. + $element = array(); + // Render the file entity, for sites using the file_entity rendering method. + if (variable_get('media_wysiwyg_default_render', 'file_entity') == 'file_entity') { + $element['content'] = file_view($file, $tag_info['view_mode']); + } + $element['content']['file'] = media_wysiwyg_get_file_without_label($file, $tag_info['view_mode'], $settings); + // Overwrite or set the file #alt attribute if it has been set in this + // instance. + if (!empty($element['content']['file']['#attributes']['alt'])) { + $element['content']['file']['#alt'] = $element['content']['file']['#attributes']['alt']; + } + // Overwrite or set the file #title attribute if it has been set in this + // instance. + if (!empty($element['content']['file']['#attributes']['title'])) { + $element['content']['file']['#title'] = $element['content']['file']['#attributes']['title']; + } + // For sites using the legacy field_attach rendering method, attach fields. + if (variable_get('media_wysiwyg_default_render', 'file_entity') == 'field_attach') { + field_attach_prepare_view('file', array($file->fid => $file), $tag_info['view_mode']); + entity_prepare_view('file', array($file->fid => $file)); + $element['content'] += field_attach_view('file', $file, $tag_info['view_mode']); + } + if (count(element_children($element['content'])) > 1) { + // Add surrounding divs to group them together. + // We dont want divs when there are no additional fields to allow files + // to display inline with text, without breaking p tags. + $element['content']['#type'] = 'container'; + $element['content']['#attributes']['class'] = array( + 'media', + 'media-element-container', + 'media-' . $element['content']['file']['#view_mode'] + ); + } + + // Conditionally add a pre-render if the media filter output is be cached. + $filters = filter_get_filters(); + if (!isset($filters['media_filter']['cache']) || $filters['media_filter']['cache']) { + $element['#pre_render'][] = 'media_wysiwyg_pre_render_cached_filter'; + } + } + drupal_alter('media_wysiwyg_token_to_markup', $element, $tag_info, $settings); + $output = drupal_render($element); + unset($recursion_stop[$file->fid]); + return $output; +} + +/** + * Parse the field array from the collapsed AJAX string. + */ +function media_wysiwyg_filter_field_parser($tag_info) { + $fields = array(); + if (isset($tag_info['fields'])) { + foreach($tag_info['fields'] as $field_name => $field_value) { + if (strpos($field_name, 'field_') === 0) { + $parsed_field = explode('[', str_replace(']', '', $field_name)); + $ref = &$fields; + + // Each key of the field needs to be the child of the previous key. + foreach ($parsed_field as $key) { + if (!isset($ref[$key])) { + $ref[$key] = array(); + } + $ref = &$ref[$key]; + } + + // The value should be set at the deepest level. + // Fields that use rich-text markup will be urlencoded. + $ref = urldecode($field_value); + } + } + } + return $fields; +} + +/** + * Creates map of inline media tags. + * + * Generates an array of [inline tags] => <html> to be used in filter + * replacement and to add the mapping to JS. + * + * @param string $text + * The String containing text and html markup of textarea + * + * @return array + * An associative array with tag code as key and html markup as the value. + * + * @see media_process_form() + * @see media_token_to_markup() + */ +function _media_wysiwyg_generate_tagMap($text) { + // Making $tagmap static as this function is called many times and + // adds duplicate markup for each tag code in Drupal.settings JS, + // so in media_process_form it adds something like tagCode:<markup>, + // <markup> and when we replace in attach see two duplicate images + // for one tagCode. Making static would make function remember value + // between function calls. Since media_process_form is multiple times + // with same form, this function is also called multiple times. + static $tagmap = array(); + preg_match_all("/\[\[.*?\]\]/s", $text, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + // We see if tagContent is already in $tagMap, if not we add it + // to $tagmap. If we return an empty array, we break embeddings of the same + // media multiple times. + if (empty($tagmap[$match[0]])) { + // @TODO: Total HACK, but better than nothing. + // We should find a better way of cleaning this up. + if ($markup_for_media = media_wysiwyg_token_to_markup($match, TRUE)) { + $tagmap[$match[0]] = $markup_for_media; + } + else { + $missing = file_create_url(drupal_get_path('module', 'media') . '/images/icons/default/image-x-generic.png'); + $tagmap[$match[0]] = '<div><img src="' . $missing . '" width="100px" height="100px"/></div>'; + } + } + } + return $tagmap; +} + +/** + * Return a list of view modes allowed for a file embedded in the WYSIWYG. + * + * @param object $file + * A file entity. + * + * @return array + * An array of view modes that can be used on the file when embedded in the + * WYSIWYG. + */ +function media_wysiwyg_get_wysiwyg_allowed_view_modes($file) { + $enabled_view_modes = &drupal_static(__FUNCTION__, array()); + + // @todo Add more caching for this. + if (!isset($enabled_view_modes[$file->type])) { + $enabled_view_modes[$file->type] = array(); + + // Add the default view mode by default. + $enabled_view_modes[$file->type]['default'] = array('label' => t('Default'), 'custom settings' => TRUE); + + $entity_info = entity_get_info('file'); + $view_mode_settings = field_view_mode_settings('file', $file->type); + foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) { + // Do not show view modes that don't have their own settings and will + // only fall back to the default view mode. + if (empty($view_mode_settings[$view_mode]['custom_settings'])) { + continue; + } + + // Don't present the user with an option to choose a view mode in which + // the file is hidden. + $extra_fields = field_extra_fields_get_display('file', $file->type, $view_mode); + if (empty($extra_fields['file']['visible'])) { + continue; + } + + // Add the view mode to the list of enabled view modes. + $enabled_view_modes[$file->type][$view_mode] = $view_mode_info; + } + } + + $view_modes = $enabled_view_modes[$file->type]; + drupal_alter('media_wysiwyg_wysiwyg_allowed_view_modes', $view_modes, $file); + return $view_modes; +} + +/** + * #pre_render callback: Modify the element if the render cache is filtered. + */ +function media_wysiwyg_pre_render_cached_filter($element) { + // Remove contextual links since they are not compatible with cached filtered + // text. + if (isset($element['content']['#contextual_links'])) { + unset($element['content']['#contextual_links']); + } + + return $element; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.pages.inc b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.pages.inc new file mode 100644 index 000000000..72653b8ff --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.pages.inc @@ -0,0 +1,111 @@ +<?php + +/** + * @file + * Common pages for the Media WYSIWYG module. + */ + +/** + * Form callback used when embedding media. + * + * Allows the user to pick a format for their media file. + * Can also have additional params depending on the media type. + */ +function media_wysiwyg_format_form($form, &$form_state, $file) { + $form_state['file'] = $file; + + // Allow for overrides to the fields. + $query_fields = isset($_GET['fields']) ? drupal_json_decode($_GET['fields']) : array(); + $fields = media_wysiwyg_filter_field_parser(array('fields' => $query_fields), $file); + + $view_modes = media_wysiwyg_get_wysiwyg_allowed_view_modes($file); + $formats = $options = array(); + foreach ($view_modes as $view_mode => $view_mode_info) { + // @TODO: Display more verbose information about which formatter and what it + // does. + $options[$view_mode] = $view_mode_info['label']; + $element = media_wysiwyg_get_file_without_label($file, $view_mode, array('wysiwyg' => TRUE)); + + // Make a pretty name out of this. + $formats[$view_mode] = drupal_render($element); + } + + // Add the previews back into the form array so they can be altered. + $form['#formats'] = &$formats; + + if (!count($formats)) { + throw new Exception('Unable to continue, no available formats for displaying media.'); + return; + } + + // Allow for overrides to the display format. + $default_view_mode = is_array($query_fields) && isset($query_fields['format']) ? $query_fields['format'] : variable_get('media_wysiwyg_wysiwyg_default_view_mode', 'full'); + if (!isset($formats[$default_view_mode])) { + $default_view_mode = key($formats); + } + + // Add the previews by reference so that they can easily be altered by + // changing $form['#formats']. + $settings['media']['formatFormFormats'] = &$formats; + $form['#attached']['js'][] = array('data' => $settings, 'type' => 'setting'); + + // Add the required libraries, JavaScript and CSS for the form. + $form['#attached']['library'][] = array('media', 'media_base'); + $form['#attached']['library'][] = array('system', 'form'); + $form['#attached']['css'][] = drupal_get_path('module', 'media_wysiwyg') . '/css/media_wysiwyg.css'; + $form['#attached']['js'][] = drupal_get_path('module', 'media_wysiwyg') . '/js/media_wysiwyg.format_form.js'; + + $form['title'] = array( + '#markup' => t('Embedding %filename', array('%filename' => $file->filename)), + ); + + $preview = media_get_thumbnail_preview($file); + + $form['preview'] = array( + '#type' => 'markup', + '#title' => check_plain(basename($file->uri)), + '#markup' => drupal_render($preview), + ); + + // These will get passed on to WYSIWYG. + $form['options'] = array( + '#type' => 'fieldset', + '#title' => t('options'), + ); + + $form['options']['format'] = array( + '#type' => 'select', + '#title' => t('Display as'), + '#options' => $options, + '#default_value' => $default_view_mode, + '#description' => t('Choose the type of display you would like for this + file. Please be aware that files may display differently than they do when + they are inserted into an editor.') + ); + + // Add fields from the file, so that we can override them if necessary. + $form['options']['fields'] = array(); + foreach ($fields as $field_name => $field_value) { + $file->{$field_name} = $field_value; + } + field_attach_form('file', $file, $form['options']['fields'], $form_state); + $instance = field_info_instances('file', $file->type); + foreach ($instance as $field_name => $field_value) { + if (isset($instance[$field_name]['settings']) && isset($instance[$field_name]['settings']['wysiwyg_override']) && !$instance[$field_name]['settings']['wysiwyg_override']) { + unset($form['options']['fields'][$field_name]); + } + } + + // Similar to a form_alter, but we want this to run first so that + // media.types.inc can add the fields specific to a given type (like alt tags + // on media). If implemented as an alter, this might not happen, making other + // alters not be able to work on those fields. + // @todo: We need to pass in existing values for those attributes. + drupal_alter('media_wysiwyg_format_form_prepare', $form, $form_state, $file); + + if (!element_children($form['options'])) { + $form['options']['#attributes'] = array('style' => 'display:none'); + } + + return $form; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.uuid.inc b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.uuid.inc new file mode 100644 index 000000000..3d228e3d0 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/includes/media_wysiwyg.uuid.inc @@ -0,0 +1,88 @@ +<?php + +/** + * @file + * Functions related to adding UUID support to embedded media. + */ + +/** + * Implements hook_entity_uuid_load(). + */ +function media_wysiwyg_entity_uuid_load(&$entities, $entity_type) { + // Go through all the entity's text fields and replace file IDs in media + // tokens with the corresponding UUID. + foreach ($entities as $entity) { + media_wysiwyg_filter_replace_tokens_in_all_text_fields($entity_type, $entity, 'media_wysiwyg_token_fid_to_uuid'); + } +} + +/** + * Implements hook_entity_uuid_presave(). + */ +function media_wysiwyg_entity_uuid_presave(&$entity, $entity_type) { + // Go through all the entity's text fields and replace UUIDs in media tokens + // with the corresponding file ID. + media_wysiwyg_filter_replace_tokens_in_all_text_fields($entity_type, $entity, 'media_wysiwyg_token_uuid_to_fid'); +} + +/** + * Replaces media tokens in an entity's text fields, using the specified callback function. + */ +function media_wysiwyg_filter_replace_tokens_in_all_text_fields($entity_type, $entity, $callback) { + $text_field_names = media_wysiwyg_filter_fields_with_text_filtering($entity_type, $entity); + foreach ($text_field_names as $field_name) { + if (!empty($entity->{$field_name})) { + $field = field_info_field($field_name); + $all_languages = field_available_languages($entity_type, $field); + $field_languages = array_intersect($all_languages, array_keys($entity->{$field_name})); + foreach ($field_languages as $language) { + if (!empty($entity->{$field_name}[$language])) { + foreach ($entity->{$field_name}[$language] as &$item) { + $item['value'] = preg_replace_callback(MEDIA_WYSIWYG_TOKEN_REGEX, $callback, $item['value']); + } + } + } + } + } +} + +/** + * Callback to replace file IDs with UUIDs in a media token. + */ +function media_wysiwyg_token_fid_to_uuid($matches) { + return _media_wysiwyg_token_uuid_replace($matches, 'entity_get_uuid_by_id'); +} + +/** + * Callback to replace UUIDs with file IDs in a media token. + */ +function media_wysiwyg_token_uuid_to_fid($matches) { + return _media_wysiwyg_token_uuid_replace($matches, 'entity_get_id_by_uuid'); +} + +/** + * Helper function to replace UUIDs with file IDs or vice versa. + * + * @param array $matches + * An array of matches for media tokens, from a preg_replace_callback() + * callback function. + * @param string $entity_uuid_function + * Either 'entity_get_uuid_by_id' (to replace file IDs with UUIDs in the + * token) or 'entity_get_id_by_uuid' (to replace UUIDs with file IDs). + * + * @return string + * A string representing the JSON-encoded token, with the appropriate + * replacement between file IDs and UUIDs. + */ +function _media_wysiwyg_token_uuid_replace($matches, $entity_uuid_function) { + $tag = $matches[0]; + $tag = str_replace(array('[[', ']]'), '', $tag); + $tag_info = drupal_json_decode($tag); + if (isset($tag_info['fid'])) { + if ($new_ids = $entity_uuid_function('file', array($tag_info['fid']))) { + $new_id = reset($new_ids); + $tag_info['fid'] = $new_id; + } + } + return '[[' . drupal_json_encode($tag_info) . ']]'; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.filter.js b/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.filter.js new file mode 100644 index 000000000..efccd59c0 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.filter.js @@ -0,0 +1,287 @@ +/** + * @file + * File with utilities to handle media in html editing. + */ +(function ($) { + + Drupal.media = Drupal.media || {}; + /** + * Utility to deal with media tokens / placeholders. + */ + Drupal.media.filter = { + /** + * Replaces media tokens with the placeholders for html editing. + * @param content + */ + replaceTokenWithPlaceholder: function(content) { + Drupal.media.filter.ensure_tagmap(); + var matches = content.match(/\[\[.*?\]\]/g); + + if (matches) { + for (var i = 0; i < matches.length; i++) { + var match = matches[i]; + if (match.indexOf('"type":"media"') == -1) { + continue; + } + + // Check if the macro exists in the tagmap. This ensures backwards + // compatibility with existing media and is moderately more efficient + // than re-building the element. + var media = Drupal.settings.tagmap[match]; + var media_json = match.replace('[[', '').replace(']]', ''); + + // Ensure that the media JSON is valid. + try { + var media_definition = JSON.parse(media_json); + } + catch (err) { + // @todo: error logging. + // Content should be returned to prevent an empty editor. + return content; + } + + // Re-build the media if the macro has changed from the tagmap. + if (!media && media_definition.fid) { + Drupal.media.filter.ensureSourceMap(); + var source = Drupal.settings.mediaSourceMap[media_definition.fid]; + media = document.createElement(source.tagName); + media.src = source.src; + media.innerHTML = source.innerHTML; + } + + // Apply attributes. + var element = Drupal.media.filter.create_element(media, media_definition); + var markup = Drupal.media.filter.outerHTML(element); + + // Use split and join to replace all instances of macro with markup. + content = content.split(match).join(markup); + } + } + + return content; + }, + + /** + * Replaces media elements with tokens. + * + * @param content (string) + * The markup within the wysiwyg instance. + */ + replacePlaceholderWithToken: function(content) { + Drupal.media.filter.ensure_tagmap(); + + // Rewrite the tagmap in case any of the macros have changed. + Drupal.settings.tagmap = {}; + + // Replace all media placeholders with their JSON macro representations. + // + // There are issues with using jQuery to parse the WYSIWYG content (see + // http://drupal.org/node/1280758), and parsing HTML with regular + // expressions is a terrible idea (see http://stackoverflow.com/a/1732454/854985) + // + // WYSIWYG editors act wacky with complex placeholder markup anyway, so an + // image is the most reliable and most usable anyway: images can be moved by + // dragging and dropping, and can be resized using interactive handles. + // + // Media requests a WYSIWYG place holder rendering of the file by passing + // the wysiwyg => 1 flag in the settings array when calling + // media_get_file_without_label(). + // + // Finds the media-element class. + var classRegex = 'class=[\'"][^\'"]*?media-element'; + // Image tag with the media-element class. + var regex = '<img[^>]+' + classRegex + '[^>]*?>'; + // Or a span with the media-element class (used for documents). + // \S\s catches any character, including a linebreak; JavaScript does not + // have a dotall flag. + regex += '|<span[^>]+' + classRegex + '[^>]*?>[\\S\\s]+?</span>'; + var matches = content.match(RegExp(regex, 'gi')); + if (matches) { + for (i = 0; i < matches.length; i++) { + markup = matches[i]; + macro = Drupal.media.filter.create_macro($(markup)); + Drupal.settings.tagmap[macro] = markup; + content = content.replace(markup, macro); + } + } + + return content; + }, + + /** + * Serializes file information as a url-encoded JSON object and stores it + * as a data attribute on the html element. + * + * @param html (string) + * A html element to be used to represent the inserted media element. + * @param info (object) + * A object containing the media file information (fid, view_mode, etc). + */ + create_element: function (html, info) { + if ($('<div>').append(html).text().length === html.length) { + // Element is not an html tag. Surround it in a span element so we can + // pass the file attributes. + html = '<span>' + html + '</span>'; + } + var element = $(html); + + // Parse out link wrappers. They will be re-applied when the image is + // rendered on the front-end. + if (element.is('a')) { + element = element.children(); + } + + // Move attributes from the file info array to the placeholder element. + if (info.attributes) { + $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { + if (info.attributes[a]) { + element.attr(a, info.attributes[a]); + } + }); + delete(info.attributes); + + // Store information to rebuild the element later, if necessary. + Drupal.media.filter.ensureSourceMap(); + Drupal.settings.mediaSourceMap[info.fid] = { + tagName: element[0].tagName, + src: element[0].src, + innerHTML: element[0].innerHTML + } + } + + info.type = info.type || "media"; + + // Store the data in the data map. + Drupal.media.filter.ensureDataMap(); + Drupal.settings.mediaDataMap[info.fid] = info; + + // Store the fid in the DOM to retrieve the data from the info map. + element.attr('data-fid', info.fid); + + // Add data-media-element attribute so we can find the markup element later. + element.attr('data-media-element', '1') + + var classes = ['media-element']; + if (info.view_mode) { + classes.push('file-' + info.view_mode.replace(/_/g, '-')); + } + element.addClass(classes.join(' ')); + + // Apply link_text if present. + if (info.link_text) { + $('a', element).html(info.link_text); + } + + return element; + }, + + /** + * Create a macro representation of the inserted media element. + * + * @param element (jQuery object) + * A media element with attached serialized file info. + */ + create_macro: function (element) { + var file_info = Drupal.media.filter.extract_file_info(element); + if (file_info) { + if (typeof file_info.link_text == 'string') { + // Make sure the link_text-html-tags are properly escaped. + file_info.link_text = file_info.link_text.replace(/</g, '<').replace(/>/g, '>'); + } + return '[[' + JSON.stringify(file_info) + ']]'; + } + return false; + }, + + /** + * Extract the file info from a WYSIWYG placeholder element as JSON. + * + * @param element (jQuery object) + * A media element with associated file info via a file id (fid). + */ + extract_file_info: function (element) { + var fid, file_info, value; + + if (fid = element.data('fid')) { + Drupal.media.filter.ensureDataMap(); + + if (file_info = Drupal.settings.mediaDataMap[fid]) { + file_info.attributes = {}; + + $.each(Drupal.settings.media.wysiwyg_allowed_attributes, function(i, a) { + if (value = element.attr(a)) { + // Replace " by \" to avoid error with JSON format. + if (typeof value == 'string') { + value = value.replace('"', '\\"'); + } + file_info.attributes[a] = value; + } + }); + + // Extract the link text, if there is any. + file_info.link_text = element.find('a').html(); + } + } + + return file_info; + }, + + /** + * Gets the HTML content of an element. + * + * @param element (jQuery object) + */ + outerHTML: function (element) { + return element[0].outerHTML || $('<div>').append(element.eq(0).clone()).html(); + }, + + /** + * Gets the wrapped HTML content of an element to insert into the wysiwyg. + * + * It also registers the element in the tag map so that the token + * replacement works. + * + * @param element (jQuery object) The element to insert. + * + * @see Drupal.media.filter.replacePlaceholderWithToken() + */ + getWysiwygHTML: function (element) { + // Create the markup and the macro. + var markup = Drupal.media.filter.outerHTML(element), + macro = Drupal.media.filter.create_macro(element); + + // Store macro/markup in the tagmap. + Drupal.media.filter.ensure_tagmap(); + Drupal.settings.tagmap[macro] = markup; + + // Return the html code to insert in an editor and use it with + // replacePlaceholderWithToken() + return markup; + }, + + /** + * Ensures the src tracking has been initialized and returns it. + */ + ensureSourceMap: function() { + Drupal.settings.mediaSourceMap = Drupal.settings.mediaSourceMap || {}; + return Drupal.settings.mediaSourceMap; + }, + + /** + * Ensures the data tracking has been initialized and returns it. + */ + ensureDataMap: function() { + Drupal.settings.mediaDataMap = Drupal.settings.mediaDataMap || {}; + return Drupal.settings.mediaDataMap; + }, + + /** + * Ensures the tag map has been initialized and returns it. + */ + ensure_tagmap: function () { + Drupal.settings.tagmap = Drupal.settings.tagmap || {}; + return Drupal.settings.tagmap; + } + } + +})(jQuery); diff --git a/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.format_form.js b/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.format_form.js new file mode 100644 index 000000000..4345a345f --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/js/media_wysiwyg.format_form.js @@ -0,0 +1,69 @@ + +/** + * @file + * Attach behaviors to formatter radio select when selecting a media's display + * formatter. + */ + +(function ($) { +namespace('Drupal.media.formatForm'); + +Drupal.media.mediaFormatSelected = {}; + +Drupal.behaviors.mediaFormatForm = { + attach: function (context, settings) { + // Add the "Submit" button inside the IFRAME that trigger the behavior of + // the hidden "OK" button that is outside the IFRAME. + // @see Drupal.media.browser.validateButtons() for more details. + + // @note I think this should be handled in media.browser.js in + // Drupal.media.browser.validateButtons but I'm not sure how crufty this + // particular functionality is. We should evaluate if it is still needed. + + // @TODO can these be added to the content being displayed via form_alter? + + // Adding the buttons should only be done once in order to prevent multiple + // buttons from being added if part of the form is updated via AJAX + $('#media-wysiwyg-format-form').once('format', function() { + $('<a class="button fake-ok">' + Drupal.t('Submit') + '</a>').appendTo($('#media-wysiwyg-format-form')).bind('click', Drupal.media.formatForm.submit); + }); + } +}; + +Drupal.media.formatForm.getOptions = function () { + // Get all the values + var ret = {}; + + $.each($('#media-wysiwyg-format-form .fieldset-wrapper *').serializeArray(), function (i, field) { + ret[field.name] = field.value; + + // When a field uses a WYSIWYG format, the value needs to be extracted. + if (field.name.match(/\[format\]/i)) { + field.name = field.name.replace(/\[format\]/i, '[value]'); + field.key = 'edit-' + field.name.replace(/[_\[]/g, '-').replace(/[\]]/g, ''); + + if (Drupal.wysiwyg && Drupal.wysiwyg.instances[field.key]) { + // Retrieve the content from the WYSIWYG instance. + ret[field.name] = Drupal.wysiwyg.instances[field.key].getContent(); + + // Encode the content to play nicely within JSON. + ret[field.name] = encodeURIComponent(ret[field.name]); + } + } + }); + + return ret; +}; + +Drupal.media.formatForm.getFormattedMedia = function () { + var formatType = $("#edit-format").val(); + return { type: formatType, options: Drupal.media.formatForm.getOptions(), html: Drupal.settings.media.formatFormFormats[formatType] }; +}; + +Drupal.media.formatForm.submit = function () { + // @see Drupal.behaviors.mediaFormatForm.attach(). + var buttons = $(parent.window.document.body).find('#mediaStyleSelector').parent('.ui-dialog').find('.ui-dialog-buttonpane button'); + buttons[0].click(); +} + +})(jQuery); diff --git a/sites/all/modules/media/modules/media_wysiwyg/js/wysiwyg-media.js b/sites/all/modules/media/modules/media_wysiwyg/js/wysiwyg-media.js new file mode 100644 index 000000000..d694b85b0 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/js/wysiwyg-media.js @@ -0,0 +1,186 @@ + +/** + * @file + * Attach Media WYSIWYG behaviors. + */ + +(function ($) { + +Drupal.media = Drupal.media || {}; + +/** + * Register the plugin with WYSIWYG. + */ +Drupal.wysiwyg.plugins.media = { + + /** + * Determine whether a DOM element belongs to this plugin. + * + * @param node + * A DOM element + */ + isNode: function(node) { + return $(node).is('img[data-media-element]'); + }, + + /** + * Execute the button. + * + * @param data + * An object containing data about the current selection: + * - format: 'html' when the passed data is HTML content, 'text' when the + * passed data is plain-text content. + * - node: When 'format' is 'html', the focused DOM element in the editor. + * - content: The textual representation of the focused/selected editor + * content. + * @param settings + * The plugin settings, as provided in the plugin's PHP include file. + * @param instanceId + * The ID of the current editor instance. + */ + invoke: function (data, settings, instanceId) { + if (data.format == 'html') { + var insert = new InsertMedia(instanceId); + if (this.isNode(data.node)) { + // Change the view mode for already-inserted media. + var media_file = Drupal.media.filter.extract_file_info($(data.node)); + insert.onSelect([media_file]); + } + else { + // Insert new media. + insert.prompt(settings.global); + } + } + }, + + /** + * Attach function, called when a rich text editor loads. + * This finds all [[tags]] and replaces them with the html + * that needs to show in the editor. + * + * This finds all JSON macros and replaces them with the HTML placeholder + * that will show in the editor. + */ + attach: function (content, settings, instanceId) { + content = Drupal.media.filter.replaceTokenWithPlaceholder(content); + return content; + }, + + /** + * Detach function, called when a rich text editor detaches + */ + detach: function (content, settings, instanceId) { + content = Drupal.media.filter.replacePlaceholderWithToken(content); + return content; + } +}; +/** + * Defining InsertMedia object to manage the sequence of actions involved in + * inserting a media element into the WYSIWYG. + * Keeps track of the WYSIWYG instance id. + */ +var InsertMedia = function (instance_id) { + this.instanceId = instance_id; + return this; +}; + +InsertMedia.prototype = { + /** + * Prompt user to select a media item with the media browser. + * + * @param settings + * Settings object to pass on to the media browser. + * TODO: Determine if this is actually necessary. + */ + prompt: function (settings) { + Drupal.media.popups.mediaBrowser($.proxy(this, 'onSelect'), settings); + }, + + /** + * On selection of a media item, display item's display configuration form. + */ + onSelect: function (media_files) { + this.mediaFile = media_files[0]; + Drupal.media.popups.mediaStyleSelector(this.mediaFile, $.proxy(this, 'insert'), {}); + }, + + /** + * When display config has been set, insert the placeholder markup into the + * wysiwyg and generate its corresponding json macro pair to be added to the + * tagmap. + */ + insert: function (formatted_media) { + var element = Drupal.media.filter.create_element(formatted_media.html, { + fid: this.mediaFile.fid, + view_mode: formatted_media.type, + attributes: formatted_media.options, + fields: formatted_media.options + }); + // Get the markup and register it for the macro / placeholder handling. + var markup = Drupal.media.filter.getWysiwygHTML(element); + + // Insert placeholder markup into wysiwyg. + Drupal.wysiwyg.instances[this.instanceId].insert(markup); + } +}; + +/** Helper functions */ + +/** + * Ensures the tag map has been initialized. + */ +function ensure_tagmap () { + return Drupal.media.filter.ensure_tagmap(); +} + +/** + * Serializes file information as a url-encoded JSON object and stores it as a + * data attribute on the html element. + * + * @param html (string) + * A html element to be used to represent the inserted media element. + * @param info (object) + * A object containing the media file information (fid, view_mode, etc). + * + * @deprecated + */ +function create_element (html, info) { + return Drupal.media.filter.create_element(html, info); +} + +/** + * Create a macro representation of the inserted media element. + * + * @param element (jQuery object) + * A media element with attached serialized file info. + * + * @deprecated + */ +function create_macro (element) { + return Drupal.media.filter.create_macro(element); +} + +/** + * Extract the file info from a WYSIWYG placeholder element as JSON. + * + * @param element (jQuery object) + * A media element with attached serialized file info. + * + * @deprecated + */ +function extract_file_info (element) { + return Drupal.media.filter.extract_file_info(element); +} + +/** + * Gets the HTML content of an element. + * + * @param element (jQuery object) + * + * @deprecated + */ +function outerHTML (element) { + return Drupal.media.filter.outerHTML(element); +} + +})(jQuery); diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.api.php b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.api.php new file mode 100644 index 000000000..894e89be7 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.api.php @@ -0,0 +1,71 @@ +<?php + +/** + * @file + * Hooks provided by the Media WYSIWYG module. + */ + +/** + * Alter a list of view modes allowed for a file embedded in the WYSIWYG. + * + * @param array $view_modes + * An array of view modes that can be used on the file when embedded in the + * WYSIWYG. + * @param object $file + * A file entity. + * + * @see media_get_wysiwyg_allowed_view_modes() + */ +function hook_media_wysiwyg_wysiwyg_allowed_view_modes_alter(&$view_modes, $file) { + $view_modes['default']['label'] = t('Display an unmodified version of the file'); + unset($view_modes['preview']); +} + +/** + * Alter the WYSIWYG view mode selection form. + * + * Similar to a form_alter, but runs first so that modules can add + * fields specific to a given file type (like alt tags on images) before alters + * begin to work on the fields. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * @param object $file + * A file entity. + * + * @see media_format_form() + */ +function hook_media_wysiwyg_format_form_prepare_alter(&$form, &$form_state, $file) { + $form['preview']['#access'] = FALSE; + + $file = $form_state['file']; + $form['heading']['#markup'] = t('Embedding %filename of type %filetype', array('%filename' => $file->filename, '%filetype' => $file->type)); +} + +/** + * Alter the output generated by Media filter tags. + * + * @param array $element + * The renderable array of output generated for the filter tag. + * @param array $tag_info + * The filter tag converted into an associative array by + * media_token_to_markup() with the following elements: + * - 'fid': The ID of the media file being rendered. + * - 'file': The object from file_load() of the media file being rendered. + * - 'view_mode': The view mode being used to render the file. + * - 'attributes': An additional array of attributes that could be output + * with media_get_file_without_label(). + * @param array $settings + * An additional array of settings. + * - 'wysiwyg': A boolean if the output is for the WYSIWYG preview or FALSE + * if for normal rendering. + * + * @see media_token_to_markup() + */ +function hook_media_wysiwyg_token_to_markup_alter(&$element, $tag_info, $settings) { + if (empty($settings['wysiwyg'])) { + $element['#attributes']['alt'] = t('This media has been output using the @mode view mode.', array('@mode' => $tag_info['view_mode'])); + } +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.ckeditor.inc b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.ckeditor.inc new file mode 100644 index 000000000..a00b15835 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.ckeditor.inc @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Provides WYSIWYG integration for CKEditor. + */ + +/** + * Implements hook_ckeditor_plugin_alter(). + */ +function media_wysiwyg_ckeditor_plugin_alter(&$plugins) { + // Override the default CKEditor Media plugin. + $plugins['media'] = array( + 'name' => 'media', + 'desc' => t('Plugin for inserting images from Drupal media module'), + 'path' => '%base_path%' . drupal_get_path('module', 'media_wysiwyg') . '/wysiwyg_plugins/media_ckeditor/', + 'buttons' => array( + 'Media' => array( + 'icon' => 'images/icon.gif', + 'label' => 'Add media', + ), + ), + 'default' => 'f', + ); +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.info b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.info new file mode 100644 index 000000000..60191dea8 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.info @@ -0,0 +1,23 @@ +name = Media WYSIWYG +description = Adds support for embedding media using client-side WYSIWYG editors. +package = Media +core = 7.x + +dependencies[] = media + +test_dependencies[] = ckeditor +test_dependencies[] = token +test_dependencies[] = wysiwyg + +files[] = media_wysiwyg.test +files[] = tests/media_wysiwyg.file_usage.test +files[] = tests/media_wysiwyg.macro.test + +configure = admin/config/media/browser + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.install b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.install new file mode 100644 index 000000000..a0a0d798d --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.install @@ -0,0 +1,54 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Media WYSIWYG module. + */ + +/** + * Implements hook_uninstall(). + */ +function media_wysiwyg_uninstall() { + // Remove variables. + variable_del('media_wysiwyg_wysiwyg_title'); + variable_del('media_wysiwyg_wysiwyg_icon_title'); + variable_del('media_wysiwyg_wysiwyg_default_view_mode'); + variable_del('media_wysiwyg_wysiwyg_upload_directory'); + variable_del('media_wysiwyg_wysiwyg_allowed_types'); + variable_del('media_wysiwyg_wysiwyg_allowed_attributes'); + variable_del('media_wysiwyg_wysiwyg_browser_plugins'); +} + +/** + * Implements hook_update_dependencies(). + */ +function media_wysiwyg_update_dependencies() { + // Ensure the "access media browser" permission is granted to users before + // using it to grant the "use media wysiwyg" permission. + $dependencies['media_wysiwyg'][7201] = array( + 'media' => 7226, + ); +} + +/** + * Grant existing user access to new media wysiwyg permission. + */ +function media_wysiwyg_update_7201() { + $roles = user_roles(TRUE, 'access media browser'); + foreach ($roles as $rid => $role) { + user_role_grant_permissions($rid, array('use media wysiwyg')); + } + + return t('Use Media WYSIWYG permission was granted to: @roles.', array( + '@roles' => check_plain(implode(', ', $roles)), + )); +} + +/** + * Use the legacy file entity rendering method for existing sites. + * + * Existing sites can change this setting at admin/config/media/browser. + */ +function media_wysiwyg_update_7202() { + variable_set('media_wysiwyg_default_render', 'field_attach'); +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.module b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.module new file mode 100644 index 000000000..162e60bb2 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.module @@ -0,0 +1,371 @@ +<?php + +/** + * @file + * Primarily Drupal hooks. + */ + +// Functions for tracking the file usage of [[inline tags]]. +require_once dirname(__FILE__) . '/includes/media_wysiwyg.file_usage.inc'; + +// Functions for working with [[inline tags]] and wysiwyg editors. +require_once dirname(__FILE__) . '/includes/media_wysiwyg.filter.inc'; + +// Functions for UUID support to embedded media. +require_once dirname(__FILE__) . '/includes/media_wysiwyg.uuid.inc'; + +/** + * Implements hook_hook_info(). + */ +function media_wysiwyg_hook_info() { + $hooks = array( + 'media_wysiwyg_token_to_markup_alter', + 'media_wysiwyg_allowed_view_modes_alter', + 'media_wysiwyg_format_form_prepare_alter', + ); + + return array_fill_keys($hooks, array('group' => 'media_wysiwyg')); +} + +/** + * Implements hook_menu(). + */ +function media_wysiwyg_menu() { + $items = array(); + + $items['media/%file/format-form'] = array( + 'title' => 'Style selector', + 'description' => 'Choose a format for a piece of media', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_wysiwyg_format_form', 1), + 'access callback' => 'media_wysiwyg_access', + 'access arguments' => array('view', 1), + 'file' => 'includes/media_wysiwyg.pages.inc', + 'theme callback' => 'media_dialog_get_theme_name', + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implements hook_permission(). + */ +function media_wysiwyg_permission() { + return array( + 'use media wysiwyg' => array( + 'title' => t('Use Media WYSIWYG in an editor'), + // Marked restrict because the WYSIWYG forms generate image derivatives, + // which could lead to a DoS security vulnerability. + 'restrict access' => TRUE, + ), + ); +} + +/** + * Access callback for WYSIWYG Media. + */ +function media_wysiwyg_access($op, $file = NULL, $account = NULL) { + return user_access('use media wysiwyg', $account) && file_entity_access($op, $file, $account); +} + +/** + * Implements hook_element_info_alter(). + */ +function media_wysiwyg_element_info_alter(&$types) { + $types['text_format']['#pre_render'][] = 'media_wysiwyg_pre_render_text_format'; +} + +/** + * Builds a map of media tags in the element. + * + * Builds a map of the media tags in an element that are being rendered to their + * rendered HTML. The map is stored in JS, so we can transform them when the + * editor is being displayed. + */ +function media_wysiwyg_pre_render_text_format($element) { + // filter_process_format() copies properties to the expanded 'value' child + // element. + if (!isset($element['format'])) { + return $element; + } + + $field = &$element['value']; + $settings = array( + 'field' => $field['#id'], + ); + + if (!isset($field['#value'])) { + return $element; + } + + $tagmap = _media_wysiwyg_generate_tagMap($field['#value']); + + if (isset($tagmap)) { + $element['#attached']['js'][] = array( + 'data' => array( + 'tagmap' => $tagmap, + ), + 'type' => 'setting', + ); + } + + // Load the media browser library. + $element['#attached']['library'][] = array('media', 'media_browser'); + $element['#attached']['library'][] = array('media', 'media_browser_settings'); + + // Add wysiwyg-specific settings. + $settings = array('wysiwyg_allowed_attributes' => variable_get('media_wysiwyg_wysiwyg_allowed_attributes', _media_wysiwyg_wysiwyg_allowed_attributes_default())); + $element['#attached']['js'][] = array( + 'data' => array( + 'media' => $settings, + ), + 'type' => 'setting', + ); + + // Add filter handling. + $element['#attached']['js'][] = drupal_get_path('module', 'media_wysiwyg') . '/js/media_wysiwyg.filter.js'; + + // Add CKEditor-specific JS. + if (module_exists('ckeditor')) { + $element['#attached']['js'][] = array( + 'data' => drupal_get_path('module', 'media_wysiwyg') . '/wysiwyg_plugins/media_ckeditor/library.js', + 'type' => 'file', + 'scope' => 'footer', + 'weight' => -20, + ); + } + + return $element; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function media_wysiwyg_form_wysiwyg_profile_form_alter(&$form, &$form_state) { + // Add warnings if the media filter is disabled for the WYSIWYG's text format. + $form['buttons']['drupal']['media']['#element_validate'][] = 'media_wysiwyg_wysiwyg_button_element_validate'; + $form['buttons']['drupal']['media']['#after_build'][] = 'media_wysiwyg_wysiwyg_button_element_validate'; + form_load_include($form_state, 'inc', 'media_wysiwyg', 'wysiwyg_plugins/media'); +} + +/** + * Element validate callback for the media WYSIWYG button. + */ +function media_wysiwyg_wysiwyg_button_element_validate($element, &$form_state) { + if (!empty($element['#value'])) { + $format = filter_format_load($form_state['build_info']['args'][0]->format); + $filters = filter_list_format($format->format); + if (empty($filters['media_filter']->status)) { + form_error($element, t('The <em>Convert Media tags to markup</em> filter must be enabled for the <a href="@format-link">@format format</a> in order to use the Media browser WYSIWYG button.', array( + '@format-link' => url('admin/config/content/formats/' . $format->format, array('query' => array('destination' => $_GET['q']))), + '@format' => $format->name, + ))); + } + } + + return $element; +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function media_wysiwyg_form_media_admin_config_browser_alter(&$form, &$form_state) { + $form['wysiwyg'] = array( + '#type' => 'fieldset', + '#title' => t('WYSIWYG configuration'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + ); + $form['wysiwyg']['media_wysiwyg_wysiwyg_browser_plugins'] = array( + '#type' => 'checkboxes', + '#title' => t('Enabled browser plugins'), + '#options' => array(), + '#required' => FALSE, + '#default_value' => variable_get('media_wysiwyg_wysiwyg_browser_plugins', array()), + '#description' => t('If no plugins are selected, they will all be available.'), + ); + + $plugins = media_get_browser_plugin_info(); + + foreach ($plugins as $key => $plugin) { + $form['wysiwyg']['media_wysiwyg_wysiwyg_browser_plugins']['#options'][$key] = !empty($plugin['title']) ? $plugin['title'] : $key; + } + + $form['wysiwyg']['media_wysiwyg_wysiwyg_upload_directory'] = array( + '#type' => 'textfield', + '#title' => t("File directory for uploaded media"), + '#default_value' => variable_get('media_wysiwyg_wysiwyg_upload_directory', ''), + '#description' => t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'), + ); + + if (module_exists('token')) { + $form['wysiwyg']['media_wysiwyg_wysiwyg_upload_directory']['#description'] .= t('This field supports tokens.'); + $form['wysiwyg']['tokens'] = array( + '#theme' => 'token_tree', + '#dialog' => TRUE, + ); + } + + $form['wysiwyg']['media_wysiwyg_default_render'] = array( + '#type' => 'radios', + '#title' => t('How should file entities be rendered within a text field?'), + '#description' => t("Full file entity rendering is the best choice for most sites. It respects the file entity's display settings specified at admin/structure/file-types. If your site already uses the legacy method, note that changing this option will affect your site's markup. Testing it on a non-production site is recommended."), + '#options' => array( + 'file_entity' => 'Full file entity rendering', + 'field_attach' => 'Legacy rendering (using field attach)', + ), + '#default_value' => variable_get('media_wysiwyg_default_render', 'file_entity'), + ); + + $form['wysiwyg']['media_wysiwyg_wysiwyg_allowed_types'] = array( + '#type' => 'checkboxes', + '#title' => t('Allowed types in WYSIWYG'), + '#options' => file_entity_type_get_names(), + '#default_value' => variable_get('media_wysiwyg_wysiwyg_allowed_types', array('audio', 'image', 'video', 'document')), + ); + + $form['#submit'][] = 'media_wysiwyg_admin_config_browser_pre_submit'; +} + +/** + * Manipulate values before form is submitted. + */ +function media_wysiwyg_admin_config_browser_pre_submit(&$form, &$form_state) { + $wysiwyg_browser_plugins = array_unique(array_values($form_state['values']['media_wysiwyg_wysiwyg_browser_plugins'])); + if (empty($wysiwyg_browser_plugins[0])) { + variable_del('media_wysiwyg_wysiwyg_browser_plugins'); + unset($form_state['values']['media_wysiwyg_wysiwyg_browser_plugins']); + } +} + +/** + * Implements hook_filter_info(). + */ +function media_wysiwyg_filter_info() { + $filters['media_filter'] = array( + 'title' => t('Convert Media tags to markup'), + 'description' => t('This filter will convert [[{type:media... ]] tags into markup. This must be enabled for the Media WYSIWYG integration to work with this input format.'), + 'process callback' => 'media_wysiwyg_filter', + 'weight' => 2, + // @TODO not implemented + 'tips callback' => 'media_filter_tips', + ); + + return $filters; +} + +/** + * Implements hook_wysiwyg_include_directory(). + */ +function media_wysiwyg_wysiwyg_include_directory($type) { + switch ($type) { + case 'plugins': + return 'wysiwyg_plugins'; + + break; + } +} + +/** + * Returns the default set of allowed attributes for use with WYSIWYG. + * + * @return array + * An array of whitelisted attributes. + */ +function _media_wysiwyg_wysiwyg_allowed_attributes_default() { + return array( + 'alt', + 'title', + 'height', + 'width', + 'hspace', + 'vspace', + 'border', + 'align', + 'style', + 'class', + 'id', + 'usemap', + 'data-picture-group', + 'data-picture-align', + 'data-picture-mapping', + ); +} + +/** + * Returns a drupal_render() array for just the file portion of a file entity. + * + * Optional custom settings can override how the file is displayed. + */ +function media_wysiwyg_get_file_without_label($file, $view_mode, $settings = array()) { + $file->override = $settings; + + $element = file_view_file($file, $view_mode); + + // The formatter invoked by file_view_file() can use $file->override to + // customize the returned render array to match the requested settings. To + // support simple formatters that don't do this, set the element attributes to + // what was requested, but not if the formatter applied its own logic for + // element attributes. + if (isset($settings['attributes'])) { + if (empty($element['#attributes'])) { + $element['#attributes'] = $settings['attributes']; + } + + // While this function may be called for any file type, images are a common + // use-case, and image theme functions have their own structure for render + // arrays. + if (isset($element['#theme'])) { + // theme_image() and theme_image_style() require the 'alt' attributes to + // be passed separately from the 'attributes' array. (see + // http://drupal.org/node/999338). Until that's fixed, implement this + // special-case logic. Image formatters using other theme functions are + // responsible for their own 'alt' attribute handling. See + // theme_media_formatter_large_icon() for an example. + if (in_array($element['#theme'], array('image', 'image_style'))) { + if (empty($element['#alt']) && isset($settings['attributes']['alt'])) { + $element['#alt'] = $settings['attributes']['alt']; + } + } + // theme_image_formatter() and any potential replacements, such as + // theme_colorbox_image_formatter(), also require attribute handling. + elseif (strpos($element['#theme'], 'image_formatter') !== FALSE) { + // theme_image_formatter() requires the attributes to be + // set on the item rather than the element itself. + if (empty($element['#item']['attributes'])) { + $element['#item']['attributes'] = $settings['attributes']; + } + + // theme_image_formatter() also requires alt, title, height, and + // width attributes to be set on the item rather than within its + // attributes array. + foreach (array('alt', 'title', 'width', 'height') as $attr) { + if (isset($settings['attributes'][$attr])) { + $element['#item'][$attr] = $settings['attributes'][$attr]; + } + } + } + } + } + + return $element; +} + +/** + * Returns an array containing the names of all fields that perform text filtering. + */ +function media_wysiwyg_filter_fields_with_text_filtering($entity_type, $entity) { + list($entity_id, $revision_id, $bundle) = entity_extract_ids($entity_type, $entity); + $fields = field_info_instances($entity_type, $bundle); + + // Get all of the fields on this entity that allow text filtering. + $fields_with_text_filtering = array(); + foreach ($fields as $field_name => $field) { + if (!empty($field['settings']['text_processing'])) { + $fields_with_text_filtering[] = $field_name; + } + } + + return $fields_with_text_filtering; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.test b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.test new file mode 100644 index 000000000..cdb672a19 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.test @@ -0,0 +1,101 @@ +<?php + +/** + * @file + * Tests for media.module. + */ + +/** + * Defines base class for media test cases. + */ +class MediaWYSIWYGTestHelper extends DrupalWebTestCase { + + /** + * Enable media and file entity modules for testing. + */ + public function setUp() { + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'media_wysiwyg'; + parent::setUp($modules); + } + + /** + * Generates markup to be inserted for a file. + * + * This is a PHP version of InsertMedia.insert() from js/wysiwyg-media.js. + * + * @param int $fid + * Drupal file id + * @param int $count + * Quantity of markup to insert + * @param array $attributes + * Extra attributes to insert. + * @param array $fields + * Extra field values to insert. + * + * @return string + * Filter markup. + */ + protected function generateJsonTokenMarkup($fid, $count = 1, array $attributes = array(), array $fields = array()) { + $markup = ''; + // Merge default atttributes. + $attributes += array( + 'height' => 100, + 'width' => 100, + 'classes' => 'media-element file_preview', + ); + + // Build the data that is used in a media tag. + $data = array( + 'fid' => $fid, + 'type' => 'media', + 'view_mode' => 'preview', + 'attributes' => $attributes, + 'fields' => $fields, + ); + + // Create the file usage markup. + for ($i = 1; $i <= $count; $i++) { + $markup .= '<p>[[' . drupal_json_encode($data) . ']]</p>'; + } + + return $markup; + } + + /** + * Utility function to create a test node. + * + * @param int $fid + * Create the node with media markup in the body field + * @param array $attributes + * Extra attributes to insert to the file. + * @param array $fields + * Extra field values to insert. + * + * @return int + * Returns the node id + */ + protected function createNode($fid = FALSE, array $attributes = array(), array $fields = array()) { + $markup = ''; + if (! empty($fid)) { + $markup = $this->generateJsonTokenMarkup($fid, 1, $attributes, $fields); + } + + // Create an article node with file markup in the body field. + $edit = array( + 'title' => $this->randomName(8), + 'body[und][0][value]' => $markup, + ); + // Save the article node. First argument is the URL, then the value array + // and the third is the label the button that should be "clicked". + $this->drupalPost('node/add/article', $edit, t('Save')); + + // Get the article node that was saved by the unique title. + $node = $this->drupalGetNodeByTitle($edit['title']); + return $node->nid; + } + +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.variable.inc b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.variable.inc new file mode 100644 index 000000000..206905a08 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/media_wysiwyg.variable.inc @@ -0,0 +1,42 @@ +<?php + +/** + * @file + * Declare Media WYSIWYG variables. + */ + +/** + * Implements hook_variable_group_info(). + */ +function media_wysiwyg_variable_group_info() { + $groups['media_wysiwyg'] = array( + 'title' => t('Media WYSIWYG'), + 'description' => t('Settings for Media WYSIWYG integration.'), + 'access' => 'administer media browser', + 'path' => 'admin/config/media/browser', + ); + + return $groups; +} + +/** +* Implements hook_variable_info(). +*/ +function media_wysiwyg_variable_info($options) { + $variables['media_wysiwyg_wysiwyg_title'] = array( + 'type' => 'string', + 'title' => t('WYSIWYG Title', array(), $options), + 'default' => t('Media browser', array(), $options), + 'description' => t('The WYSIWYG media plugin title.', array(), $options), + 'group' => 'media_wysiwyg', + ); + $variables['media_wysiwyg_wysiwyg_icon_title'] = array( + 'type' => 'string', + 'title' => t('WYSIWYG Icon Title', array(), $options), + 'default' => t('Add media', array(), $options), + 'description' => t('The WYSIWYG media button title to display on hover.', array(), $options), + 'group' => 'media_wysiwyg', + ); + + return $variables; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.file_usage.test b/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.file_usage.test new file mode 100644 index 000000000..bd82ecfa4 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.file_usage.test @@ -0,0 +1,237 @@ +<?php + +/** + * @file + * Tests for the file usage in entity fields with the Media filter markup. + */ + +class MediaWYSIWYGFileUsageTest extends MediaWYSIWYGTestHelper { + + /** + * Provide test information. + */ + public static function getInfo() { + return array( + 'name' => t('File usage tracking'), + 'description' => t('Tests tracking of usage for files in text fields.'), + 'group' => t('Media WYSIWYG'), + ); + } + + /** + * Enable media and file entity modules for testing. + */ + public function setUp() { + parent::setUp(); + + // Create and log in a user. + $account = $this->drupalCreateUser(array('administer nodes', 'create article content')); + $this->drupalLogin($account); + } + + /** + * Tests the tracking of file usages for files submitted via the WYSIWYG editor. + */ + public function testFileUsageIncrementing() { + // Create a file. + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[0]); + $fid = $file->fid; + + // There should be zero usages of this file prior to node creation, + $file_uses = file_usage_list($file); + $this->assertEqual(empty($file_uses), TRUE, t('Created a new file with zero uses.')); + + // Create a node to test with. + $nid = $this->createNode($fid); + + // Get the new file usage count. + $file_uses = file_usage_list($file); + + $this->assertEqual($file_uses['media']['node'][$nid], 1, t('File usage increases when added to a new node.')); + + // Create a new revision that has the file on it. File usage will be 2. + $node = node_load($nid); + $node->revision = TRUE; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + $revisions = count(node_revision_list($node)); + // Keep track of this VID to test deletion later on. + $delete_one = $node->vid; + + $this->assertEqual($revisions, 2, t('Node save created a second revision')); + $this->assertEqual($file_uses['media']['node'][$nid], 2, t('File usage incremented with a new node revision.')); + + // Create a new revision that has two instances of the file. File usage will + // be 4. + $node = node_load($nid); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2); + $node->revision = TRUE; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + $revisions = count(node_revision_list($node)); + // Keep track of this VID to test deletion later on. + $delete_two = $node->vid; + + $this->assertEqual($revisions, 3, t('Node save created a third revision.')); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('File usage incremented with multiple files and a new node revision.')); + + // Create a new revision that has no file on it. File usage will be 4. + $node = node_load($nid); + $node->body[LANGUAGE_NONE][0]['value'] = ''; + $node->revision = TRUE; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + $revisions = count(node_revision_list($node)); + // Keep track of this VID to test deletion later on. + $delete_zero = $node->vid; + + $this->assertEqual($revisions, 4, t('Node save created a fourth revision.')); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('File usage does not change with a new revision of the node without the file')); + + // Create a new revision that has the file on it. File usage will be 5. + $node = node_load($nid); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1); + $node->revision = TRUE; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + $revisions = count(node_revision_list($node)); + + $this->assertEqual($revisions, 5, t('Node save created a new revision.')); + $this->assertEqual($file_uses['media']['node'][$nid], 5, t('File usage incremented with a single file on a new node revision.')); + + // Delete a revision that has the file on it once. File usage will be 4. + node_revision_delete($delete_one); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('Deleting revision with file decreases file usage')); + + // Delete a revision that has no file on it. File usage will be 4. + node_revision_delete($delete_zero); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('Deleting revision without a file does not change file usage.')); + + // Delete a revision that has the file on it twice. File usage will be 2. + node_revision_delete($delete_two); + $node = node_load($nid); + $file_uses = file_usage_list($file); + $this->assertEqual($file_uses['media']['node'][$nid], 2, t('Deleting revision with file decreases file usage')); + + // Create a new revision with the file on it twice. File usage will be 4. + $node = node_load($nid); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 2); + $node->revision = TRUE; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + + $this->assertEqual($file_uses['media']['node'][$nid], 4, t('File usage incremented with files on a new node revision.')); + + // Re-save current revision with file on it once instead of twice. File + // usage will be 3. + $node = node_load($nid); + $node->body[LANGUAGE_NONE][0]['value'] = $this->generateJsonTokenMarkup($fid, 1); + $saved_vid = $node->vid; + node_save($node); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + + $this->assertEqual($node->vid, $saved_vid, t('Resaved node revision does not create new revision.')); + $this->assertEqual($file_uses['media']['node'][$nid], 3, t('Resaved node revision with fewer files reduces file usage.')); + + // Delete the node. File usage will be 0. + $node = node_load($nid); + node_delete($nid); + + $node = node_load($nid); + $file_uses = file_usage_list($file); + + $this->assertEqual(empty($node), TRUE, t('Node has been deleted.')); + $this->assertEqual(empty($file_uses), TRUE, t('Deleting the node removes all file uses.')); + } + + /** + * Tests the behavior of node and file deletion. + */ + public function testFileUsageIncrementingDelete() { + // Create a node with file markup in the body field with a new file. + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[1]); + $fid = $file->fid; + $file_uses = file_usage_list($file); + + $this->assertEqual(empty($file_uses), TRUE, t('Created a new file with zero uses.')); + + // Create a new node with file markup. + $nid = $this->createNode($fid); + $file_uses = file_usage_list($file); + + $this->assertEqual($file_uses['media']['node'][$nid], 1, t('Incremented file usage on node save.')); + + // Try to delete the file. file_delete() should return file_usage(). + $deleted = file_delete($file); + $this->assertTrue(is_array($deleted), t('File cannot be deleted while in use by a node.')); + + // Delete the node. + node_delete($nid); + $node = node_load($nid); + $file_uses = file_usage_list($file); + + $this->assertEqual(empty($node), TRUE, t('Node has been deleted.')); + $this->assertEqual(empty($file_uses), TRUE, t('File has zero usage after node is deleted.')); + + $deleted = file_delete($file); + $this->assertTrue($deleted, t('File can be deleted with no usage.')); + + $file = file_load($fid); + $this->assertTrue(empty($file), t('File no longer exists after delete.')); + } + + + /** + * Tests if node still remains updatable if file was deleted. + */ + public function testFileUsageForcedDelete() { + // Create a node with file markup in the body field with a new file. + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[1]); + $fid = $file->fid; + $file_uses = file_usage_list($file); + + $this->assertEqual(empty($file_uses), TRUE, t('Created a new file with zero uses.')); + + // Create a new node with file markup. + $nid = $this->createNode($fid); + $file_uses = file_usage_list($file); + + $this->assertEqual($file_uses['media']['node'][$nid], 1, t('Incremented file usage on node save.')); + + // Force the file to delete. + $deleted = file_delete($file, TRUE); + $this->assertTrue($deleted, t('File was deleted although in use sice we forced it.')); + + // Try to update the node that uses broken file. + $account = $this->drupalCreateUser(array('edit any article content')); + $node = node_load($nid); + $this->drupalLogin($account); + $this->drupalGet('node/' . $nid . '/edit'); + $this->assertRaw(check_plain($node->body['und'][0]['value']), t('Reference to deleted file found in node body.')); + $edit = array( + 'body[und][0][value]' => '', + ); + $this->drupalPost(NULL, $edit, t('Save')); + $type = node_type_load($node->type); + $this->assertRaw(t('@type %title has been updated.', array('@type' => $type->name, '%title' => $node->title)), t('Node without reference to deleted file saved successfully.')); + } +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.macro.test b/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.macro.test new file mode 100644 index 000000000..7ac1bc4c7 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/tests/media_wysiwyg.macro.test @@ -0,0 +1,95 @@ +<?php + +/** + * @file + * Tests for ensuring media macros render properly. + */ + +/** + * Defines media macro test cases. + */ +class MediaWYSIWYGWYSIWYGOverridesTest extends MediaWYSIWYGTestHelper { + + /** + * Provide test information. + */ + public static function getInfo() { + return array( + 'name' => t('Media WYSIWYG WYSIWYG overrides'), + 'description' => t('Tests that overridden attributes display correct.'), + 'group' => t('Media WYSIWYG'), + 'dependencies' => array('token'), + ); + } + + public function setUp() { + parent::setUp('token'); + + // Create and log in a user. + $account = $this->drupalCreateUser(array('create article content', 'administer filters', 'use text format filtered_html')); + $this->drupalLogin($account); + + // Enable the media filter for full html. + $edit = array( + 'filters[media_filter][status]' => TRUE, + 'filters[filter_html][status]' => FALSE, + ); + $this->drupalPost('admin/config/content/formats/filtered_html', $edit, t('Save configuration')); + } + + /** + * Test image media overrides. + */ + public function testAttributeOverrides() { + $files = $this->drupalGetTestFiles('image'); + $file = file_save($files[0]); + + // Create a node to test with. + $nid = $this->createNode($file->fid); + + $this->drupalGet('node/' . $nid); + $this->assertRaw('width="100"', t('Image displays with default width attribute.')); + $this->assertRaw('height="100"', t('Image displays with default height attribute.')); + + // Create a node with a style attribute. + $attributes = array( + 'style' => 'float: left; width: 50px;', + ); + $nid = $this->createNode($file->fid, $attributes); + $this->drupalGet('node/' . $nid); + $this->assertRaw(drupal_attributes($attributes), t('Image displays with overriden attributes.')); + + // Create a node with overriden alt/title attributes. + $attributes = array( + 'alt' => $this->randomName(), + 'title' => $this->randomName(), + ); + $nid = $this->createNode($file->fid, $attributes); + $this->drupalGet('node/' . $nid); + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes.')); + + // Create a node with overriden alt/title fields. + $fields = $attributes = array(); + $attributes['alt'] = $fields['field_file_image_alt_text[und][0][value]'] = $this->randomName(); + $attributes['title'] = $fields['field_file_image_title_text[und][0][value]'] = $this->randomName(); + + $nid = $this->createNode($file->fid, array(), $fields); + $this->drupalGet('node/' . $nid); + // Ensure that the alt/title from attributes display. + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as fields.')); + + // Create a node with overriden alt/title fields as well as attributes. + $attributes = array( + 'alt' => $this->randomName(), + 'title' => $this->randomName(), + ); + $fields = array( + 'field_file_image_alt_text[und][0][value]' => $this->randomName(), + 'field_file_image_title_text[und][0][value]' => $this->randomName(), + ); + $nid = $this->createNode($file->fid, $attributes, $fields); + $this->drupalGet('node/' . $nid); + // Ensure that the alt/title from attributes display rather the field ones. + $this->assertRaw(drupal_attributes($attributes), t('Image displays with alt/title set as attributes overriding field values.')); + } +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media.inc b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media.inc new file mode 100644 index 000000000..9cd03bbd2 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media.inc @@ -0,0 +1,34 @@ +<?php + +/** + * @file + * Define the WYSIWYG browser plugin. + */ + +/** + * Implements WYSIWYG's hook_INCLUDE_plugin(). + */ +function media_wysiwyg_media_plugin() { + $plugins['media'] = array( + 'title' => variable_get('media_wysiwyg_wysiwyg_title', t('Media browser')), + 'vendor url' => 'http://drupal.org/project/media', + 'icon path' => drupal_get_path('module', 'media_wysiwyg') . '/wysiwyg_plugins/media_ckeditor/images', + 'icon file' => 'icon.gif', + 'icon title' => variable_get('media_wysiwyg_wysiwyg_icon_title', t('Add media')), + // @todo: move this to the plugin directory for the wysiwyg plugin. + 'js path' => drupal_get_path('module', 'media_wysiwyg') . '/js', + 'js file' => 'wysiwyg-media.js', + 'css path' => drupal_get_path('module', 'media_wysiwyg') . '/css', + 'css file' => 'media_wysiwyg.css', + 'settings' => array( + 'global' => array( + 'enabledPlugins' => variable_get('media_wysiwyg_wysiwyg_browser_plugins', array()), + 'file_directory' => variable_get('media_wysiwyg_wysiwyg_upload_directory', ''), + 'types' => variable_get('media_wysiwyg_wysiwyg_allowed_types', array('audio', 'image', 'video', 'document')), + 'id' => 'media_wysiwyg', + ), + ), + ); + + return $plugins; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/images/icon.gif b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/images/icon.gif Binary files differnew file mode 100644 index 000000000..bf9b501c3 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/images/icon.gif diff --git a/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/lang/en.js b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/lang/en.js new file mode 100644 index 000000000..efc1d6853 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/lang/en.js @@ -0,0 +1,14 @@ +/* + Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. + For licensing, see LICENSE.md or http://ckeditor.com/license + */ +CKEDITOR.plugins.setLang( 'media', 'en', { + alt: 'Alternative Text', // Inherit from image plugin. + captioned: 'Captioned image', // NEW property. + lockRatio: 'Lock Ratio', // Inherit from image plugin. + menu: 'Image Properties', // Inherit from image plugin. + resetSize: 'Reset Size', // Inherit from image plugin. + resizer: 'Click and drag to resize', // NEW property. + title: 'Image Properties', // Inherit from image plugin. + urlMissing: 'Image source URL is missing.' // Inherit from image plugin. +} ); diff --git a/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/library.js b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/library.js new file mode 100644 index 000000000..b097d170b --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/library.js @@ -0,0 +1,183 @@ + +/** + * @file + * Attach Media ckeditor behaviors. + */ + +(function ($) { + Drupal.media = Drupal.media || {}; + + Drupal.settings.ckeditor.plugins['media'] = { + /** + * Execute the button. + */ + invoke: function (data, settings, instanceId) { + if (data.format == 'html') { + if (jQuery(data.node).is('[data-media-element]')) { + // Change the view mode for already-inserted media. + var mediaFile = Drupal.media.filter.extract_file_info(jQuery(data.node)); + Drupal.media.popups.mediaStyleSelector(mediaFile, function (mediaFiles) { + Drupal.settings.ckeditor.plugins['media'].insertMediaFile(mediaFile, mediaFiles, CKEDITOR.instances[instanceId]); + }, settings['global']); + } + else { + Drupal.media.popups.mediaBrowser(function (mediaFiles) { + Drupal.settings.ckeditor.plugins['media'].mediaBrowserOnSelect(mediaFiles, instanceId); + }, settings['global']); + } + } + }, + + /** + * Respond to the mediaBrowser's onSelect event. + */ + mediaBrowserOnSelect: function (mediaFiles, instanceId) { + var mediaFile = mediaFiles[0]; + var options = {}; + Drupal.media.popups.mediaStyleSelector(mediaFile, function (formattedMedia) { + Drupal.settings.ckeditor.plugins['media'].insertMediaFile(mediaFile, formattedMedia, CKEDITOR.instances[instanceId]); + }, options); + + return; + }, + + insertMediaFile: function (mediaFile, formattedMedia, ckeditorInstance) { + // Customization of Drupal.media.filter.registerNewElement(). + var element = Drupal.media.filter.create_element(formattedMedia.html, { + fid: mediaFile.fid, + view_mode: formattedMedia.type, + attributes: formattedMedia.options + }); + + // Use own wrapper element to be able to properly deal with selections. + // Check prepareDataForWysiwygMode() in plugin.js for details. + var wysiwygHTML = Drupal.media.filter.getWysiwygHTML(element); + + // Insert element. Use CKEDITOR.dom.element.createFromHtml to ensure our + // custom wrapper element is preserved. + if (wysiwygHTML.indexOf("<!--MEDIA-WRAPPER-START-") !== -1) { + ckeditorInstance.plugins.media.mediaLegacyWrappers = true; + wysiwygHTML = wysiwygHTML.replace(/<!--MEDIA-WRAPPER-START-(\d+)-->(.*?)<!--MEDIA-WRAPPER-END-\d+-->/gi, ''); + } else { + wysiwygHTML = '<mediawrapper data="">' + wysiwygHTML + '</mediawrapper>'; + } + + var editorElement = CKEDITOR.dom.element.createFromHtml(wysiwygHTML); + ckeditorInstance.insertElement(editorElement); + + // Initialize widget on our html if possible. + if (parseFloat(CKEDITOR.version) >= 4.3 && typeof(CKEDITOR.plugins.registered.widget) != 'undefined') { + ckeditorInstance.widgets.initOn( editorElement, 'mediabox' ); + } + }, + + /** + * Forces custom attributes into the class field of the specified image. + * + * Due to a bug in some versions of Firefox + * (http://forums.mozillazine.org/viewtopic.php?f=9&t=1991855), the + * custom attributes used to share information about the image are + * being stripped as the image markup is set into the rich text + * editor. Here we encode these attributes into the class field so + * the data survives. + * + * @param imgElement + * The image + * @fid + * The file id. + * @param view_mode + * The view mode. + * @param additional + * Additional attributes to add to the image. + */ + forceAttributesIntoClass: function (imgElement, fid, view_mode, additional) { + var wysiwyg = imgElement.attr('wysiwyg'); + if (wysiwyg) { + imgElement.addClass('attr__wysiwyg__' + wysiwyg); + } + var format = imgElement.attr('format'); + if (format) { + imgElement.addClass('attr__format__' + format); + } + var typeOf = imgElement.attr('typeof'); + if (typeOf) { + imgElement.addClass('attr__typeof__' + typeOf); + } + if (fid) { + imgElement.addClass('img__fid__' + fid); + } + if (view_mode) { + imgElement.addClass('img__view_mode__' + view_mode); + } + if (additional) { + for (var name in additional) { + if (additional.hasOwnProperty(name)) { + switch (name) { + case 'field_file_image_alt_text[und][0][value]': + imgElement.attr('alt', additional[name]); + break; + case 'field_file_image_title_text[und][0][value]': + imgElement.attr('title', additional[name]); + break; + default: + imgElement.addClass('attr__' + name + '__' + additional[name]); + break; + } + } + } + } + }, + + /** + * Retrieves encoded attributes from the specified class string. + * + * @param classString + * A string containing the value of the class attribute. + * @return + * An array containing the attribute names as keys, and an object + * with the name, value, and attribute type (either 'attr' or + * 'img', depending on whether it is an image attribute or should + * be it the attributes section) + */ + getAttributesFromClass: function (classString) { + var actualClasses = []; + var otherAttributes = []; + var classes = classString.split(' '); + var regexp = new RegExp('^(attr|img)__([^\S]*)__([^\S]*)$'); + for (var index = 0; index < classes.length; index++) { + var matches = classes[index].match(regexp); + if (matches && matches.length === 4) { + otherAttributes[matches[2]] = { + name: matches[2], + value: matches[3], + type: matches[1] + }; + } + else { + actualClasses.push(classes[index]); + } + } + if (actualClasses.length > 0) { + otherAttributes['class'] = { + name: 'class', + value: actualClasses.join(' '), + type: 'attr' + }; + } + return otherAttributes; + }, + + sortAttributes: function (a, b) { + var nameA = a.name.toLowerCase(); + var nameB = b.name.toLowerCase(); + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + } + }; + +})(jQuery); diff --git a/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/plugin.js b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/plugin.js new file mode 100644 index 000000000..37fd3b9d3 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg/wysiwyg_plugins/media_ckeditor/plugin.js @@ -0,0 +1,217 @@ +/* +Copyright (c) 2003-2013, CKSource - Frederico Knabben. All rights reserved. +For licensing, see LICENSE.html or http://ckeditor.com/license +*/ + +/** + * @file Plugin for inserting images from Drupal media module + * + * @TODO Remove all the legecy media wrapper once it's sure nobody uses that + * anymore. + */ +( function() { + var mediaPluginDefinition = { + icons: 'media', + requires: ['button'], + // Check if this instance has widget support. All the default distributions + // of the editor have the widget plugin disabled by default. + hasWidgetSupport: typeof(CKEDITOR.plugins.registered.widget) != 'undefined', + mediaLegacyWrappers: false, + + // Wrap Drupal plugin in a proxy plugin. + init: function(editor){ + editor.addCommand( 'media', + { + exec: function (editor) { + var data = { + format: 'html', + node: null, + content: '' + }; + var selection = editor.getSelection(); + + if (selection) { + data.node = selection.getSelectedElement(); + if (data.node) { + data.node = data.node.$; + } + if (selection.getType() == CKEDITOR.SELECTION_TEXT) { + if (CKEDITOR.env.ie && CKEDITOR.env.version < 10) { + data.content = selection.getNative().createRange().text; + } + else { + data.content = selection.getNative().toString(); + } + } + else if (data.node) { + // content is supposed to contain the "outerHTML". + data.content = data.node.parentNode.innerHTML; + } + } + Drupal.settings.ckeditor.plugins['media'].invoke(data, Drupal.settings.ckeditor.plugins['media'], editor.name); + } + }); + + editor.ui.addButton( 'Media', + { + label: 'Add media', + command: 'media', + icon: this.path + 'images/icon.gif' + }); + + var ckeditorversion = parseFloat(CKEDITOR.version); + + // Because the media comment wrapper don't work well for CKEditor we + // replace them by using a custom mediawrapper element. + // Instead having + // <!--MEDIA-WRAPPER-START-1--><img /><!--MEDIA-WRAPPER-END-1--> + // We wrap the placeholder with + // <mediawrapper data="1"><img /></mediawrapper> + // That way we can deal better with selections - see selectionChange. + CKEDITOR.dtd['mediawrapper'] = CKEDITOR.dtd; + CKEDITOR.dtd.$blockLimit['mediawrapper'] = 1; + CKEDITOR.dtd.$inline['mediawrapper'] = 1; + CKEDITOR.dtd.$nonEditable['mediawrapper'] = 1; + if (ckeditorversion >= 4.1) { + // Register allowed tag for advanced filtering. + editor.filter.allow( 'mediawrapper[!data]', 'mediawrapper', true); + // Don't remove the data-file_info attribute added by media! + editor.filter.allow( '*[!data-file_info]', 'mediawrapper', true); + // Ensure image tags accept all kinds of attributes. + editor.filter.allow( 'img[*]{*}(*)', 'mediawrapper', true); + // Objects should be selected as a whole in the editor. + CKEDITOR.dtd.$object['mediawrapper'] = 1; + } + function prepareDataForWysiwygMode(data) { + data = Drupal.media.filter.replaceTokenWithPlaceholder(data); + // Legacy media wrapper. + mediaPluginDefinition.mediaLegacyWrappers = (data.indexOf("<!--MEDIA-WRAPPER-START-") !== -1); + data = data.replace(/<!--MEDIA-WRAPPER-START-(\d+)-->(.*?)<!--MEDIA-WRAPPER-END-\d+-->/gi, '<mediawrapper data="$1">$2</mediawrapper>'); + return data; + } + function prepareDataForSourceMode(data) { + var replacement = '$2'; + // Legacy wrapper + if (mediaPluginDefinition.mediaLegacyWrappers) { + replacement = '<!--MEDIA-WRAPPER-START-$1-->$2<!--MEDIA-WRAPPER-END-$1-->'; + } + data = data.replace(/<mediawrapper data="(.*)">(.*?)<\/mediawrapper>/gi, replacement); + data = Drupal.media.filter.replacePlaceholderWithToken(data); + return data; + } + + // Ensure the tokens are replaced by placeholders while editing. + // Check for widget support. + if (mediaPluginDefinition.hasWidgetSupport) { + editor.widgets.add( 'mediabox', + { + button: 'Create a mediabox', + template: '<mediawrapper></mediawrapper>', + editables: {}, + allowedContent: '*', + upcast: function( element ) { + if (element.name != 'mediawrapper') { + // Ensure media tokens are converted to media placeholdes. + element.setHtml(prepareDataForWysiwygMode(element.getHtml())); + } + return element.name == 'mediawrapper'; + }, + + downcast: function( widgetElement ) { + var token = prepareDataForSourceMode(widgetElement.getOuterHtml()); + return new CKEDITOR.htmlParser.text(token); + return element.name == 'mediawrapper'; + } + }); + } + else if (ckeditorversion >= 4) { + // CKEditor >=4.0 + editor.on('setData', function( event ) { + event.data.dataValue = prepareDataForWysiwygMode(event.data.dataValue); + }); + } + else { + // CKEditor >=3.6 behaviour. + editor.on( 'beforeSetMode', function( event, data ) { + event.removeListener(); + var wysiwyg = editor._.modes[ 'wysiwyg' ]; + var source = editor._.modes[ 'source' ]; + wysiwyg.loadData = CKEDITOR.tools.override( wysiwyg.loadData, function( org ) + { + return function( data ) { + return ( org.call( this, prepareDataForWysiwygMode(data)) ); + }; + } ); + source.loadData = CKEDITOR.tools.override( source.loadData, function( org ) + { + return function( data ) { + return ( org.call( this, prepareDataForSourceMode(data) ) ); + }; + } ); + }); + } + + // Provide alternative to the widget functionality introduced in 4.3. + if (!mediaPluginDefinition.hasWidgetSupport) { + // Ensure tokens instead the html element is saved. + editor.on('getData', function( event ) { + event.data.dataValue = prepareDataForSourceMode(event.data.dataValue); + }); + + // Ensure our enclosing wrappers are always included in the selection. + editor.on('selectionChange', function( event ) { + var ranges = editor.getSelection().getRanges().createIterator(); + var newRanges = []; + var currRange; + while(currRange = ranges.getNextRange()) { + var commonAncestor = currRange.getCommonAncestor(false); + if (commonAncestor && typeof(commonAncestor.getName) != 'undefined' && commonAncestor.getName() == 'mediawrapper') { + var range = new CKEDITOR.dom.range( editor.document ); + if (currRange.collapsed === true) { + // Don't allow selection within the wrapper element. + if (currRange.startOffset == 0) { + // While v3 plays nice with setting start and end to avoid + // editing within the media wrapper element, v4 ignores that. + // Thus we try to move the cursor further away. + if (parseInt(CKEDITOR.version) > 3) { + range.setStart(commonAncestor.getPrevious()); + range.setEnd(commonAncestor.getPrevious()); + } + else { + range.setStartBefore(commonAncestor); + } + } + else { + // While v3 plays nice with setting start and end to avoid + // editing within the media wrapper element, v4 ignores that. + // Thus we try to move the cursor further away. + if (parseInt(CKEDITOR.version) > 3) { + range.setStart(commonAncestor.getNext(), 1); + range.setEnd(commonAncestor.getNext(), 1); + } + else { + range.setStartAfter(commonAncestor); + } + } + } + else { + // Always select the whole wrapper element. + range.setStartBefore(commonAncestor); + range.setEndAfter(commonAncestor); + } + newRanges.push(range); + } + } + if (newRanges.length) { + editor.getSelection().selectRanges(newRanges); + } + }); + } + } + }; + // Add dependency to widget plugin if possible. + if (parseFloat(CKEDITOR.version) >= 4.3 && mediaPluginDefinition.hasWidgetSupport) { + mediaPluginDefinition.requires.push('widget'); + } + CKEDITOR.plugins.add( 'media', mediaPluginDefinition); +} )(); diff --git a/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.admin.inc b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.admin.inc new file mode 100644 index 000000000..4d1f0a60d --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.admin.inc @@ -0,0 +1,77 @@ +<?php + +/** + * @file + * Generate configuration form and save settings. + */ + +/** + * Configuration form for Media's WYSIWYG view modes. + */ +function media_wysiwyg_view_mode_configuration_form($form, &$form_state) { + $options = array(); + + // Add the default view mode by default + $options['default'] = t('Default'); + + $entity_info = entity_get_info('file'); + foreach ($entity_info['view modes'] as $view_mode => $view_mode_info) { + $options[$view_mode] = check_plain($view_mode_info['label']); + } + + $form['media_wysiwyg_view_mode_wysiwyg_restricted_view_modes'] = array( + '#type' => 'fieldset', + '#title' => t('WYSIWYG allowed view modes'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#description' => t('Restrict the allowed view modes when embedding files inside of the the WYSIWYG editor.'), + ); + + foreach (file_type_get_enabled_types() as $type) { + $form['media_wysiwyg_view_mode_wysiwyg_restricted_view_modes']["media_wysiwyg_view_mode_{$type->type}_wysiwyg_restricted_view_modes_status"] = array( + '#type' => 'checkbox', + '#title' => t('Restrict allowed view modes for %type', array('%type' => $type->label)), + '#default_value' => variable_get("media_wysiwyg_view_mode_{$type->type}_wysiwyg_restricted_view_modes_status", FALSE), + ); + $form['media_wysiwyg_view_mode_wysiwyg_restricted_view_modes']["media_wysiwyg_view_mode_{$type->type}_wysiwyg_restricted_view_modes"] = array( + '#type' => 'checkboxes', + '#title' => t('Restrict view modes'), + '#options' => $options, + '#default_value' => variable_get("media_wysiwyg_view_mode_{$type->type}_wysiwyg_restricted_view_modes", array()), + '#states' => array( + 'visible' => array( + ':input[name="media_wysiwyg_view_mode_' . $type->type . '_wysiwyg_restricted_view_modes_status"]' => array('checked' => TRUE), + ), + ), + ); + } + + $form['media_wysiwyg_view_mode_file_wysiwyg_view_mode'] = array( + '#type' => 'fieldset', + '#title' => t('File WYSIWYG view mode'), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#description' => t('Use a custom view mode when displaying files inside of the WYSIWYG editor.'), + ); + + foreach (file_type_get_enabled_types() as $type) { + $form['media_wysiwyg_view_mode_file_wysiwyg_view_mode']["media_wysiwyg_view_mode_{$type->type}_file_wysiwyg_view_mode_status"] = array( + '#type' => 'checkbox', + '#title' => t('Use a custom view mode for %type', array('%type' => $type->label)), + '#default_value' => variable_get("media_wysiwyg_view_mode_{$type->type}_file_wysiwyg_view_mode_status", FALSE), + ); + $form['media_wysiwyg_view_mode_file_wysiwyg_view_mode']["media_wysiwyg_view_mode_{$type->type}_file_wysiwyg_view_mode"] = array( + '#type' => 'select', + '#title' => t('View mode'), + '#options' => $options, + '#default_value' => variable_get("media_wysiwyg_view_mode_{$type->type}_file_wysiwyg_view_mode", 'wysiwyg'), + '#states' => array( + 'visible' => array( + ':input[name="media_wysiwyg_view_mode_' . $type->type . '_file_wysiwyg_view_mode_status"]' => array('checked' => TRUE), + ), + ), + ); + } + + return system_settings_form($form); +} diff --git a/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.info b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.info new file mode 100644 index 000000000..a17131e0e --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.info @@ -0,0 +1,17 @@ +name = Media WYSIWYG View Mode +description = Enables files inside of the WYSIWYG editor to be displayed using a separate view mode. +package = Media +core = 7.x + +dependencies[] = media_wysiwyg + +configure = admin/config/media/wysiwyg-view-mode + +files[] = media_wysiwyg_view_mode.test + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.install b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.install new file mode 100644 index 000000000..56b706fa7 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.install @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Install, update and uninstall functions for the Media WYSIWYG View Mode module. + */ + +/** + * Implements hook_uninstall(). + */ +function media_wysiwyg_view_mode_uninstall() { + db_delete('variable') + ->condition('name', "media_wysiwyg_view_mode_%", "LIKE") + ->execute(); +} diff --git a/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.module b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.module new file mode 100644 index 000000000..d36bec716 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.module @@ -0,0 +1,165 @@ +<?php + +/** + * @file + * Primarily Drupal hooks. + */ + +/** + * Implements hook_permission(). + */ +function media_wysiwyg_view_mode_permission() { + return array( + 'administer media wysiwyg view mode' => array( + 'title' => t('Administer Media WYSIWYG View Mode'), + ), + ); +} + +/** + * Implements hook_menu(). + */ +function media_wysiwyg_view_mode_menu() { + $items['admin/config/media/wysiwyg-view-mode'] = array( + 'title' => 'Media WYSIWYG View Mode', + 'description' => 'Configure view mode settings for files embedded into and displayed inside of the WYSIWYG editor.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_wysiwyg_view_mode_configuration_form'), + 'access arguments' => array('administer media wysiwyg view mode'), + 'file' => 'media_wysiwyg_view_mode.admin.inc', + ); + + return $items; +} + +/** + * Implements hook_help(). + */ +function media_wysiwyg_view_mode_help($path, $arg) { + switch ($path) { + case 'admin/config/media/wysiwyg-view-mode': + $output = ''; + $output .= '<p>' . t('Configure view modes for files displayed inside of the WYSIWYG editor.') . '</p>'; + $output .= '<p>' . t('View modes can be configured per file type. Only enabled view modes are selectable.') . '</p>'; + return $output; + } +} + +/** + * Implements hook_entity_info_alter(). + */ +function media_wysiwyg_view_mode_entity_info_alter(&$entity_info) { + $entity_info['file']['view modes'] += array( + 'wysiwyg' => array( + 'label' => t('WYSIWYG'), + 'custom settings' => TRUE, + ), + ); +} + +/** + * Implements hook_media_wysiwyg_wysiwyg_allowed_view_modes_alter(). + */ +function media_wysiwyg_view_mode_media_wysiwyg_wysiwyg_allowed_view_modes_alter(&$view_modes, &$file) { + if (variable_get("media_wysiwyg_view_mode_{$file->type}_wysiwyg_restricted_view_modes_status", FALSE) == TRUE) { + $restricted_view_modes = variable_get("media_wysiwyg_view_mode_{$file->type}_wysiwyg_restricted_view_modes", array()); + + foreach ($restricted_view_modes as $restricted_view_mode) { + if (array_key_exists($restricted_view_mode, $view_modes)) { + unset($view_modes[$restricted_view_mode]); + } + } + } +} + +/** + * Implements hook_media_wysiwyg_token_to_markup_alter(). + */ +function media_wysiwyg_view_mode_media_wysiwyg_token_to_markup_alter(&$element, $tag_info, $settings) { + if (!empty($settings['wysiwyg'])) { + $file = $tag_info['file']; + + if (variable_get("media_wysiwyg_view_mode_{$file->type}_file_wysiwyg_view_mode_status", FALSE) == TRUE) { + $element = media_wysiwyg_get_file_without_label($file, variable_get("media_wysiwyg_view_mode_{$file->type}_file_wysiwyg_view_mode", 'wysiwyg'), $settings); + } + else { + $element = media_wysiwyg_get_file_without_label($file, $tag_info['view_mode'], $settings); + } + } +} + +/** + * Implements hook_form_alter(). + */ +function media_wysiwyg_view_mode_form_alter(&$form, $form_state, $form_id) { + switch ($form_id) { + case 'media_wysiwyg_format_form': + $file = $form_state['file']; + + // Check to see if a view mode ("format") has already been specified for + // this media item. First, check for a standard form-submitted value. + if (!empty($form_state['values']['format'])) { + $view_mode = $form_state['values']['format']; + } + // Second, check the request for a JSON-encoded value. + elseif (isset($_GET['fields'])) { + $query_fields = drupal_json_decode($_GET['fields']); + if (isset($query_fields['format'])) { + $view_mode = $query_fields['format']; + } + } + // If we were unable to determine a view mode, or we found a view mode + // that does not exist in the list of format options presented on this + // form, use the default view mode. + if (!isset($view_mode) || !array_key_exists($view_mode, $form['options']['format']['#options'])) { + $view_mode = variable_get('media_wysiwyg_wysiwyg_default_view_mode', 'full'); + } + + $form['preview'] = file_view_file($file, $view_mode); + $form['preview']['#prefix'] = '<div id="preview">'; + $form['preview']['#suffix'] = '</div>'; + + if (!isset($form['options']['format']['#default_value'])) { + $form['options']['format']['#default_value'] = $view_mode; + } + $form['options']['format']['#ajax'] = array( + 'callback' => 'media_format_form_preview', + 'wrapper' => 'preview', + ); + + $view_modes = media_wysiwyg_get_wysiwyg_allowed_view_modes($file); + $formats = $options = array(); + foreach ($view_modes as $view_mode => $view_mode_info) { + //@TODO: Display more verbose information about which formatter and what it does. + $options[$view_mode] = $view_mode_info['label']; + + if (variable_get("media_wysiwyg_view_mode_{$file->type}_file_wysiwyg_view_mode_status", FALSE) == TRUE) { + $element = media_wysiwyg_get_file_without_label($file, variable_get("media_wysiwyg_view_mode_{$file->type}_file_wysiwyg_view_mode", 'wysiwyg'), array('wysiwyg' => TRUE)); + } + else { + $element = media_wysiwyg_get_file_without_label($file, $view_mode, array('wysiwyg' => TRUE)); + } + + // Make a pretty name out of this. + $formats[$view_mode] = drupal_render($element); + } + + $form['#formats'] = $formats; + break; + } +} + +/** + * AJAX callback to select the portion of the format form to be updated with a preview. + * + * @param array $form + * An associative array containing the structure of the form. + * @param array $form_state + * An associative array containing the current state of the form. + * + * @return array + * The preview form item. + */ +function media_format_form_preview($form, $form_state) { + return $form['preview']; +} diff --git a/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.test b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.test new file mode 100644 index 000000000..3d27e0749 --- /dev/null +++ b/sites/all/modules/media/modules/media_wysiwyg_view_mode/media_wysiwyg_view_mode.test @@ -0,0 +1,65 @@ +<?php + +/** + * @file + * Tests for media_wysiwyg_view_mode.module. + */ + +/** + * Defines base class for media_wysiwyg_view_mode test cases. + */ +class MediaWYSIWYGViewModeTestHelper extends MediaWYSIWYGTestHelper { + function setUp() { + parent::setUp('media_wysiwyg_view_mode'); + + $web_user = $this->drupalCreateUser(array('administer media wysiwyg view mode', 'view files', 'use media wysiwyg')); + $this->drupalLogin($web_user); + } +} + +/** + * Test configuring view modes available on the format form. + */ +class FormatFormViewModesTest extends MediaWYSIWYGViewModeTestHelper { + public static function getInfo() { + return array( + 'name' => 'Format Form WYSIWYG View Modes', + 'description' => 'Test configuring view modes available on the format form.', + 'group' => 'Media WYSIWYG View Mode', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Configure format form view mode restrictions and ensure that they are followed. + */ + function testAllowedFormatFormViewModes() { + // Load the Media WYSIWYG View Mode administration page. + $this->drupalGet('admin/config/media/wysiwyg-view-mode'); + $this->assertResponse(200, t('The privileged user can access the Media WYSIWYG View Mode administration page.')); + + // Create an image file to test with. + $files = $this->drupalGetTestFiles('image'); + $files[0]->status = FILE_STATUS_PERMANENT; + $file = file_save($files[0]); + $fid = $file->fid; + + // The default view mode should be selected by default. + $this->drupalGet('media/' . $fid . '/format-form'); + $this->assertOptionSelected('edit-format', 'default'); + + // Restrict the use of the default view mode. + variable_set('media_wysiwyg_view_mode_image_wysiwyg_restricted_view_modes_status', TRUE); + $restricted_view_modes = array( + 'default' => 'default', + ); + variable_set('media_wysiwyg_view_mode_image_wysiwyg_restricted_view_modes', $restricted_view_modes); + + // The teaser view mode should now be selected by default. + $this->drupalGet('media/' . $fid . '/format-form'); + $this->assertOptionSelected('edit-format', 'teaser'); + } +} diff --git a/sites/all/modules/media/modules/mediafield/mediafield.info b/sites/all/modules/media/modules/mediafield/mediafield.info new file mode 100644 index 000000000..d1ab05b6e --- /dev/null +++ b/sites/all/modules/media/modules/mediafield/mediafield.info @@ -0,0 +1,12 @@ +name = Media Field +description = "Provides a field type that stores media-specific data. <em>Deprecated by the core File field type.</em>" +package = Media +core = 7.x +dependencies[] = media + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/modules/mediafield/mediafield.install b/sites/all/modules/media/modules/mediafield/mediafield.install new file mode 100644 index 000000000..3e867b89a --- /dev/null +++ b/sites/all/modules/media/modules/mediafield/mediafield.install @@ -0,0 +1,43 @@ +<?php + +/** + * @file + * Install and schema hooks for mediafield. + */ + +/** + * Implements hook_field_schema(). + */ +function mediafield_field_schema($field) { + return array( + 'columns' => array( + 'fid' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + 'title' => array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'data' => array( + 'type' => 'text', + 'not null' => FALSE, + 'size' => 'big', + 'serialize' => TRUE, + // 'description' => 'Used for storing additional information. + // Can be harnessed by widgets', + ), + ), + 'indexes' => array( + 'fid' => array('fid'), + ), + 'foreign keys' => array( + 'file_managed' => array( + 'table' => 'file_managed', + 'columns' => array('fid' => 'fid'), + ), + ), + ); +} diff --git a/sites/all/modules/media/modules/mediafield/mediafield.module b/sites/all/modules/media/modules/mediafield/mediafield.module new file mode 100644 index 000000000..2dd77f3f2 --- /dev/null +++ b/sites/all/modules/media/modules/mediafield/mediafield.module @@ -0,0 +1,323 @@ +<?php + +/** + * @file + * Provide a "Multimedia asset" field. + */ + +/** + * Implements hook_field_info(). + */ +function mediafield_field_info() { + return array( + 'media' => array( + 'label' => t('Multimedia asset'), + 'description' => t('This field stores a reference to a multimedia asset.'), + 'settings' => array(), + 'instance_settings' => array( + 'file_extensions' => variable_get('file_entity_default_allowed_extensions', 'jpg jpeg gif png txt doc docx xls xlsx pdf ppt pptx pps ppsx odt ods odp mp3 mov mp4 m4a m4v mpeg avi ogg oga ogv weba webp webm'), + ), + 'default_widget' => 'media_generic', + 'default_formatter' => 'media', + 'property_type' => 'field_item_file', + 'property_callbacks' => array('entity_metadata_field_file_callback'), + ), + ); +} + +/** + * Implements hook_field_widget_info_alter(). + * + * Alter the media file selector so it is available for media fields. + */ +function mediafield_field_widget_info_alter(&$info) { + $info['media_generic']['field types'][] = 'media'; +} + +/** + * Implements hook_field_instance_settings_form(). + */ +function mediafield_field_instance_settings_form($field, $instance) { + $settings = $instance['settings']; + + // Make the extension list a little more human-friendly by comma-separation. + $extensions = str_replace(' ', ', ', $settings['file_extensions']); + $form['file_extensions'] = array( + '#type' => 'textfield', + '#title' => t('Allowed file extensions for uploaded files'), + '#default_value' => $extensions, + '#description' => t('Separate extensions with a space or comma and do not include the leading dot.'), + '#element_validate' => array('_file_generic_settings_extensions'), + // By making this field required, we prevent a potential security issue + // that would allow files of any type to be uploaded. + '#required' => TRUE, + '#maxlength' => 255, + ); + + return $form; +} + +/** + * Implements hook_field_is_empty(). + */ +function mediafield_field_is_empty($item, $field) { + return empty($item['fid']); +} + +/** + * Implements hook_field_formatter_info(). + */ +function mediafield_field_formatter_info() { + $formatters = array( + 'media' => array( + 'label' => t('Media'), + 'field types' => array('media'), + 'settings' => array('file_view_mode' => 'default'), + ), + ); + + return $formatters; +} + +/** + * Implements hook_field_formatter_settings_form(). + */ +function mediafield_field_formatter_settings_form($field, $instance, $view_mode, $form, &$form_state) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $element = array(); + + if ($display['type'] == 'media') { + $entity_info = entity_get_info('file'); + $options = array('default' => t('Default')); + foreach ($entity_info['view modes'] as $file_view_mode => $file_view_mode_info) { + $options[$file_view_mode] = $file_view_mode_info['label']; + } + $element['file_view_mode'] = array( + '#title' => t('File view mode'), + '#type' => 'select', + '#default_value' => $settings['file_view_mode'], + '#options' => $options, + ); + } + + return $element; +} + +/** + * Implements hook_field_formatter_settings_summary(). + */ +function mediafield_field_formatter_settings_summary($field, $instance, $view_mode) { + $display = $instance['display'][$view_mode]; + $settings = $display['settings']; + + $summary = ''; + + if ($display['type'] == 'media') { + $entity_info = entity_get_info('file'); + $file_view_mode_label = isset($entity_info['view modes'][$settings['file_view_mode']]) ? $entity_info['view modes'][$settings['file_view_mode']]['label'] : t('Default'); + $summary = t('File view mode: @view_mode', array('@view_mode' => $file_view_mode_label)); + } + + return $summary; +} + +/** + * Implements hook_field_formatter_view(). + */ +function mediafield_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) { + $element = array(); + + $files = array(); + foreach ($items as $delta => $item) { + if (!empty($item['file'])) { + $files[$item['fid']] = $item['file']; + } + } + + if (!empty($files)) { + $output = file_view_multiple($files, $display['settings']['file_view_mode'], 0, $langcode); + // Remove the first level from the output array. + $element = reset($output); + } + + return $element; +} + +/** + * Implements hook_field_prepare_view(). + */ +function mediafield_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { + // Collect all file IDs that need loading. + $fids = array(); + foreach ($entities as $id => $entity) { + // Load the files from the files table. + foreach ($items[$id] as $delta => $item) { + if (!empty($item['fid'])) { + $fids[] = $item['fid']; + } + } + } + + // Load the file entities. + $files = file_load_multiple($fids); + + // Add the loaded file entities to the field item array. + foreach ($entities as $id => $entity) { + foreach ($items[$id] as $delta => $item) { + // If the file does not exist, mark the entire item as empty. + if (empty($files[$item['fid']])) { + unset($items[$id][$delta]); + } + else { + $items[$id][$delta]['file'] = $files[$item['fid']]; + } + } + } +} + +/** + * Implements hook_field_validate(). + * + * Possible error codes: + * - 'media_remote_file_type_not_allowed': The remote file is not an allowed + * file type. + */ +function mediafield_field_validate($obj_type, $object, $field, $instance, $langcode, $items, &$errors) { + $allowed_types = array_keys(array_filter($instance['widget']['settings']['allowed_types'])); + + // @TODO: merge in stuff from media_uri_value + foreach ($items as $delta => $item) { + if (empty($item['fid'])) { + return TRUE; + //@TODO: make support for submiting with just a URI here? + } + + $file = file_load($item['fid']); + + // Only validate allowed types if the file is remote and not local. + if (!file_entity_file_is_local($file)) { + if (!in_array($file->type, $allowed_types)) { + $errors[$field['field_name']][$langcode][$delta][] = array( + 'error' => 'media_remote_file_type_not_allowed', + 'message' => t('%name: Only remote files with the following types are allowed: %types-allowed.', array('%name' => t($instance['label']), '%types-allowed' => !empty($allowed_types) ? implode(', ', $allowed_types) : t('no file types selected'))), + ); + } + } + } +} + +/** + * Implements_hook_field_widget_error(). + */ +function mediafield_field_widget_error($element, $error, $form, &$form_state) { + form_error($element['fid'], $error['message']); +} + +/** + * @todo The following hook_field_(insert|update|delete|delete_revision) + * implementations are nearly identical to the File module implementations of + * the same field hooks. The only differences are: + * - We pass 'media' rather than 'file' as the module argument to the + * file_usage_(add|delete)() functions. + * - We do not delete the file / media entity when its usage count goes to 0. + * We should submit a core patch to File module to make it flexible with + * respect to the above, so that we can reuse its implementation rather than + * duplicating it. + */ + +/** + * Implements hook_field_insert(). + */ +function mediafield_field_insert($entity_type, $entity, $field, $instance, $langcode, &$items) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // Add a new usage of each uploaded file. + foreach ($items as $item) { + $file = (object) $item; + file_usage_add($file, 'mediafield', $entity_type, $id); + } +} + +/** + * Implements hook_field_update(). + * + * Checks for files that have been removed from the object. + */ +function mediafield_field_update($entity_type, $entity, $field, $instance, $langcode, &$items) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // On new revisions, all files are considered to be a new usage and no + // deletion of previous file usages are necessary. + if (!empty($entity->revision)) { + foreach ($items as $item) { + $file = (object) $item; + file_usage_add($file, 'mediafield', $entity_type, $id); + } + return; + } + + // Build a display of the current FIDs. + $current_fids = array(); + foreach ($items as $item) { + $current_fids[] = $item['fid']; + } + + // Compare the original field values with the ones that are being saved. + $original_fids = array(); + if (!empty($entity->original->{$field['field_name']}[$langcode])) { + foreach ($entity->original->{$field['field_name']}[$langcode] as $original_item) { + $original_fids[] = $original_item['fid']; + if (isset($original_item['fid']) && !in_array($original_item['fid'], $current_fids)) { + // Decrement the file usage count by 1. + $file = (object) $original_item; + file_usage_delete($file, 'mediafield', $entity_type, $id, 1); + } + } + } + + // Add new usage entries for newly added files. + foreach ($items as $item) { + if (!in_array($item['fid'], $original_fids)) { + $file = (object) $item; + file_usage_add($file, 'mediafield', $entity_type, $id); + } + } +} + +/** + * Implements hook_field_delete(). + */ +function mediafield_field_delete($entity_type, $entity, $field, $instance, $langcode, &$items) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + + // Delete all file usages within this entity. + foreach ($items as $delta => $item) { + $file = (object) $item; + file_usage_delete($file, 'mediafield', $entity_type, $id, 0); + } +} + +/** + * Implements hook_field_delete_revision(). + */ +function mediafield_field_delete_revision($entity_type, $entity, $field, $instance, $langcode, &$items) { + list($id, $vid, $bundle) = entity_extract_ids($entity_type, $entity); + foreach ($items as $delta => $item) { + // @TODO: Not sure if this is correct + $file = (object)$item; + if (file_usage_delete($file, 'mediafield', $entity_type, $id, 1)) { + $items[$delta] = NULL; + } + } +} + +/** + * Implements hook_views_api(). + */ +function mediafield_views_api() { + return array( + 'api' => 3, + ); +} diff --git a/sites/all/modules/media/modules/mediafield/mediafield.views.inc b/sites/all/modules/media/modules/mediafield/mediafield.views.inc new file mode 100644 index 000000000..bc1e24065 --- /dev/null +++ b/sites/all/modules/media/modules/mediafield/mediafield.views.inc @@ -0,0 +1,25 @@ +<?php + +/** + * @file + * Provide Views data and handlers for mediafield. + */ + +/** + * Implements hook_field_views_data(). + */ +function mediafield_field_views_data($field) { + $data = field_views_field_default_views_data($field); + foreach ($data as $table_name => $table_data) { + // Add the relationship only on the fid field. + $data[$table_name][$field['field_name'] . '_fid']['relationship'] = array( + 'handler' => 'views_handler_relationship', + 'base' => 'file_managed', + 'entity type' => 'file', + 'base field' => 'fid', + 'label' => t('file from !field_name', array('!field_name' => $field['field_name'])), + ); + } + + return $data; +} diff --git a/sites/all/modules/media/templates/media-dialog-page.tpl.php b/sites/all/modules/media/templates/media-dialog-page.tpl.php new file mode 100644 index 000000000..f779c1dc2 --- /dev/null +++ b/sites/all/modules/media/templates/media-dialog-page.tpl.php @@ -0,0 +1,91 @@ +<?php + +/** + * @file + * Default theme implementation to display a single Drupal page. + * + * Available variables: + * + * General utility variables: + * - $base_path: The base URL path of the Drupal installation. At the very + * least, this will always default to /. + * - $directory: The directory the template is located in, e.g. modules/system + * or themes/garland. + * - $is_front: TRUE if the current page is the front page. + * - $logged_in: TRUE if the user is registered and signed in. + * - $is_admin: TRUE if the user has permission to access administration pages. + * + * Site identity: + * - $front_page: The URL of the front page. Use this instead of $base_path, + * when linking to the front page. This includes the language domain or + * prefix. + * - $logo: The path to the logo image, as defined in theme configuration. + * - $site_name: The name of the site, empty when display has been disabled + * in theme settings. + * - $site_slogan: The slogan of the site, empty when display has been disabled + * in theme settings. + * + * Navigation: + * - $main_menu (array): An array containing the Main menu links for the + * site, if they have been configured. + * - $secondary_menu (array): An array containing the Secondary menu links for + * the site, if they have been configured. + * - $breadcrumb: The breadcrumb trail for the current page. + * + * Page content (in order of occurrence in the default page.tpl.php): + * - $title_prefix (array): An array containing additional output populated by + * modules, intended to be displayed in front of the main title tag that + * appears in the template. + * - $title: The page title, for use in the actual HTML content. + * - $title_suffix (array): An array containing additional output populated by + * modules, intended to be displayed after the main title tag that appears in + * the template. + * - $messages: HTML for status and error messages. Should be displayed + * prominently. + * - $tabs (array): Tabs linking to any sub-pages beneath the current page + * (e.g., the view and edit tabs when displaying a node). + * - $action_links (array): Actions local to the page, such as 'Add menu' on the + * menu administration interface. + * - $feed_icons: A string of all feed icons for the current page. + * - $node: The node object, if there is an automatically-loaded node + * associated with the page, and the node ID is the second argument + * in the page's path (e.g. node/12345 and node/12345/revisions, but not + * comment/reply/12345). + * + * Regions: + * - $page['help']: Dynamic help text, mostly for admin pages. + * - $page['highlight']: Items for the highlighted content region. + * - $page['content']: The main content of the current page. + * - $page['sidebar_first']: Items for the first sidebar. + * - $page['sidebar_second']: Items for the second sidebar. + * - $page['header']: Items for the header region. + * - $page['footer']: Items for the footer region. + * + * @see template_preprocess() + * @see template_preprocess_page() + * @see template_process() + */ +?> + +<?php if (isset($messages)) { print $messages; } ?> +<div id="media-browser-page-wrapper"> + <div id="media-browser-page"> + <div id="media-browser-tabset"> + <div id="branding" class="clearfix"> + <div> + <h1><?php print render($page['content']['system_main']['title']); ?></h1> + </div> + <div id="media-tabs-wrapper"> + <?php print render($page['content']['system_main']['tabset']['tabs']); ?> + </div> + </div> + <?php print render($page['content']['system_main']['tabset']['panes']); ?> + </div> <!-- /#media-tabs-set --> + </div> <!-- /#media-browser-page --> +</div> <!-- /#media-browser-page-wrapper --> + +<?php + hide($page['content']['system_main']['tabset']); + hide($page['content']['system_main']['title']); + print render($page['content']); +?> diff --git a/sites/all/modules/media/tests/includes/MediaModuleTest.inc b/sites/all/modules/media/tests/includes/MediaModuleTest.inc new file mode 100644 index 000000000..5e6c5d5e9 --- /dev/null +++ b/sites/all/modules/media/tests/includes/MediaModuleTest.inc @@ -0,0 +1,30 @@ +<?php + +/** + * @file + * Definition of MediaModuleTest. + */ + +/** + * Test browser plugin. + */ +class MediaModuleTest extends MediaBrowserPlugin { + /** + * Implements MediaBrowserPluginInterface::access(). + */ + public function access($account = NULL) { + return TRUE; + } + + /** + * Implements MediaBrowserPlugin::view(). + */ + public function view() { + $build = array(); + $build['test'] = array( + '#markup' => '<p>' . t('Test browser plugin output.') . '</p>', + ); + + return $build; + } +} diff --git a/sites/all/modules/media/tests/media.test b/sites/all/modules/media/tests/media.test new file mode 100644 index 000000000..3594aaf01 --- /dev/null +++ b/sites/all/modules/media/tests/media.test @@ -0,0 +1,1187 @@ +<?php + +/** + * @file + * Tests for media.module. + */ + +/** + * Provides methods specifically for testing Media module's field handling. + */ +class MediaFileFieldTestCase extends DrupalWebTestCase { + protected $admin_user; + + function setUp() { + // Since this is a base class for many test cases, support the same + // flexibility that DrupalWebTestCase::setUp() has for the modules to be + // passed in as either an array or a variable number of string arguments. + $modules = func_get_args(); + if (isset($modules[0]) && is_array($modules[0])) { + $modules = $modules[0]; + } + $modules[] = 'media'; + $modules[] = 'media_module_test'; + parent::setUp($modules); + $this->admin_user = $this->drupalCreateUser(array('access content', 'view files', 'view own files', 'access media browser', 'access administration pages', 'administer site configuration', 'administer users', 'administer permissions', 'administer content types', 'administer nodes', 'administer files', 'bypass node access', 'bypass file access')); + $this->drupalLogin($this->admin_user); + } + + /** + * Retrieves a sample file of the specified type. + */ + function getTestFile($type_name, $size = NULL) { + // Get a file to upload. + $file = current($this->drupalGetTestFiles($type_name, $size)); + + // Add a filesize property to files as would be read by file_load(). + $file->filesize = filesize($file->uri); + + return $file; + } + + /** + * Creates a new file entity. + * + * @param $settings + * A list of settings that will be added to the entity defaults. + */ + protected function createFileEntity($settings = array()) { + $file = new stdClass(); + + // Populate defaults array. + $settings += array( + 'filepath' => 'Файл для тестирования ' . $this->randomName(), // Prefix with non-latin characters to ensure that all file-related tests work with international filenames. + 'filemime' => 'text/plain', + 'uid' => 1, + 'timestamp' => REQUEST_TIME, + 'status' => FILE_STATUS_PERMANENT, + 'contents' => "file_put_contents() doesn't seem to appreciate empty strings so let's put in some data.", + 'scheme' => file_default_scheme(), + 'type' => NULL, + ); + + $filepath = $settings['scheme'] . '://' . $settings['filepath']; + + file_put_contents($filepath, $settings['contents']); + $this->assertTrue(is_file($filepath), t('The test file exists on the disk.'), 'Create test file'); + + $file = new stdClass(); + $file->uri = $filepath; + $file->filename = drupal_basename($file->uri); + $file->filemime = $settings['filemime']; + $file->uid = $settings['uid']; + $file->timestamp = $settings['timestamp']; + $file->filesize = filesize($file->uri); + $file->status = $settings['status']; + $file->type = $settings['type']; + + // The file type is used as a bundle key, and therefore, must not be NULL. + if (!isset($file->type)) { + $file->type = FILE_TYPE_NONE; + } + + // If the file isn't already assigned a real type, determine what type should + // be assigned to it. + if ($file->type === FILE_TYPE_NONE) { + $type = file_get_type($file); + if (isset($type)) { + $file->type = $type; + } + } + + // Write the record directly rather than calling file_save() so we don't + // invoke the hooks. + $this->assertNotIdentical(drupal_write_record('file_managed', $file), FALSE, t('The file was added to the database.'), 'Create test file'); + + return $file; + } + + /** + * Creates a new file field. + * + * @param $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param $type_name + * The node type that this field will be added to. + * @param $field_settings + * A list of field settings that will be added to the defaults. + * @param $instance_settings + * A list of instance settings that will be added to the instance defaults. + * @param $widget_settings + * A list of widget settings that will be added to the widget defaults. + */ + function createFileField($name, $type_name, $field_settings = array(), $instance_settings = array(), $widget_settings = array()) { + $field = array( + 'field_name' => $name, + 'type' => 'file', + 'settings' => array(), + 'cardinality' => !empty($field_settings['cardinality']) ? $field_settings['cardinality'] : 1, + ); + $field['settings'] = array_merge($field['settings'], $field_settings); + field_create_field($field); + + $this->attachFileField($name, 'node', $type_name, $instance_settings, $widget_settings); + } + + /** + * Attaches a file field to an entity. + * + * @param $name + * The name of the new field (all lowercase), exclude the "field_" prefix. + * @param $entity_type + * The entity type this field will be added to. + * @param $bundle + * The bundle this field will be added to. + * @param $field_settings + * A list of field settings that will be added to the defaults. + * @param $instance_settings + * A list of instance settings that will be added to the instance defaults. + * @param $widget_settings + * A list of widget settings that will be added to the widget defaults. + */ + function attachFileField($name, $entity_type, $bundle, $instance_settings = array(), $widget_settings = array()) { + $instance = array( + 'field_name' => $name, + 'label' => $name, + 'entity_type' => $entity_type, + 'bundle' => $bundle, + 'required' => !empty($instance_settings['required']), + 'settings' => array(), + 'widget' => array( + 'type' => 'media_generic', + 'settings' => array(), + ), + ); + $instance['settings'] = array_merge($instance['settings'], $instance_settings); + $instance['widget']['settings'] = array_merge($instance['widget']['settings'], $widget_settings); + field_create_instance($instance); + } + + /** + * Attaches a file to a node. + */ + function attachNodeFile($file, $field_name, $nid_or_type, $new_revision = TRUE, $extras = array()) { + $langcode = LANGUAGE_NONE; + $edit = array( + "title" => $this->randomName(), + 'revision' => (string) (int) $new_revision, + ); + + if (is_numeric($nid_or_type)) { + $nid = $nid_or_type; + } + else { + // Add a new node. + $extras['type'] = $nid_or_type; + $node = $this->drupalCreateNode($extras); + $nid = $node->nid; + // Save at least one revision to better simulate a real site. + $this->drupalCreateNode(get_object_vars($node)); + $node = node_load($nid, NULL, TRUE); + $this->assertNotEqual($nid, $node->vid, 'Node revision exists.'); + } + + // Attach a file to the node. + $edit['media[' . $field_name . '_' . $langcode . '_0]'] = $file->fid; + $this->drupalPost("node/$nid/edit", $edit, t('Save')); + + return $nid; + } + + /** + * Replaces a file within a node. + */ + function replaceNodeFile($file, $field_name, $nid, $new_revision = TRUE) { + $edit = array( + $field_name . '[' . LANGUAGE_NONE . '][0][fid]' => $file->fid, + 'revision' => (string) (int) $new_revision, + ); + + $this->drupalPost('node/' . $nid . '/edit', array(), t('Remove')); + $this->drupalPost(NULL, $edit, t('Save')); + } + + /** + * Asserts that a file exists physically on disk. + */ + function assertFileExists($file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri)); + $this->assertTrue(is_file($file->uri), $message); + } + + /** + * Asserts that a file exists in the database. + */ + function assertFileEntryExists($file, $message = NULL) { + entity_get_controller('file')->resetCache(); + $db_file = file_load($file->fid); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri)); + $this->assertEqual($db_file->uri, $file->uri, $message); + } + + /** + * Asserts that a file does not exist on disk. + */ + function assertFileNotExists($file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file exists on the disk.', array('%file' => $file->uri)); + $this->assertFalse(is_file($file->uri), $message); + } + + /** + * Asserts that a file does not exist in the database. + */ + function assertFileEntryNotExists($file, $message) { + entity_get_controller('file')->resetCache(); + $message = isset($message) ? $message : format_string('File %file exists in database at the correct path.', array('%file' => $file->uri)); + $this->assertFalse(file_load($file->fid), $message); + } + + /** + * Asserts that a file's status is set to permanent in the database. + */ + function assertFileIsPermanent($file, $message = NULL) { + $message = isset($message) ? $message : format_string('File %file is permanent.', array('%file' => $file->uri)); + $this->assertTrue($file->status == FILE_STATUS_PERMANENT, $message); + } +} + +/** + * Tests the 'media' element type. + * + * @todo Create a MediaFileTestCase base class and move MediaFileFieldTestCase + * methods that aren't related to fields into it. + */ +class MediaElementTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media element test', + 'description' => 'Tests the media element type.', + 'group' => 'Media', + ); + } + + /** + * Tests the media element type. + */ + function testMedia() { + // Check that $element['#size'] is passed to the child upload element. + $this->drupalGet('media/test'); + $this->assertFieldByXpath('//input[@name="media[nested_media]" and @size="13"]', NULL, 'The custom #size attribute is passed to the child upload element.'); + + // Perform the tests with all permutations of $form['#tree'] and + // $element['#extended']. + foreach (array(0, 1) as $tree) { + foreach (array(0, 1) as $extended) { + $test_file = $this->getTestFile('text'); + $test_file->uid = $this->admin_user->uid; + $test_file = file_save($test_file); + $path = 'media/test/' . $tree . '/' . $extended; + $input_base_name = $tree ? 'nested_media' : 'media'; + + // Submit without a file. + $this->drupalPost($path, array(), t('Save')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submitted without a file.'); + + // Submit a new file, without using the Attach button. + $edit = array('media[' . $input_base_name . ']' => $test_file->fid); + $this->drupalPost($path, $edit, t('Save')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => $test_file->fid)), 'Submit handler has correct file info.'); + + // Now, test the Attach and Remove buttons, with and without Ajax. + foreach (array(FALSE) as $ajax) { + // Attach, then Submit. + $this->drupalGet($path); + $edit = array('media[' . $input_base_name . ']' => $test_file->fid); + if ($ajax) { + $this->drupalPostAJAX(NULL, $edit, $input_base_name . '_attach_button'); + } + else { + $this->drupalPost(NULL, $edit, t('Attach')); + } + $this->drupalPost(NULL, array(), t('Save')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => $test_file->fid)), 'Submit handler has correct file info.'); + + // Attach, then Remove, then Submit. + $this->drupalGet($path); + $edit = array('media[' . $input_base_name . ']' => $test_file->fid); + if ($ajax) { + $this->drupalPostAJAX(NULL, $edit, $input_base_name . '_attach_button'); + $this->drupalPostAJAX(NULL, array(), $input_base_name . '_remove_button'); + } + else { + $this->drupalPost(NULL, $edit, t('Attach')); + $this->drupalPost(NULL, array(), t('Remove')); + } + $this->drupalPost(NULL, array(), t('Save')); + $this->assertRaw(t('The file id is %fid.', array('%fid' => 0)), 'Submission after file attachment and removal was successful.'); + } + } + } + } +} + +/** + * Test media file administration page functionality. + */ +class MediaAdminTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media file administration', + 'description' => 'Test media file administration page functionality.', + 'group' => 'Media', + ); + } + + function setUp() { + parent::setUp(); + // Remove the "view files" permission which is set + // by default for all users so we can test this permission + // correctly. + $roles = user_roles(); + foreach ($roles as $rid => $role) { + user_role_revoke_permissions($rid, array('view files')); + } + + $this->base_user_1 = $this->drupalCreateUser(array('administer files')); + $this->base_user_2 = $this->drupalCreateUser(array('administer files', 'view own private files')); + $this->base_user_3 = $this->drupalCreateUser(array('administer files', 'view private files')); + $this->base_user_4 = $this->drupalCreateUser(array('administer files', 'edit any document files', 'delete any document files', 'edit any image files', 'delete any image files')); + } + + /** + * Tests that the table sorting works on the files admin pages. + */ + function testFilesAdminSort() { + $i = 0; + foreach (array('dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB') as $prefix) { + $this->createFileEntity(array('filepath' => $prefix . $this->randomName(6), 'timestamp' => $i)); + $i++; + } + + // Test that the default sort by file_managed.timestamp DESC actually fires properly. + $files_query = db_select('file_managed', 'fm') + ->fields('fm', array('fid')) + ->orderBy('timestamp', 'DESC') + ->execute() + ->fetchCol(); + + $files_form = array(); + $this->drupalGet('admin/content/file/thumbnails'); + foreach ($this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]/@data-fid') as $input) { + $files_form[] = $input; + } + $this->assertEqual($files_query, $files_form, 'Files are sorted in the form according to the default query.'); + + // Compare the rendered HTML node list to a query for the files ordered by + // filename to account for possible database-dependent sort order. + $files_query = db_select('file_managed', 'fm') + ->fields('fm', array('fid')) + ->orderBy('filename') + ->execute() + ->fetchCol(); + + $files_form = array(); + $this->drupalGet('admin/content/file/thumbnails', array('query' => array('sort' => 'asc', 'order' => 'Title'))); + foreach ($this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]/@data-fid') as $input) { + $files_form[] = $input; + } + $this->assertEqual($files_query, $files_form, 'Files are sorted in the form the same as they are in the query.'); + } + + /** + * Tests files overview with different user permissions. + */ + function testFilesAdminPages() { + $files['public_image'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_1->uid, 'type' => 'image')); + $files['public_document'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_2->uid, 'type' => 'document')); + $files['private_image'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_1->uid, 'type' => 'image')); + $files['private_document'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_2->uid, 'type' => 'document')); + + // Verify view and edit links for any file. + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + foreach ($files as $file) { + $this->assertLinkByHref('file/' . $file->fid); + $this->assertLinkByHref('file/' . $file->fid . '/edit'); + // Verify tableselect. + $this->assertFieldByName('files[' . $file->fid . ']', '', t('Tableselect found.')); + } + + // Verify no operation links are displayed for regular users. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_1); + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + $this->assertLinkByHref('file/' . $files['public_image']->fid); + $this->assertLinkByHref('file/' . $files['public_document']->fid); + $this->assertNoLinkByHref('file/' . $files['public_image']->fid . '/edit'); + $this->assertNoLinkByHref('file/' . $files['public_document']->fid . '/edit'); + + // Verify no tableselect. + $this->assertNoFieldByName('files[' . $files['public_image']->fid . ']', '', t('No tableselect found.')); + + // Verify private file is displayed with permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_2); + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + $this->assertLinkByHref('file/' . $files['private_document']->fid); + // Verify no operation links are displayed. + $this->assertNoLinkByHref('file/' . $files['private_document']->fid . '/edit'); + + // Verify user cannot see private file of other users. + $this->assertNoLinkByHref('file/' . $files['private_image']->fid); + $this->assertNoLinkByHref('file/' . $files['private_image']->fid . '/edit'); + + // Verify no tableselect. + $this->assertNoFieldByName('files[' . $files['private_document']->fid . ']', '', t('No tableselect found.')); + + // Verify private file is displayed with permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_3); + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + + // Verify user can see private file of other users. + $this->assertLinkByHref('file/' . $files['private_document']->fid); + $this->assertLinkByHref('file/' . $files['private_image']->fid); + + // Verify operation links are displayed for users with appropriate permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_4); + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + foreach ($files as $file) { + $this->assertLinkByHref('file/' . $file->fid); + $this->assertLinkByHref('file/' . $file->fid . '/edit'); + } + + // Verify file access can be bypassed. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->drupalGet('admin/content/file/thumbnails'); + $this->assertResponse(200); + foreach ($files as $file) { + $this->assertLinkByHref('file/' . $file->fid); + $this->assertLinkByHref('file/' . $file->fid . '/edit'); + } + } +} + +/** + * Tests the media hooks. + */ +class MediaHooksTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media hooks test', + 'description' => 'Tests the Media hooks.', + 'group' => 'Media', + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Tests that the media browser hooks. + */ + function testMediaBrowserHooks() { + // Enable media_module_test.module's hook_media_browser_plugin_info() + // implementation and ensure it is working as designed. + variable_set('media_module_test_media_browser_plugin_info', TRUE); + + $this->drupalGet('media/browser'); + $this->assertRaw(t('Media module test'), 'Custom browser plugin found.'); + + // Enable media_module_test.module's hook_media_browser_plugin_info_alter() + // implementation and ensure it is working as designed. + variable_set('media_module_test_media_browser_plugin_info_alter', TRUE); + + $this->drupalGet('media/browser'); + $this->assertRaw(t('Altered plugin title'), 'Custom browser plugin was successfully altered.'); + + // Enable media_module_test.module's hook_media_browser_plugins_alter() + // implementation and ensure it is working as designed. + variable_set('media_module_test_media_browser_plugins_alter', TRUE); + + $this->drupalGet('media/browser'); + $this->assertRaw(t('Altered browser plugin output.'), 'Custom browser plugin was successfully altered before being rendered.'); + } +} + +/** + * Tests the media browser 'Library' tab. + */ +class MediaBrowserLibraryTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media browser library test', + 'description' => 'Tests the media browser library tab.', + 'group' => 'Media', + ); + } + + function setUp() { + parent::setUp(); + $this->base_user_1 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files')); + $this->base_user_2 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files', 'view own private files')); + $this->base_user_3 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files', 'view private files')); + } + + /** + * Tests that the views sorting works on the media browser 'Library' tab. + */ + function testFilesBrowserSort() { + // Load only the 'Library' tab of the media browser. + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_1' => 'media_default--media_browser_1', + ), + ), + ); + + $i = 0; + foreach (array('dd', 'aa', 'DD', 'bb', 'cc', 'CC', 'AA', 'BB') as $prefix) { + $this->createFileEntity(array('filepath' => $prefix . $this->randomName(6), 'timestamp' => $i)); + $i++; + } + + // Test that the default sort by file_managed.timestamp DESC actually fires properly. + $files_query = db_select('file_managed', 'fm') + ->fields('fm', array('fid')) + ->orderBy('timestamp', 'DESC') + ->execute() + ->fetchCol(); + + $files_form = array(); + $this->drupalGet('media/browser', $options); + foreach ($this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]/@data-fid') as $input) { + $files_form[] = $input; + } + $this->assertEqual($files_query, $files_form, 'Files are sorted in the form according to the default query.'); + } + + /** + * Tests media browser 'Library' tab with different user permissions. + */ + function testFilesBrowserLibrary() { + // Load only the 'Library' tab of the media browser. + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_1' => 'media_default--media_browser_1', + ), + ), + ); + + $files['public_image'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_1->uid, 'type' => 'image')); + $files['public_document'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_2->uid, 'type' => 'document')); + $files['private_image'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_1->uid, 'type' => 'image')); + $files['private_document'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_2->uid, 'type' => 'document')); + + // Verify all files are displayed for administrators. + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + foreach ($files as $file) { + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $file->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $file->fid))); + } + + // Verify public files are displayed. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_1); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['public_image']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['public_image']->fid))); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['public_document']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['public_document']->fid))); + + // Verify private file is displayed with permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_2); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_document']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['private_document']->fid))); + + // Verify user cannot see private file of other users. + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_image']->fid, + )); + $this->assertNoFieldByXPath($xpath, TRUE, format_string('File with file ID %fid not found.', array('%fid' => $files['private_image']->fid))); + + // Verify private file is displayed with permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_3); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + + // Verify user can see private file of other users. + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_document']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['private_document']->fid))); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_image']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['private_image']->fid))); + + // Verify file access can be bypassed. + $this->drupalLogout(); + $this->drupalLogin($this->admin_user); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + foreach ($files as $file) { + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $file->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $file->fid))); + } + } +} + +/** + * Tests the media browser settings. + */ +class MediaBrowserSettingsTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media browser settings test', + 'description' => 'Tests the media browser settings.', + 'group' => 'Media', + ); + } + + function setUp() { + parent::setUp('file_test'); + } + + /** + * Tests the media browser settings. + */ + function testBrowserSettings() { + $settings = array( + 'scheme' => array('public', 'private'), + 'type' => array('image', 'document'), + 'extension' => array('jpg', 'txt'), + ); + + // Perform the tests with unique permutations of $scheme, $type and + // $extension. + foreach ($settings['scheme'] as $scheme) { + foreach ($settings['type'] as $type) { + foreach ($settings['extension'] as $extension) { + $file = $this->createFileEntity(array('scheme' => $scheme, 'uid' => $this->admin_user->uid, 'type' => $type, 'filemime' => media_get_extension_mimetype($extension))); + + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_1' => 'media_default--media_browser_1', + ), + 'schemes' => array($scheme), + 'types' => array($type), + 'file_extensions' => $extension, + ), + ); + + // Verify that the file is displayed. + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $file->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $file->fid))); + + // Verify that no other files are also displayed. + $files = $this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]'); + $this->assertEqual(count($files), 1, 'There is only one file that matches the current browser configuration.'); + } + } + } + + // Perform the tests with none and all of the restrictions. + foreach (array('none', 'all') as $restrictions) { + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_1' => 'media_default--media_browser_1', + ), + ), + ); + + switch ($restrictions) { + case 'none': + $options['query']['schemes'] = array(); + $options['query']['types'] = array(); + $options['query']['file_extensions'] = array(); + break; + case 'all': + $options['query']['schemes'] = $settings['scheme']; + $options['query']['types'] = $settings['type']; + $options['query']['file_extensions'] = implode(' ', $settings['extension']); + break; + } + + // Verify that all of the files are displayed. + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $files = $this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]'); + $this->assertEqual(count($files), 8, format_string('All of the files were displayed when %restrictions of the restrictions were enabled.', array('%restrictions' => $restrictions))); + } + + // Verify that extension restrictions do not affect remote files. + $scheme = 'dummy-remote'; + $type = 'video'; + $extension = 'mp4'; + + $file = $this->createFileEntity(array('scheme' => $scheme, 'uid' => $this->admin_user->uid, 'type' => $type, 'filemime' => media_get_extension_mimetype($extension))); + + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_1' => 'media_default--media_browser_1', + ), + 'schemes' => array($scheme, 'public'), // Include a local stream wrapper in order to trigger extension restrictions. + 'types' => array($type), + 'file_extensions' => 'fake', // Use an invalid file extension to ensure that it does not affect restrictions. + ), + ); + + // Verify that the file is displayed. + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $file->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $file->fid))); + + // Verify that no other files are also displayed. + $files = $this->xpath('//ul[@class="media-list-thumbnails"]/li/div[@data-fid]'); + $this->assertEqual(count($files), 1, 'There is only one file that matches the current browser configuration.'); + } +} + +/** + * Tests the media browser 'My files' tab. + */ +class MediaBrowserMyFilesTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media browser my files test', + 'description' => 'Tests the media browser my files tab.', + 'group' => 'Media', + ); + } + + function setUp() { + parent::setUp(); + $this->base_user_1 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files')); + $this->base_user_2 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files', 'view own files')); + $this->base_user_3 = $this->drupalCreateUser(array('access media browser', 'create files', 'administer files', 'view own files', 'view own private files')); + } + + /** + * Tests media browser 'My files' tab with different user permissions. + */ + function testFilesBrowserMyFiles() { + // Load only the 'My files' tab of the media browser. + $options = array( + 'query' => array( + 'enabledPlugins' => array( + 'media_default--media_browser_my_files' => 'media_default--media_browser_my_files', + ), + ), + ); + + $files['public_image'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_2->uid, 'type' => 'image')); + $files['public_document'] = $this->createFileEntity(array('scheme' => 'public', 'uid' => $this->base_user_3->uid, 'type' => 'document')); + $files['private_image'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_2->uid, 'type' => 'image')); + $files['private_document'] = $this->createFileEntity(array('scheme' => 'private', 'uid' => $this->base_user_3->uid, 'type' => 'document')); + + // Verify administrators do not have any special access to files. + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + foreach ($files as $file) { + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $file->fid, + )); + $this->assertNoFieldByXPath($xpath, TRUE, format_string('File with file ID %fid not found.', array('%fid' => $file->fid))); + } + + // Verify users require the 'view own files' permission in order to access + // the 'My files' tab. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_1); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//div[@class="media_default--media_browser_my_files"]'); + $this->assertNoFieldByXPath($xpath, TRUE, 'User with insufficient permissions was unable to view the My files tab.'); + + // Verify own public files are displayed. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_2); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['public_image']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['public_image']->fid))); + + // Verify own private file is displayed with permission. + $this->drupalLogout(); + $this->drupalLogin($this->base_user_3); + $this->drupalGet('media/browser', $options); + $this->assertResponse(200); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_document']->fid, + )); + $this->assertFieldByXPath($xpath, TRUE, format_string('File with file ID %fid found.', array('%fid' => $files['private_document']->fid))); + + // Verify user cannot see files of other users. + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['public_image']->fid, + )); + $this->assertNoFieldByXPath($xpath, TRUE, format_string('File with file ID %fid not found.', array('%fid' => $files['public_image']->fid))); + $xpath = $this->buildXPathQuery('//ul[@class="media-list-thumbnails"]/li/div[@data-fid=:fid]/@data-fid', array( + ':fid' => $files['private_image']->fid, + )); + $this->assertNoFieldByXPath($xpath, TRUE, format_string('File with file ID %fid not found.', array('%fid' => $files['private_image']->fid))); + } +} + +/** + * Tests the 'media' element type settings. + */ +class MediaElementSettingsTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media element settings test', + 'description' => 'Tests the media element type JavaScript settings.', + 'group' => 'Media', + ); + } + + /** + * Tests the media element type settings. + */ + function testElementSettings() { + $form = array( + '#type' => 'media', + ); + drupal_render($form); + $javascript = drupal_get_js(); + $global = array( + 'media' => array( + 'global' => array( + 'global' => array( + 'types' => array(), + 'schemes' => array(), + ), + ), + ), + ); + $settings = drupal_json_encode(drupal_array_merge_deep_array($global)); + $this->assertTrue(strpos($javascript, $settings) > 0, 'Rendered media element adds the global settings.'); + } + + /** + * Tests the media file field widget settings. + */ + function testWidgetSettings() { + // Use 'page' instead of 'article', so that the 'article' image field does + // not conflict with this test. If in the future the 'page' type gets its + // own default file or image field, this test can be made more robust by + // using a custom node type. + $type_name = 'page'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, $type_name); + + $javascript = $this->drupalGet("node/add/$type_name"); + $field_widget = array( + 'elements' => array( + '#edit-' . $field_name . '-' . LANGUAGE_NONE . '-0-upload' => array( + 'global' => array( + 'types' => array( + 'image' => 'image', + ), + 'enabledPlugins' => array(), + 'schemes' => array( + 'public' => 'public', + ), + 'file_directory' => '', + 'file_extensions' => 'txt', + 'max_filesize' => '', + 'uri_scheme' => 'public', + ), + ), + ), + ); + $settings = drupal_json_encode(drupal_array_merge_deep_array($field_widget)); + $this->assertTrue(strpos($javascript, $settings) > 0, 'Media file field widget adds element-specific settings.'); + } +} + +/** + * Tests file handling with node revisions. + */ +class MediaFileFieldRevisionTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media file field revision test', + 'description' => 'Test creating and deleting revisions with files attached.', + 'group' => 'Media', + ); + } + + /** + * Tests creating multiple revisions of a node and managing attached files. + * + * Expected behaviors: + * - Adding a new revision will make another entry in the field table, but + * the original file will not be duplicated. + * - Deleting a revision should not delete the original file if the file + * is in use by another revision. + * - When the last revision that uses a file is deleted, the original file + * should be deleted also. + */ + function testRevisions() { + $type_name = 'article'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, $type_name); + + // Attach the same fields to users. + $this->attachFileField($field_name, 'user', 'user'); + + $test_file = $this->getTestFile('text'); + $test_file->uid = $this->admin_user->uid; + $test_file = file_save($test_file); + + // Create a new node with the uploaded file. + $nid = $this->attachNodeFile($test_file, $field_name, $type_name); + + // Check that the file exists on disk and in the database. + $node = node_load($nid, NULL, TRUE); + $node_file_r1 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $node_vid_r1 = $node->vid; + $this->assertFileExists($node_file_r1, 'New file saved to disk on node creation.'); + $this->assertFileEntryExists($node_file_r1, 'File entry exists in database on node creation.'); + $this->assertFileIsPermanent($node_file_r1, 'File is permanent.'); + + // Upload another file to the same node in a new revision. + $this->replaceNodeFile($test_file, $field_name, $nid); + $node = node_load($nid, NULL, TRUE); + $node_file_r2 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $node_vid_r2 = $node->vid; + $this->assertFileExists($node_file_r2, 'Replacement file exists on disk after creating new revision.'); + $this->assertFileEntryExists($node_file_r2, 'Replacement file entry exists in database after creating new revision.'); + $this->assertFileIsPermanent($node_file_r2, 'Replacement file is permanent.'); + + // Check that the original file is still in place on the first revision. + $node = node_load($nid, $node_vid_r1, TRUE); + $this->assertEqual($node_file_r1, (object) $node->{$field_name}[LANGUAGE_NONE][0], 'Original file still in place after replacing file in new revision.'); + $this->assertFileExists($node_file_r1, 'Original file still in place after replacing file in new revision.'); + $this->assertFileEntryExists($node_file_r1, 'Original file entry still in place after replacing file in new revision'); + $this->assertFileIsPermanent($node_file_r1, 'Original file is still permanent.'); + + // Save a new version of the node without any changes. + // Check that the file is still the same as the previous revision. + $this->drupalPost('node/' . $nid . '/edit', array('revision' => '1'), t('Save')); + $node = node_load($nid, NULL, TRUE); + $node_file_r3 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $node_vid_r3 = $node->vid; + $this->assertEqual($node_file_r2, $node_file_r3, 'Previous revision file still in place after creating a new revision without a new file.'); + $this->assertFileIsPermanent($node_file_r3, 'New revision file is permanent.'); + + // Revert to the first revision and check that the original file is active. + $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r1 . '/revert', array(), t('Revert')); + $node = node_load($nid, NULL, TRUE); + $node_file_r4 = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $node_vid_r4 = $node->vid; + $this->assertEqual($node_file_r1, $node_file_r4, 'Original revision file still in place after reverting to the original revision.'); + $this->assertFileIsPermanent($node_file_r4, 'Original revision file still permanent after reverting to the original revision.'); + + // Delete the second revision and check that the file is kept (since it is + // still being used by the third revision). + $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r2 . '/delete', array(), t('Delete')); + $this->assertFileExists($node_file_r3, 'Second file is still available after deleting second revision, since it is being used by the third revision.'); + $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting second revision, since it is being used by the third revision.'); + $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting second revision, since it is being used by the third revision.'); + + // Attach the second file to a user. + $user = $this->drupalCreateUser(); + $edit = (array) $user; + $edit[$field_name][LANGUAGE_NONE][0] = (array) $node_file_r3; + user_save($user, $edit); + $this->drupalGet('user/' . $user->uid . '/edit'); + + // Delete the third revision and check that the file is not deleted yet. + $this->drupalPost('node/' . $nid . '/revisions/' . $node_vid_r3 . '/delete', array(), t('Delete')); + $this->assertFileExists($node_file_r3, 'Second file is still available after deleting third revision, since it is being used by the user.'); + $this->assertFileEntryExists($node_file_r3, 'Second file entry is still available after deleting third revision, since it is being used by the user.'); + $this->assertFileIsPermanent($node_file_r3, 'Second file entry is still permanent after deleting third revision, since it is being used by the user.'); + + // Delete the user and check that the file still exists. + user_delete($user->uid); + // TODO: This seems like a bug in File API. Clearing the stat cache should + // not be necessary here. The file really exists, but stream wrappers + // doesn't seem to think so unless we clear the PHP file stat() cache. + clearstatcache(); + // @todo Files referenced from entity revisions cannot currently be deleted after the entity is deleted. + // @see https://drupal.org/node/1613290 + // $this->assertFileNotExists($node_file_r3, 'Second file is now deleted after deleting third revision, since it is no longer being used by any other nodes.'); + // $this->assertFileEntryNotExists($node_file_r3, 'Second file entry is now deleted after deleting third revision, since it is no longer being used by any other nodes.'); + + // Delete the entire node and check that the original file is deleted. + $this->drupalPost('node/' . $nid . '/delete', array(), t('Delete')); + $this->assertFileNotExists($node_file_r1, 'Original file is deleted after deleting the entire node with two revisions remaining.'); + $this->assertFileEntryNotExists($node_file_r1, 'Original file entry is deleted after deleting the entire node with two revisions remaining.'); + } +} + +/** + * Tests various validations. + */ +class MediaFileFieldValidateTestCase extends MediaFileFieldTestCase { + protected $field; + + public static function getInfo() { + return array( + 'name' => 'Media file field validation tests', + 'description' => 'Tests validation functions such as required.', + 'group' => 'Media', + ); + } + + /** + * Tests the required property on file fields. + */ + function testRequired() { + $type_name = 'article'; + $field_name = strtolower($this->randomName()); + $this->createFileField($field_name, $type_name, array(), array('required' => '1')); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, $type_name); + + $test_file = $this->getTestFile('text'); + $test_file->uid = $this->admin_user->uid; + $test_file = file_save($test_file); + + // Try to post a new node without attaching a file. + $langcode = LANGUAGE_NONE; + $edit = array("title" => $this->randomName()); + $this->drupalPost('node/add/' . $type_name, $edit, t('Save')); + $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required file field was empty.'); + + // Create a new node with the attached file. + $nid = $this->attachNodeFile($test_file, $field_name, $type_name); + $this->assertTrue($nid !== FALSE, format_string('attachNodeFile(@test_file, @field_name, @type_name) succeeded', array('@test_file' => $test_file->uri, '@field_name' => $field_name, '@type_name' => $type_name))); + + $node = node_load($nid, NULL, TRUE); + + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $this->assertFileExists($node_file, 'File exists after attaching to the required field.'); + $this->assertFileEntryExists($node_file, 'File entry exists after attaching to the required field.'); + + // Try again with a multiple value field. + field_delete_field($field_name); + $this->createFileField($field_name, $type_name, array('cardinality' => FIELD_CARDINALITY_UNLIMITED), array('required' => '1')); + + // Try to post a new node without attaching a file in the multivalue field. + $edit = array('title' => $this->randomName()); + $this->drupalPost('node/add/' . $type_name, $edit, t('Save')); + $this->assertRaw(t('!title field is required.', array('!title' => $instance['label'])), 'Node save failed when required multiple value file field was empty.'); + + // Create a new node with the attached file into the multivalue field. + $nid = $this->attachNodeFile($test_file, $field_name, $type_name); + $node = node_load($nid, NULL, TRUE); + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $this->assertFileExists($node_file, 'File exists after attaching to the required multiple value field.'); + $this->assertFileEntryExists($node_file, 'File entry exists after attaching to the required multipel value field.'); + + // Remove our file field. + field_delete_field($field_name); + } +} + +/** + * Tests that formatters are working properly. + */ +class MediaFileFieldDisplayTestCase extends MediaFileFieldTestCase { + public static function getInfo() { + return array( + 'name' => 'Media file field display tests', + 'description' => 'Test the display of file fields in node and views.', + 'group' => 'Media', + ); + } + + /** + * Tests normal formatter display on node display. + */ + function testNodeDisplay() { + $field_name = strtolower($this->randomName()); + $type_name = 'article'; + $field_settings = array( + 'display_field' => '1', + 'display_default' => '1', + ); + $instance_settings = array( + 'description_field' => '1', + ); + $widget_settings = array(); + $this->createFileField($field_name, $type_name, $field_settings, $instance_settings, $widget_settings); + $field = field_info_field($field_name); + $instance = field_info_instance('node', $field_name, $type_name); + + // Create a new node *without* the file field set, and check that the field + // is not shown for each node display. + $node = $this->drupalCreateNode(array('type' => $type_name)); + $file_formatters = array('file_default', 'file_table', 'file_url_plain', 'hidden'); + foreach ($file_formatters as $formatter) { + $edit = array( + "fields[$field_name][type]" => $formatter, + ); + $this->drupalPost("admin/structure/types/manage/$type_name/display", $edit, t('Save')); + $this->drupalGet('node/' . $node->nid); + $this->assertNoText($field_name, format_string('Field label is hidden when no file attached for formatter %formatter', array('%formatter' => $formatter))); + } + + $test_file = $this->getTestFile('text'); + $test_file->uid = $this->admin_user->uid; + $test_file = file_save($test_file); + + // Create a new node with the attached file. + $nid = $this->attachNodeFile($test_file, $field_name, $type_name); + $this->drupalGet('node/' . $nid . '/edit'); + + // Check that the media thumbnail is displaying with the file name. + $node = node_load($nid, NULL, TRUE); + $node_file = (object) $node->{$field_name}[LANGUAGE_NONE][0]; + $thumbnail = media_get_thumbnail_preview($node_file); + $default_output = drupal_render($thumbnail); + $this->assertRaw($default_output, 'Default formatter displaying correctly on full node view.'); + + // Turn the "display" option off and check that the file is no longer displayed. + $edit = array($field_name . '[' . LANGUAGE_NONE . '][0][display]' => FALSE); + $this->drupalPost('node/' . $nid . '/edit', $edit, t('Save')); + + $this->assertNoRaw($default_output, 'Field is hidden when "display" option is unchecked.'); + + } +} diff --git a/sites/all/modules/media/tests/media_module_test.info b/sites/all/modules/media/tests/media_module_test.info new file mode 100644 index 000000000..e62679488 --- /dev/null +++ b/sites/all/modules/media/tests/media_module_test.info @@ -0,0 +1,14 @@ +name = Media test +description = Provides hooks for testing Media module functionality. +package = Media +core = 7.x +hidden = TRUE + +files[] = includes/MediaModuleTest.inc + +; Information added by Drupal.org packaging script on 2015-07-14 +version = "7.x-2.0-beta1" +core = "7.x" +project = "media" +datestamp = "1436895542" + diff --git a/sites/all/modules/media/tests/media_module_test.module b/sites/all/modules/media/tests/media_module_test.module new file mode 100644 index 000000000..49c0c4098 --- /dev/null +++ b/sites/all/modules/media/tests/media_module_test.module @@ -0,0 +1,106 @@ +<?php + +/** + * @file + * Provides Media module pages for testing purposes. + */ + +/** + * Implements hook_media_browser_plugin_info(). + */ +function media_module_test_media_browser_plugin_info() { + // Allow tests to enable or disable this hook. + if (!variable_get('media_module_test_media_browser_plugin_info', FALSE)) { + return array(); + } + + $info['media_module_test'] = array( + 'title' => t('Media module test'), + 'class' => 'MediaModuleTest', + 'weight' => 50, + ); + + return $info; +} + +/** + * Implements hook_media_browser_plugin_info_alter(). + */ +function media_module_test_media_browser_plugin_info_alter(&$info) { + // Allow tests to enable or disable this hook. + if (!variable_get('media_module_test_media_browser_plugin_info_alter', FALSE)) { + return; + } + + $info['media_module_test']['title'] = t('Altered plugin title'); +} + +/** + * Implements hook_media_browser_plugins_alter(). + */ +function media_module_test_media_browser_plugins_alter(&$plugin_output) { + // Allow tests to enable or disable this hook. + if (!variable_get('media_module_test_media_browser_plugins_alter', FALSE)) { + return; + } + + $plugin_output['media_module_test']['test']['#markup'] = '<p>' . t('Altered browser plugin output.') . '</p>'; +} + +/** + * Implements hook_menu(). + */ +function media_module_test_menu() { + $items = array(); + + $items['media/test'] = array( + 'title' => 'Media test', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('media_module_test_form'), + 'access arguments' => array('view files'), + ); + + return $items; +} + +/** + * Form constructor for testing a 'media' element. + * + * @see media_module_test_form_submit() + * @ingroup forms + */ +function media_module_test_form($form, &$form_state, $tree = TRUE, $extended = FALSE) { + $form['#tree'] = (bool) $tree; + + $form['nested']['media'] = array( + '#type' => 'media', + '#title' => t('Media'), + '#extended' => (bool) $extended, + '#size' => 13, + ); + + $form['textfield'] = array( + '#type' => 'textfield', + '#title' => t('Type a value and ensure it stays'), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + ); + + return $form; +} + +/** + * Form submission handler for media_module_test_form(). + */ +function media_module_test_form_submit($form, &$form_state) { + if ($form['#tree']) { + $fid = $form['nested']['media']['#extended'] ? $form_state['values']['nested']['media']['fid'] : $form_state['values']['nested']['media']; + } + else { + $fid = $form['nested']['media']['#extended'] ? $form_state['values']['media']['fid'] : $form_state['values']['media']; + } + drupal_set_message(t('The file id is %fid.', array('%fid' => $fid))); +} diff --git a/sites/all/modules/media/views/media_default.view.inc b/sites/all/modules/media/views/media_default.view.inc new file mode 100644 index 000000000..54074650e --- /dev/null +++ b/sites/all/modules/media/views/media_default.view.inc @@ -0,0 +1,171 @@ +<?php + +/** + * @file + * The default view for the media browser library tab. + */ + +$view = new view(); +$view->name = 'media_default'; +$view->description = 'Default view for the media browser library tab.'; +$view->tag = 'media, default'; +$view->base_table = 'file_managed'; +$view->human_name = 'Media browser'; +$view->core = 7; +$view->api_version = '3.0'; +$view->disabled = FALSE; /* Edit this to true to make a default view disabled initially */ + +/* Display: Master */ +$handler = $view->new_display('default', 'Master', 'default'); +$handler->display->display_options['use_ajax'] = TRUE; +$handler->display->display_options['use_more_always'] = FALSE; +$handler->display->display_options['group_by'] = TRUE; +$handler->display->display_options['access']['type'] = 'perm'; +$handler->display->display_options['access']['perm'] = 'view files'; +$handler->display->display_options['cache']['type'] = 'none'; +$handler->display->display_options['query']['type'] = 'views_query'; +$handler->display->display_options['query']['options']['query_tags'] = array( + 0 => 'media_browser', +); +$handler->display->display_options['exposed_form']['type'] = 'basic'; +$handler->display->display_options['exposed_form']['options']['reset_button'] = FALSE; +$handler->display->display_options['pager']['type'] = 'full'; +$handler->display->display_options['pager']['options']['items_per_page'] = '25'; +$handler->display->display_options['pager']['options']['offset'] = '0'; +$handler->display->display_options['pager']['options']['id'] = '0'; +$handler->display->display_options['style_plugin'] = 'media_browser'; +/* No results behavior: Global: Text area */ +$handler->display->display_options['empty']['area']['id'] = 'area'; +$handler->display->display_options['empty']['area']['table'] = 'views'; +$handler->display->display_options['empty']['area']['field'] = 'area'; +$handler->display->display_options['empty']['area']['content'] = 'No files available.'; +$handler->display->display_options['empty']['area']['format'] = filter_fallback_format(); +/* Field: File: Name */ +$handler->display->display_options['fields']['filename']['id'] = 'filename'; +$handler->display->display_options['fields']['filename']['table'] = 'file_managed'; +$handler->display->display_options['fields']['filename']['field'] = 'filename'; +$handler->display->display_options['fields']['filename']['label'] = ''; +$handler->display->display_options['fields']['filename']['alter']['word_boundary'] = FALSE; +$handler->display->display_options['fields']['filename']['alter']['ellipsis'] = FALSE; +$handler->display->display_options['fields']['filename']['link_to_file'] = TRUE; +/* Sort criterion: File: Upload date */ +$handler->display->display_options['sorts']['timestamp']['id'] = 'timestamp'; +$handler->display->display_options['sorts']['timestamp']['table'] = 'file_managed'; +$handler->display->display_options['sorts']['timestamp']['field'] = 'timestamp'; +$handler->display->display_options['sorts']['timestamp']['order'] = 'DESC'; +$handler->display->display_options['sorts']['timestamp']['exposed'] = TRUE; +$handler->display->display_options['sorts']['timestamp']['expose']['label'] = 'Upload date'; +/* Sort criterion: SUM(File Usage: Use count) */ +$handler->display->display_options['sorts']['count']['id'] = 'count'; +$handler->display->display_options['sorts']['count']['table'] = 'file_usage'; +$handler->display->display_options['sorts']['count']['field'] = 'count'; +$handler->display->display_options['sorts']['count']['group_type'] = 'sum'; +$handler->display->display_options['sorts']['count']['exposed'] = TRUE; +$handler->display->display_options['sorts']['count']['expose']['label'] = 'Use count'; +/* Filter criterion: File: Status */ +$handler->display->display_options['filters']['status']['id'] = 'status'; +$handler->display->display_options['filters']['status']['table'] = 'file_managed'; +$handler->display->display_options['filters']['status']['field'] = 'status'; +$handler->display->display_options['filters']['status']['value'] = array( + 1 => '1', +); +/* Filter criterion: File: Name */ +$handler->display->display_options['filters']['filename']['id'] = 'filename'; +$handler->display->display_options['filters']['filename']['table'] = 'file_managed'; +$handler->display->display_options['filters']['filename']['field'] = 'filename'; +$handler->display->display_options['filters']['filename']['operator'] = 'contains'; +$handler->display->display_options['filters']['filename']['exposed'] = TRUE; +$handler->display->display_options['filters']['filename']['expose']['operator_id'] = 'filename_op'; +$handler->display->display_options['filters']['filename']['expose']['label'] = 'File name'; +$handler->display->display_options['filters']['filename']['expose']['operator'] = 'filename_op'; +$handler->display->display_options['filters']['filename']['expose']['identifier'] = 'filename'; +/* Filter criterion: File: Type */ +$handler->display->display_options['filters']['type']['id'] = 'type'; +$handler->display->display_options['filters']['type']['table'] = 'file_managed'; +$handler->display->display_options['filters']['type']['field'] = 'type'; +$handler->display->display_options['filters']['type']['exposed'] = TRUE; +$handler->display->display_options['filters']['type']['expose']['operator_id'] = 'type_op'; +$handler->display->display_options['filters']['type']['expose']['label'] = 'Type'; +$handler->display->display_options['filters']['type']['expose']['operator'] = 'type_op'; +$handler->display->display_options['filters']['type']['expose']['identifier'] = 'type'; + +/* Display: Media browser */ +$handler = $view->new_display('media_browser', 'Media browser', 'media_browser_1'); +$handler->display->display_options['defaults']['title'] = FALSE; +$handler->display->display_options['title'] = 'Library'; +$handler->display->display_options['defaults']['hide_admin_links'] = FALSE; + +/* Display: My files */ +$handler = $view->new_display('media_browser', 'My files', 'media_browser_my_files'); +$handler->display->display_options['defaults']['title'] = FALSE; +$handler->display->display_options['title'] = 'My files'; +$handler->display->display_options['defaults']['hide_admin_links'] = FALSE; +$handler->display->display_options['defaults']['access'] = FALSE; +$handler->display->display_options['access']['type'] = 'perm'; +$handler->display->display_options['access']['perm'] = 'view own files'; +$handler->display->display_options['defaults']['relationships'] = FALSE; +/* Relationship: File: User who uploaded */ +$handler->display->display_options['relationships']['uid']['id'] = 'uid'; +$handler->display->display_options['relationships']['uid']['table'] = 'file_managed'; +$handler->display->display_options['relationships']['uid']['field'] = 'uid'; +$handler->display->display_options['relationships']['uid']['required'] = TRUE; +$handler->display->display_options['defaults']['arguments'] = FALSE; +$handler->display->display_options['defaults']['filter_groups'] = FALSE; +$handler->display->display_options['defaults']['filters'] = FALSE; +/* Filter criterion: File: Status */ +$handler->display->display_options['filters']['status']['id'] = 'status'; +$handler->display->display_options['filters']['status']['table'] = 'file_managed'; +$handler->display->display_options['filters']['status']['field'] = 'status'; +$handler->display->display_options['filters']['status']['value'] = array( + 1 => '1', +); +/* Filter criterion: File: Name */ +$handler->display->display_options['filters']['filename']['id'] = 'filename'; +$handler->display->display_options['filters']['filename']['table'] = 'file_managed'; +$handler->display->display_options['filters']['filename']['field'] = 'filename'; +$handler->display->display_options['filters']['filename']['operator'] = 'contains'; +$handler->display->display_options['filters']['filename']['exposed'] = TRUE; +$handler->display->display_options['filters']['filename']['expose']['operator_id'] = 'filename_op'; +$handler->display->display_options['filters']['filename']['expose']['label'] = 'File name'; +$handler->display->display_options['filters']['filename']['expose']['operator'] = 'filename_op'; +$handler->display->display_options['filters']['filename']['expose']['identifier'] = 'filename'; +/* Filter criterion: File: Type */ +$handler->display->display_options['filters']['type']['id'] = 'type'; +$handler->display->display_options['filters']['type']['table'] = 'file_managed'; +$handler->display->display_options['filters']['type']['field'] = 'type'; +$handler->display->display_options['filters']['type']['exposed'] = TRUE; +$handler->display->display_options['filters']['type']['expose']['operator_id'] = 'type_op'; +$handler->display->display_options['filters']['type']['expose']['label'] = 'Type'; +$handler->display->display_options['filters']['type']['expose']['operator'] = 'type_op'; +$handler->display->display_options['filters']['type']['expose']['identifier'] = 'type'; +/* Filter criterion: User: Current */ +$handler->display->display_options['filters']['uid_current']['id'] = 'uid_current'; +$handler->display->display_options['filters']['uid_current']['table'] = 'users'; +$handler->display->display_options['filters']['uid_current']['field'] = 'uid_current'; +$handler->display->display_options['filters']['uid_current']['relationship'] = 'uid'; +$handler->display->display_options['filters']['uid_current']['value'] = '1'; +$translatables['media_default'] = array( + t('Master'), + t('more'), + t('Apply'), + t('Reset'), + t('Sort by'), + t('Asc'), + t('Desc'), + t('Items per page'), + t('- All -'), + t('Offset'), + t('« first'), + t('‹ previous'), + t('next ›'), + t('last »'), + t('No files available.'), + t('Upload date'), + t('Use count'), + t('File name'), + t('Type'), + t('Media browser'), + t('Library'), + t('My files'), + t('User who uploaded'), +); |