diff --git a/frontend/drupal/sites/all/modules/advagg/LICENSE.txt b/frontend/drupal/sites/all/modules/advagg/LICENSE.txt new file mode 100644 index 000000000..d159169d1 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/frontend/drupal/sites/all/modules/advagg/README.md b/frontend/drupal/sites/all/modules/advagg/README.md new file mode 100644 index 000000000..8740c676c --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/README.md @@ -0,0 +1,806 @@ + +ADVANCED CSS/JS AGGREGATION MODULE +================================== + + +CONTENTS OF THIS FILE +--------------------- + + - Introduction + - Requirements + - Recommended modules + - Installation + - How to get a high PageSpeed score + - JSMin PHP Extension + - Brotli PHP Extension + - Zopfli PHP Extension + - nginx Configuration + - JavaScript Bookmarklet + - Troubleshooting + - Features & benefits + - Configuration + - Additional options for `drupal_add_css/js` functions + - Technical Details & Hooks + + +INTRODUCTION +------------ + +The Advanced CSS/JS Aggregation allows you to improve the frontend performance +of your site. Be sure to do a before and after comparison by using Google's +PageSpeed Insights and WebPagetest.org. +https://developers.google.com/speed/pagespeed/insights/ +http://www.webpagetest.org/easy + + +REQUIREMENTS +------------ + +No special requirements. + + +RECOMMENDED MODULES +------------------- + + - Libraries (https://www.drupal.org/project/libraries) + Allows for 3rd party code for minification to be used by AdvAgg. + + +INSTALLATION +------------ + + - Install as you would normally install a contributed Drupal module. Visit: + https://drupal.org/documentation/install/modules-themes/modules-7 + for further information. + + +HOW TO GET A HIGH PAGESPEED SCORE +--------------------------------- + +Be sure to check the site after every section to make sure the change didn't +mess up your site. The changes under AdvAgg Modifier are usually the most +problematic but they offer the biggest improvements. + +#### Advanced CSS/JS Aggregation #### +Go to `admin/config/development/performance/advagg` + +Select "Use recommended (optimized) settings" + +#### AdvAgg Compress Javascript #### +Install AdvAgg Compress Javascript if not enabled and go to +`admin/config/development/performance/advagg/js-compress` + + - Select JSMin if available; otherwise select JSMin+ + - Select Strip everything (smallest files) + - Save configuration + - Click the batch compress link to process these files at the top. + +#### AdvAgg Async Font Loader #### +Install AdvAgg Async Font Loader if not enabled and go to +`admin/config/development/performance/advagg/font` + + - Select Local file included in aggregate (version: X.X.X). If this option is + not available follow the directions right below the options on how to install + it. + +Keep the 2 checkboxes checked. + +#### AdvAgg Bundler #### +Install AdvAgg Bundler if not enabled and go to +`admin/config/development/performance/advagg/bundler` + +If your server supports HTTP 2 then select "Use HTTP 2.0 settings"; otherwise +leave it at the "Use HTTP 1.1 settings". + +#### AdvAgg Relocate #### +Install AdvAgg Relocate if not enabled and go to +`admin/config/development/performance/advagg/relocate` + +Select "Use recommended (optimized) settings" + +#### AdvAgg Modifier #### +Install AdvAgg Modifier if not enabled and go to +`admin/config/development/performance/advagg/mod` + +Select "Use recommended (optimized) settings" + +#### AdvAgg Critical CSS module #### +Install AdvAgg Critical CSS if not enabled and go to +`admin/config/development/performance/advagg/critical-css` + +These are the directions for the front page of your site. + +Under Add Critical CSS +- Select the theme that is your front page; usually the default is correct. +- User type should be set to 'anonymous' under most circumstances. +- Type of lookup, select URL +- Value to lookup, type in `` +- Critical CSS, paste in the generated CSS from running your homepage url +through https://www.sitelocity.com/critical-path-css-generator which is inside +of the 'Critical Path CSS' textarea on the sitelocity page. +- Click Save Configuration. + +Other landing pages should have their critical CSS added as well. If you have +Google Analytics this will show you how to find your top landing pages +https://developers.google.com/analytics/devguides/reporting/core/v3/common-queries#top-landing-pages +or for Piwik https://piwik.org/faq/how-to/faq_160/. You can also use this +chrome browser plugin to generate critical CSS +https://chrome.google.com/webstore/detail/critical-style-snapshot/gkoeffcejdhhojognlonafnijfkcepob?hl=en + + +JSMIN PHP EXTENSION +------------------- + +The AdvAgg JS Compress module can take advantage of jsmin.c. JavaScript parsing +and minimizing will be done in C instead of PHP dramatically speeding up the +process. If using PHP 5.3.10 or higher https://github.com/sqmk/pecl-jsmin is +recommended. If using PHP 5.3.9 or lower +http://www.ypass.net/software/php_jsmin/ is recommended. + + +BROTLI PHP EXTENSION +-------------------- + +The AdvAgg module can take advantage of Brotli compression. Install this +extension to take advantage of it. Should reduce CSS/JS files by 20%. +https://github.com/kjdev/php-ext-brotli + + +ZOPFLI PHP EXTENSION +-------------------- + +The AdvAgg module can take advantage of the Zopfli compression algorithm. +Install this extension to take advantage of it. This gives higher gzip +compression ratios compared to stock PHP. +https://github.com/kjdev/php-ext-zopfli + + +NGINX CONFIGURATION +------------------- + +https://drupal.org/node/1116618 +Note that @drupal (last line of code below) might be @rewrite or @rewrites +depending on your servers configuration. If there are image style rules in your +Nginx configuration add this right below that. If you want to have brotli +support https://github.com/google/ngx_brotli is how to install that; add +`brotli_static on;` right above `gzip_static on;` in the configuration below. + + ### + ### advagg_css and advagg_js support + ### + location ~* files/advagg_(?:css|js)/ { + gzip_static on; + access_log off; + expires max; + add_header ETag ""; + add_header Cache-Control "max-age=31449600, no-transform, public"; + try_files $uri $uri/ @drupal; + } + +Also noted that some ready made nginx configurations add in a Last-Modified +header inside the advagg directories. These should be removed. + + +JAVASCRIPT BOOKMARKLET +---------------------- + +You can use this JS code as a bookmarklet for toggling the AdvAgg URL parameter. +See http://en.wikipedia.org/wiki/Bookmarklet for more details. + + javascript:(function(){var loc = document.location.href,qs = document.location.search,regex_off = /\&?advagg=-1/,goto = loc;if(qs.match(regex_off)) {goto = loc.replace(regex_off, '');} else {qs = qs ? qs + '&advagg=-1' : '?advagg=-1';goto = document.location.pathname + qs;}window.location = goto;})(); + + +TROUBLESHOOTING +--------------- + +If the core Fast 404 Pages functionality is enabled via settings.php, the +settings must be changed in order for the on-demand file compilation to work. +Change this: + + $conf['404_fast_paths_exclude'] = '/\/(?:styles)\//'; + +to this: + + $conf['404_fast_paths_exclude'] = '/\/(?:styles|advagg_(cs|j)s)\//'; + +Similarly, if the Fast_404 module is enabled, the 'fast_404_string_whitelisting' +variable must be set inside of settings.php. Add this to your settings.php file: + + $conf['fast_404_string_whitelisting'][] = '/advagg_'; + + +Modules like the Central Authentication Services https://drupal.org/project/cas +will redirect all anonymous requests to a login page. Most of the time there is +a setting that allows certain pages to be excluded from the redirect. You should +add the following to those exclusions. Note that sites/default/files is the +location of you public file system (public://) so you might have to adjust this +to fit your setup. services/* is the default (`CAS_EXCLUDE`) and +`httprl_async_function_callback` is needed if httprl will be used. + + services/* + sites/default/files/advagg_css/* + sites/default/files/advagg_js/* + httprl_async_function_callback + +In the example of CAS this setting can be found on the `admin/config/people/cas` +page and under Redirection there should be a setting called "Excluded Pages". + + +If Far-Future headers are not being sent out and you are using Apache here are +some tips to hopefully get it working. For Apache enable `mod_rewrite`, +`mod_headers`, and `mod_expires`. Add the following code to the bottom of +Drupal's core .htaccess file (located at the webroot level). + + + # No mod_headers. Apache module headers is not enabled. + + # No mod_expires. Apache module expires is not enabled. + + # Use ETags. + FileETag MTime Size + + + + # Use Expires Directive if apache module expires is enabled. + + # Do not use ETags. + FileETag None + # Enable expirations. + ExpiresActive On + # Cache all aggregated css/js files for 52 weeks after access (A). + ExpiresDefault A31449600 + + + # Use Headers Directive if apache module headers is enabled. + + # Do not use etags for cache validation. + Header unset ETag + + # Set a far future Cache-Control header to 52 weeks. + Header set Cache-Control "max-age=31449600, no-transform, public" + + + Header append Cache-Control "no-transform, public" + + + + # Force advagg .js file to have the type of application/javascript. + + ForceType application/javascript + + + +If pages on the site stop working correctly or looks broken after Advanced +CSS/JS Aggregation is enabled, the first step should be to validate the +individual CSS and/or JS files using the included `advagg_validator` module - +something as simple as an errant unfinished comment in one file may cause entire +aggregates of files to be ignored. + + +If AdvAgg was installed via drush sometimes directory permissions need to be +fixed. Using `chown -R` on the advagg directories usually solves this issue. + + +If hosting on Pantheon, you might need to add this to your settings.php file if +you get Numerous login prompts after enabling Adv Agg module on Pantheon Test +and Live instances. + + if (isset($_SERVER['PANTHEON_ENVIRONMENT'])) { + // NO trailing slash when setting the $base_url variable. + switch ($_SERVER['PANTHEON_ENVIRONMENT']) { + case 'dev': + $base_url = 'http://dev-sitename.gotpantheon.com'; + break; + + case 'test': + $base_url = 'http://test-sitename.gotpantheon.com'; + break; + + case 'live': + $base_url = 'http://www.domain.tld'; + break; + } + // Remove a trailing slash if one was added. + if (!empty($base_url)) { + $base_url = rtrim($base_url, '/'); + } + } + + +If you're getting the "HTTP requests to advagg are not getting though" error, +you can try to fix it by making sure the `$base_url` is correctly set for +production and not production environments. + + +If you're getting mixed content error for CSS JS files over HTTPS then you can +try to redirect all http traffic to be https. + + RewriteCond %{HTTPS} off + RewriteCond %{HTTP:X-Forwarded-Proto} !https + RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] + + +If brotli compression is not working and you are using Apache here are some tips +to hopefully get it working. For Apache enable `mod_rewrite`, `mod_headers`, and +`mod_expires`. Add the following code right above this line `# Rules to +correctly serve gzip compressed CSS and JS files.` + + + # Serve brotli compressed CSS if they exist and the client accepts br. + RewriteCond %{HTTP:Accept-encoding} br + RewriteCond %{REQUEST_FILENAME}\.br -s + RewriteRule ^(.*)\.css $1\.css\.br [QSA] + RewriteRule \.css\.br$ - [T=text/css,E=no-gzip:1] + + # Serve brotli compressed JS if they exist and the client accepts br. + RewriteCond %{HTTP:Accept-encoding} br + RewriteCond %{REQUEST_FILENAME}\.br -s + RewriteRule ^(.*)\.js $1\.js\.br [QSA] + RewriteRule \.js\.br$ - [T=application/javascript,E=no-gzip:1] + + + # Serve correct encoding type. + Header set Content-Encoding br + # Force proxies to cache compressed and non-compressed css/js files + # separately. + Header append Vary Accept-Encoding + + + + +If you are having 5 minute or longer timeouts on the admin/reports/status page +then you might need to use an alternative to drupal_httP_request(). The cURL +HTTP Request module https://www.drupal.org/project/chr might fix this issue. + + +FEATURES & BENEFITS +------------------- + +**Advanced CSS/JS Aggregation core features** + + - On demand generation of CSS/JS Aggregates. If the file doesn't exist it will + be generated on demand. + - Stampede protection for CSS and JS aggregation. Uses locking so multiple + requests for the same thing will result in only one thread doing the work. + - Fully cached CSS/JS assets allow for zero file I/O if the Aggregated file + already exists. Results in better page generation performance. + - Smarter aggregate deletion. CSS/JS aggregates only get removed from the + folder if they have not been used/accessed in the last 30 days. + - Smarter cache flushing. Scans all CSS/JS files that have been added to any + aggregate; if that file has changed then flush the correct caches so the + changes go out. The new name ensures changes go out when using CDNs. + - One can add JS to any region of the theme & have it aggregated. + - Url query string to turn off aggregation for that request. `?advagg=0` will + turn off file aggregation if the user has the "bypass advanced aggregation" + permission. `?advagg=-1` will completely bypass all of Advanced CSS/JS + Aggregations modules and submodules. `?advagg=1` will enable Advanced CSS/JS + Aggregation if it is currently disabled. + - Button on the admin page for dropping a cookie that will turn off file + aggregation. Useful for theme development. + - Gzip support. All aggregated files can be pre-compressed into a .gz file and + served from Apache. This is faster then gzipping the file on each request. + +**Included submodules** + + - `advagg_bundler`: + Smartly groups files together - given a target number of CSS/JS aggregates, + this will try very hard to meet that goal. + - `advagg_css_cdn`: + Load CSS libraries from a public CDN; currently only supports Google's CDN. + - `advagg_css_compress`: + Compress the compiled CSS files using a 3rd party compressor; currently + supports YUI (included). + - `advagg_js_cdn`: + Load JavaScript libraries from a public CDN; currently only supports Google's + CDN. + - `advagg_js_compress`: + Compress the compiled JavaScript files using a 3rd party compressor; + currently supports JSMin+ (included). + - `advagg_mod`: + Includes additional tweaks that may not work for all sites: + - Force preprocessing for all CSS/JS. + - Move JS to footer. + - Add defer tag to all JS. + - Defer loading of CSS. + - Inline all CSS/JS for given paths. + - Use a shared directory for a unified multisite. + - `advagg_validator`: + Validate all CSS files using jigsaw.w3.org. Check all CSS files with CSSLint. + Check all JS files with JSHint. + + +CONFIGURATION +------------- + +Settings page is located at: +`admin/config/development/performance/advagg` + +**Global Options** + + - Enable Advanced Aggregation: Check this to start using this module. You can + also quickly disable the module here. For testing purposes, this has the same + effect as placing `?advagg=-1` in the URL. Disabled by default. + - Use Cores Grouping Logic: Leave this checkbox enabled until you are ready to + begin exploring the AdvAgg Bundler sub-module which overrides Core's + functionality. This groups files just like Core does so should just work. + Enabled by default. You will also have to disable this checkbox if you wish + to enable some of the CSS Options below on this settings page. + - Use HTTPRL to generate aggregates: If the HTTPRL module is enabled - + https://drupal.org/project/httprl - advagg will use it to generate aggregates + on the fly in a background parallel process. Enabling HTTPRL will improve + page generation speeds when a new aggregate is created because the CSS/JS + file creation will happen in a different process. If HTTPRL is installed it + is Enabled by default; otherwise is it Disabled. + - AdvAgg Cache Settings: As a reference, core takes about 25 ms to run. + Development will scan all files for a change on every page load. Normal is + fine for all use cases. Aggressive should be fine in almost all use cases; + if your inline css/js changes based off of a variable then the aggressive + cache hit ratio will be low; if that is the case consider using + Drupal.settings for a better cache hit ratio. + +**Resource Hints** + +Preemptively get resources (CSS/JS & sub requests). This will set tags in the +document head telling the browser to open up connections before they are needed. + + - DNS Prefetch: Start the DNS lookup for external CSS and JavaScript files as + soon as possible. + - Preconnect: Start the connection to external resources before an HTTP request + is actually sent to the server. On HTTPS this can have a dramatic effect. + - Location of resource hints: This only needs to be changed if the above + settings are not working. + - Preload link http headers: If your server supports HTTP/2 push then this + allows for resources to be sent before the browser knows it needs it. + +**Cron Options** + +Adjusting the frequency of how often something happens on cron. + +**Obscure Options** + + - Create .gz files: Check this by default as it will improve your performance. + For every Aggregated file generated, this will create a gzip version of file + and then only serve it out if the browser accepts gzip files compression. + Enabled by default. + - Create .br files: Check this by default as it will improve your performance. + For every Aggregated file generated, this will create a brotli version of + file and then only serve it out if the browser accepts gzip files + compression. Enabled by default IF the Brotli Extension for PHP is installed. + See https://github.com/kjdev/php-ext-brotli + - Run advagg_ajax_render_alter(): Turn this off if you're having issues with + ajax. Also keep in mind that the max_input_vars setting can cause issues if + you are submitting a lot of data. + - Include the base_url variable in the hooks hash array: Enabled only if you + know you need it. + - Convert absolute paths to be self references: Turn on unless pages are used + inside of an iframe. + - Convert absolute paths to be protocol relative paths: Safe to use unless you + need to support IE6. + - Convert http:// to https://: Usually not needed, but here in case you do. + - Do not run CSS url() values through file_create_url(): Usually not needed, + but here in case you do. + + +**CSS Options & JS Options** + + - Combine CSS files by using media queries: "Use cores grouping logic" needs to + be unchecked in order for this to work. Also noted is that due to an issue + with IE9, compatibility mode is forced off if this is enabled by adding this + tag in the html head: + + + + Disabled by default. + - Prevent more than 4095 CSS selectors in an aggregated CSS file: Internet + Explorer before version 10; IE9, IE8, IE7, & IE6 all have 4095 as the limit + for the maximum number of css selectors that can be in a file. Enabling this + will prevent CSS aggregates from being created that exceed this limit. For + more information see + http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx Disabled + by default. + - Fix improperly set type (CSS/JS): If type is external but does not start with + http, https, or // change it to be type file. If type is file but it starts + with http, https, or // change type to be external. + +**Information page** + +located at `admin/config/development/performance/advagg/info`. This page +provides debugging information. There are no configuration options here. + + - Hook Theme Info: Displays the `process_html` order. Used for debugging. + - Hook Render Info: Displays the scripts and styles render callbakcs. Used for + debugging. + - CSS files: Displays how often a file has changed. + - JS files: Displays how often a file has changed. + - Modules implementing AdvAgg CSS/JS hooks: Lets you know what modules are + using advagg. + - AdvAgg CSS/JS hooks implemented by modules: Lets you know what advagg hooks + are in use. + - Hooks And Variables Used In Hash: Show what is used to calculate the 3rd hash + of an aggregates filename. + - Get detailed info about an aggregate file: Look up detailed array about any + CSS or JS file listed. + +**Operations page** + +located at `admin/config/development/performance/advagg/operations`. This is a +collection of commands to control the cache and to manage testing of this +module. In general this page is useful when troubleshooting some aggregation +issues. For normal operations, you do not need to do anything on this page below +the Smart Cache Flush. There are no configuration options here. + + - Smart Cache Flush + - Flush AdvAgg Cache: Scan all files referenced in aggregated files. If + any of them have changed, increment the counters containing that file and + rebuild the bundle. + + - Aggregation Bypass Cookie + - Toggle The "aggregation bypass cookie" For This Browser: This will set or + remove a cookie that disables aggregation for the remainder of the browser + session. It acts almost the same as adding `?advagg=0` to every URL. + + - Cron Maintenance Tasks + - Clear All Stale Files: Scan all files in the `advagg_css/js` directories + and remove the ones that have not been accessed in the last 30 days. + - Delete Orphaned Aggregates: Scan CSS/JS advagg dir and remove file if + there is no associated db record. + - Clear Missing Files From the Database: Scan for missing files and remove + the associated entries in the database. + - Delete Unused Aggregates from Database: Delete aggregates that have not + been accessed in the last 6 weeks. + - Delete orphaned/expired advagg locks from the semaphore database table. + - Delete leftover temporary files: Delete old temporary files from the + filesystem. + + - Drastic Measures + - Clear All Caches: Remove all data stored in the advagg cache bins. + - Clear All Files: Remove All Generated Files. Remove all files in the + `advagg_(css|js)` directories. + - Force New Aggregates: Increment Global Counter. Force the creation of all + new aggregates by incrementing a global counter. + - Rescan All Files: Force rescan all files and clear the cache. Useful if a + css/js change from a deployment did not work. + - Remove deleted files in the advagg_files table: Remove entries in the + advagg_files table that have a filesize of 0 and delete the + javascript_parsed variable. This gets around the grace period that the + cron cleanup does. + - Reset the AdvAgg Files table: Truncate the advagg_files table and delete + the javascript_parsed variable. This may cause some 404s for CSS/JS assets + for a short amount of time (seconds). Useful if you really want to reset + some stuff. Might be best to put the site into maintenance mode before + doing this. + +**Hidden Settings** + +The following settings are not configurable from the admin UI and must be set in +settings.php. In general they are settings that should not be changed. The +current defaults are shown. + + // Display a message that the bypass cookie is set. + $conf['advagg_show_bypass_cookie_message'] = TRUE; + + // Display a message when a css/js file changed while in development mode. + $conf['advagg_show_file_changed_message'] = TRUE; + + // Skip the 404 check on status page. + $conf['advagg_skip_404_check'] = FALSE; + + // Force the scripts #aggregate_callback to always be _advagg_aggregate_js. + $conf['advagg_enforce_scripts_callback'] = TRUE; + + // Default location of AdvAgg configuration items. + $conf['advagg_admin_config_root_path'] = 'admin/config/development/performance'; + + // Run advagg_url_inbound_alter(). + $conf['advagg_url_inbound_alter'] = TRUE; + + // Allow JavaScript insertion into any scope even if theme does not support + // it. + $conf['advagg_scripts_scope_anywhere'] = FALSE; + + // Empty the scripts key inside of template_process_html replacement + // function. + $conf['advagg_scripts_scope_anywhere'] = FALSE; + + // Do more file operations in main thread if the file system is fast. If + // AdvAgg's directories are mounted on something like S3, you might want to + // set this to FALSE. + $conf['advagg_fast_filesystem'] = TRUE; + + // Pregenerate aggregate files. If disable the browser requesting the file + // will cause the generation to happen. If advagg 404 handling is broken + // then setting this to false will break your site in bad ways. + $conf['advagg_pregenerate_aggregate_files'] = TRUE; + + // Set the jQuery UI version. + $conf['advagg_css_cdn_jquery_ui_version'] = '1.8.7'; + + // See if jQuery UI should be grabbed from the Google CDN. + $conf['advagg_css_cdn_jquery_ui'] = TRUE; + + // Set the jQuery UI version. + $conf['advagg_js_cdn_jquery_ui_version'] = '1.8.7'; + + // Set the jQuery version. + $conf['advagg_js_cdn_jquery_version'] = '1.4.4'; + + // Use minification. + $conf['advagg_js_cdn_compression'] = TRUE; + + // See if jQuery UI should be grabbed from the Google CDN. + $conf['advagg_js_cdn_jquery_ui'] = TRUE; + + // See if jQuery should be grabbed from the Google CDN. + $conf['advagg_js_cdn_jquery'] = TRUE; + + // Value for the compression ratio test. + $conf['advagg_js_compress_max_ratio'] = 0.9; + + // Value for the compression ratio test. + $conf['advagg_js_compress_ratio'] = 0.1; + + // Skip far future check on status page. + $conf['advagg_skip_far_future_check'] = FALSE; + + // Skip preprocess and enabled checks. + $conf['advagg_skip_enabled_preprocess_check'] = FALSE; + + // Default root dir for the advagg files; see advagg_get_root_files_dir(). + $conf['advagg_root_dir_prefix'] = 'public://'; + + // How long to wait when writing the aggregate if a file is missing or the + // hash doesn't match. + $conf['advagg_file_read_failure_timeout'] = 3600; + + // If FALSE mtime of files will only trigger a change if they are in the + // future. + $conf['advagg_strict_mtime_check'] = TRUE; + + // Skip 304 check on status page. + $conf['advagg_skip_304_check'] = FALSE; + + // Control how many bytes can be inlined. + $conf['advagg_mod_css_defer_inline_size_limit'] = 12288; + + // Control how many bytes the preload header can use. + $conf['advagg_resource_hints_preload_max_size'] = 3072; + + // If TRUE, only verify 1st hash instead of all 3 of the filename. + $conf['advagg_weak_file_verification'] = FALSE; + + +ADDITIONAL OPTIONS FOR DRUPAL_ADD_CSS/JS FUNCTIONS +-------------------------------------------------- + +AdvAgg extends the available options inside of `drupal_add_css` and +`drupal_add_js`. + +`drupal_add_js` - additional keys for $options. + + - `browsers`: Works the same as the one found in drupal_add_css. + - `onload`: Run this js code when after the js file has loaded. + - `onerror`: Run this js code when if the js file did not load. + - `async`: TRUE - Load this file using async. + - `no_defer`: TRUE - Never defer or async load this js file. + +Both `drupal_add_js` + `drupal_add_css` - additional keys for $options. + + - `scope_lock`: TRUE - Make sure the scope of this will not ever change. + - `movable`: FALSE - Make sure the ordering of this will not ever change. + - `preprocess_lock`: TRUE - Make sure the preprocess key will not ever change. + + +TECHNICAL DETAILS & HOOKS +------------------------- + +**Technical Details** + + - There are five database tables and two cache table used by advagg. + `advagg_schema` documents what they are used for. + - Files are generated by this pattern: + + css__[BASE64_HASH]__[BASE64_HASH]__[BASE64_HASH].css + + The first base64 hash value tells us what files are included in the + aggregate. Changing what files get included will change this value. + + The second base64 hash value is used as a sort of version control; it changes + if any of the base files contents have changed. Changing a base files content + (like drupal.js) will change this value. + + The third base64 hash value records what settings were used when generating + the aggregate. Changing a setting that affects how aggregates get built + (like toggling "Create .gz files") will change this value. + + - To trigger scanning of the CSS / JS file cache to identify new files, run + the following: + + // Trigger reloading the CSS and JS file cache in AdvAgg. + if (module_exists('advagg')) { + module_load_include('inc', 'advagg', 'advagg.cache'); + advagg_push_new_changes(); + } + + - Aggressive Cache Setting: This will fully cache the rendered html generated + by AdvAgg. The cache ID is set by this code: + + // CSS. + $hooks_hash = advagg_get_current_hooks_hash(); + $css_cache_id_full = + 'advagg:css:full:' . $hooks_hash . ':' . + drupal_hash_base64(serialize($full_css)); + // JS. + $hooks_hash = advagg_get_current_hooks_hash(); + $js_cache_id_full = + 'advagg:js:full:' . $hooks_hash . ':' . + drupal_hash_base64(serialize($js_scope_array)); + + The second and final hash value in this cache id is the css/js_hash value. + This takes the input from `drupal_add_css/js()` and creates a hash value from + it. If a different file is added and/or inline code changed, this hash value + will be different. + + The first hash value will take the current_hooks_hash value which is the + third base64 hash value listed above in this section (Technical Details) as + the first part of the hash. This means that if any value is changed in this + nested array a different cache id will be used. You can see the contents of + this nested array by going to + `admin/config/development/performance/advagg/info` under + "Hooks And Variables Used In Hash". An example of this being properly used is + if you enable the core locale module the language key will appear in the + array. This is needed because the `locale_css_alter` and `locale_js_alter` + functions both use the global $language variable in determining what css or + js files need to be altered. To add in your own context you can use + `hook_advagg_current_hooks_hash_array_alter` to do so. Be careful when doing + so as including something like the user id will make every user have a + different set of aggregate files. + +**Hooks** + +Modify file contents: + + - `advagg_get_css_file_contents_alter`. Modify the data of each file before it + gets glued together into the bigger aggregate. Useful for minification. + - `advagg_get_js_file_contents_alter`. Modify the data of each file before it + gets glued together into the bigger aggregate. Useful for minification. + - `advagg_get_css_aggregate_contents_alter`. Modify the data of the complete + aggregate before it gets written to a file. Useful for minification. + - `advagg_get_js_aggregate_contents_alter`. Modify the data of the complete + aggregate before it gets written to a file.Useful for minification. + - `advagg_save_aggregate_alter`. Modify the data of the complete aggregate + allowing one create multiple files from one base file. Useful for gzip + compression. Also useful for mirroring data. + +Modify file names and aggregate bundles: + + - `advagg_current_hooks_hash_array_alter`. Add in your own settings and hooks + allowing one to modify the 3rd base64 hash in a filename. + - `advagg_build_aggregate_plans_alter`. Regroup files into different + aggregates. + - `advagg_css_groups_alter`. Allow other modules to modify `$css_groups` right + before it is processed. + - `advagg_js_groups_alter`. Allow other modules to modify `$js_groups` right + before it is processed. + +Others: + + - `advagg_hooks_implemented_alter`. Tell advagg about other hooks related to + advagg. + - `advagg_get_root_files_dir_alter`. Allow other modules to alter css and js + paths. + - `advagg_modify_css_pre_render_alter`. Allow other modules to modify $children + & $elements before they are rendered. + - `advagg_modify_js_pre_render_alter`. Allow other modules to modify $children + & $elements before they are rendered. + - `advagg_changed_files`. Let other modules know about the changed files. + - `advagg_removed_aggregates`. Let other modules know about removed aggregates. + - `advagg_scan_for_changes`. Let other modules see if files related to this + file has changed. Useful for detecting changes to referenced images in css. + - `advagg_get_info_on_files_alter`. Let other modules modify information about + the base CSS/JS files. + - `advagg_context_alter`. Allow other modules to swap important contextual + information on generation. + - `advagg_bundler_analysis`. If the bundler module is installed allow for other + modules to change the bundler analysis. diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg.admin.inc new file mode 100644 index 000000000..80601001e --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.admin.inc @@ -0,0 +1,1965 @@ + t('Use default (safe) settings'), + 2 => t('Use recommended (optimized) settings'), + 4 => t('Use customized settings'), + ); + $form['advagg_admin_mode'] = array( + '#type' => 'radios', + '#title' => t('AdvAgg Settings'), + '#default_value' => variable_get('advagg_admin_mode', ADVAGG_ADMIN_MODE), + '#options' => $options, + '#description' => t("Default settings will mirror core as closely as possible.
Recommended settings are optimized for speed."), + ); + + $form['global_container'] = array( + '#type' => 'container', + '#hidden' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="advagg_admin_mode"]' => array('value' => '4'), + ), + ), + ); + + // Simple checkbox settings. + $form['global_container']['global'] = array( + '#type' => 'fieldset', + '#title' => t('Global Options'), + ); + $form['global_container']['global']['advagg_enabled'] = array( + '#type' => 'checkbox', + '#title' => t('Enable advanced aggregation (recommended)'), + '#default_value' => variable_get('advagg_enabled', ADVAGG_ENABLED), + '#description' => t('Uncheck this box to completely disable AdvAgg functionality.'), + '#recommended_value' => TRUE, + ); + $advagg_core_groups_default = variable_get('advagg_core_groups', ADVAGG_CORE_GROUPS); + if (variable_get('advagg_combine_css_media', ADVAGG_COMBINE_CSS_MEDIA) + || variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER) + || (module_exists('advagg_bundler') && variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE))) { + $advagg_core_groups_default = FALSE; + } + $form['global_container']['global']['advagg_core_groups'] = array( + '#type' => 'checkbox', + '#title' => t('Use cores grouping logic'), + '#default_value' => $advagg_core_groups_default, + '#description' => t('Will group files just like core does.'), + '#recommended_value' => FALSE, + '#states' => array( + 'enabled' => array( + '#edit-advagg-combine-css-media' => array('checked' => FALSE), + '#edit-advagg-ie-css-selector-limiter' => array('checked' => FALSE), + ), + ), + ); + $form['global_container']['global']['advagg_use_httprl'] = array( + '#type' => 'checkbox', + '#title' => t('Use HTTPRL to generate aggregates (recommended)'), + '#default_value' => module_exists('httprl') ? variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL) : FALSE, + '#disabled' => module_exists('httprl') ? FALSE : TRUE, + '#description' => t('If HTTPRL is installed, advagg will use it to generate aggregates on the fly in a background parallel process.', array('@link' => 'http://drupal.org/project/httprl')), + '#recommended_value' => TRUE, + ); + + $aggressive_cache_conflicts = advagg_aggressive_cache_conflicts(); + if (empty($aggressive_cache_conflicts)) { + $description = t('It appears that there are no incompatible modules, so you should be able to safely use the Aggressive cache.'); + } + else { + $description = t('It appears that there might be some incompatible modules. I would test these out before setting the cache to aggressive: %modules', array('%modules' => implode(', ', $aggressive_cache_conflicts))); + } + $options = array( + -1 => t('Development ~ 300ms'), + 1 => t('Normal ~ 60ms'), + 3 => t('Render Cache ~ 30ms (recommended)'), + 5 => t('Aggressive Render Cache ~ 10ms'), + ); + $form['global_container']['global']['advagg_cache_level'] = array( + '#type' => 'radios', + '#title' => t('AdvAgg Cache Settings'), + '#default_value' => variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL), + '#options' => $options, + '#recommended_value' => 3, + '#description' => t("As a reference, core takes about 25 ms to run. Development will scan all files for a change on every page load. Normal and the render cache is fine for all use cases. Aggressive should be fine for most use cases. If your inline css/js changes based off of a variable then the cache hit ratio will be low; if that is the case consider using Drupal.settings for a better cache hit ratio when using the render cache. The aggressive render cache will cache the output from js_alter and css_alter. !description", array( + '@information' => url($config_path . '/advagg/info', array( + 'fragment' => 'edit-hooks-implemented', + )), + '!description' => $description, + )), + ); + $stream_wrappers = file_get_stream_wrappers(); + $prefix = variable_get('advagg_root_dir_prefix', ADVAGG_ROOT_DIR_PREFIX); + if ($prefix !== 'public://' + || $stream_wrappers['public']['class'] !== 'DrupalPublicStreamWrapper' + || count($stream_wrappers) > 2 + || file_default_scheme() !== 'public' + ) { + $stream_wrappers_string = array(); + foreach ($stream_wrappers as $key => $values) { + $stream_wrappers_string[] = t("@key:// uses @class; @description", array( + '@key' => $key, + '@class' => $values['class'], + '@description' => $values['description'], + )); + } + $stream_wrappers_string = implode("
\n", $stream_wrappers_string); + $form['global_container']['global']['advagg_root_dir_prefix'] = array( + '#type' => 'textfield', + '#title' => t('Stream wrapper for AdvAgg.'), + '#default_value' => $prefix, + '#description' => t("Options:
\n!stream_wrappers", array( + '!stream_wrappers' => $stream_wrappers_string, + )), + ); + } + + $form['global_container']['global']['dev_container'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="advagg_cache_level"]' => array('value' => '-1'), + ), + ), + ); + // Show msg about advagg css compress. + if (module_exists('advagg_css_compress') && (variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR) > 0 || variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE))) { + $form['global_container']['global']['dev_container']['advagg_css_compress_msg'] = array( + '#markup' => '

' . t('The AdvAgg CSS Compression module is disabled when in development mode.', array('@css' => url($config_path . '/advagg/css-compress'))) . '

', + ); + } + // Show msg about advagg js compress. + if (module_exists('advagg_js_compress') && (variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR) || variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE))) { + $form['global_container']['global']['dev_container']['advagg_js_compress_msg'] = array( + '#markup' => '

' . t('The AdvAgg JS Compression module is disabled when in development mode.', array('@js' => url($config_path . '/advagg/js-compress'))) . '

', + ); + } + + // Show msg about the jquery update compression setting. + if (module_exists('jquery_update')) { + if (variable_get('jquery_update_compression_type', 'min') === 'min') { + $form['global_container']['global']['dev_container']['advagg_jquery_update_development'] = array( + '#markup' => '

' . t('You might want to change the jQuery update compression level to "Development" as well.', array( + '!url' => url('admin/config/development/jquery_update'), + )) . '

', + ); + } + else { + $form['global_container']['global']['prod_container'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="advagg_cache_level"]' => array('!value' => '-1'), + ), + ), + ); + $form['global_container']['global']['prod_container']['advagg_jquery_update_development'] = array( + '#markup' => '

' . t('You might want to change the jQuery update compression level to "Production" as well.', array( + '!url' => url('admin/config/development/jquery_update'), + )) . '

', + ); + } + } + + $advagg_resource_hints_dns_prefetch = variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH); + $advagg_resource_hints_preconnect = variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT); + $advagg_resource_hints_preload = variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD); + $form['global_container']['global']['resource_hints'] = array( + '#type' => 'fieldset', + '#title' => t('Resource Hints'), + '#collapsible' => TRUE, + '#collapsed' => ($advagg_resource_hints_dns_prefetch || $advagg_resource_hints_preconnect || $advagg_resource_hints_preload) ? FALSE : TRUE, + '#description' => t('Preemptively get resources (CSS/JS & sub requests).'), + ); + $form['global_container']['global']['resource_hints']['advagg_resource_hints_dns_prefetch'] = array( + '#type' => 'checkbox', + '#title' => t('DNS Prefetch (recommended).'), + '#default_value' => $advagg_resource_hints_dns_prefetch, + '#disabled' => '', + '#description' => t('Start the DNS lookup for external CSS and JavaScript files as soon as possible.'), + '#recommended_value' => TRUE, + ); + $form['global_container']['global']['resource_hints']['advagg_resource_hints_preconnect'] = array( + '#type' => 'checkbox', + '#title' => t('Preconnect (recommended).'), + '#default_value' => $advagg_resource_hints_preconnect, + '#disabled' => '', + '#description' => t('Start the connection to external resources before an HTTP request is actually sent to the server.'), + '#recommended_value' => TRUE, + ); + $form['global_container']['global']['resource_hints']['advagg_resource_hints_location'] = array( + '#type' => 'radios', + '#title' => t('Location of resource hints.'), + '#default_value' => variable_get('advagg_resource_hints_location', ADVAGG_RESOURCE_HINTS_LOCATION), + '#description' => t('If you have css and/or js files being loaded right after the "charset=utf-8" meta tag, you need to use the "above charset=utf-8" option in order for some of the resource hints to work; Optimizely is an example that would need this other option. Link headers are not supported in every browser.'), + '#recommended_value' => 1, + '#options' => array( + 1 => t('Below charset=utf-8 (recommended)'), + 3 => t('Above charset=utf-8'), + ), + '#states' => array( + 'disabled' => array( + '#edit-advagg-resource-hints-dns-prefetch' => array('checked' => FALSE), + '#edit-advagg-resource-hints-preconnect' => array('checked' => FALSE), + ), + ), + ); + + // Preload Section. + $form['global_container']['global']['resource_hints']['preload']['advagg_resource_hints_preload'] = array( + '#type' => 'checkbox', + '#title' => t('Preload link http headers.'), + '#default_value' => $advagg_resource_hints_preload, + '#disabled' => '', + '#description' => t('Use link http headers to send out preload hints for local and external resources.'), + '#recommended_value' => FALSE, + ); + $advagg_resource_hints_preload_settings = advagg_get_resource_hints_preload_settings(); + $form['global_container']['global']['resource_hints']['preload']['advagg_resource_hints_preload_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Preload Ordering and Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Change the order of what preload items get sent first. Can also control what will be sent and if HTTP/2 push should be used (if your server supports it). If your webserver supports HTTP/2 Push, and it is enabled, the server will send every asset flagged to be pushed to the browser on every request, ignoring the browser cache (not a good thing for repeat visits). More complex server configuration will be needed to make this efficient; no solutions are currently available.'), + '#states' => array( + 'invisible' => array( + '#edit-advagg-resource-hints-preload' => array('checked' => FALSE), + ), + ), + ); + // Build the rows. + foreach ($advagg_resource_hints_preload_settings as $id => $entry) { + // Build the table rows. + $rows[$id] = array( + 'data' => array( + // Cell for the cross drag and drop element. + array('class' => array('entry-cross')), + // Weight item for the tabledrag. + array( + 'data' => array( + '#type' => 'weight', + '#title' => t('Weight'), + '#title_display' => 'invisible', + '#default_value' => $entry['#weight'], + '#parents' => array( + 'advagg_resource_hints_preload_settings', + $id, + 'weight', + ), + '#attributes' => array( + 'class' => array('entry-order-weight'), + ), + ), + ), + check_plain($entry['title']), + array( + 'data' => array( + '#type' => 'checkbox', + '#title' => t('Enable'), + '#title_display' => 'invisible', + '#default_value' => $entry['enabled'], + '#parents' => array( + 'advagg_resource_hints_preload_settings', + $id, + 'enabled', + ), + ), + ), + array( + 'data' => array( + '#type' => 'checkbox', + '#title' => t('HTTP/2 Push'), + '#title_display' => 'invisible', + '#default_value' => $entry['push'], + '#parents' => array( + 'advagg_resource_hints_preload_settings', + $id, + 'push', + ), + ), + ), + array( + 'data' => array( + '#type' => 'checkbox', + '#title' => t('Local'), + '#title_display' => 'invisible', + '#default_value' => $entry['local'], + '#parents' => array( + 'advagg_resource_hints_preload_settings', + $id, + 'local', + ), + ), + ), + array( + 'data' => array( + '#type' => 'checkbox', + '#title' => t('External'), + '#title_display' => 'invisible', + '#default_value' => $entry['external'], + '#parents' => array( + 'advagg_resource_hints_preload_settings', + $id, + 'external', + ), + ), + ), + ), + 'class' => array('draggable'), + ); + // Build rows of the form elements in the table. + $row_elements[$id] = array( + 'weight' => &$rows[$id]['data'][1]['data'], + 'enabled' => &$rows[$id]['data'][3]['data'], + 'push' => &$rows[$id]['data'][4]['data'], + 'local' => &$rows[$id]['data'][5]['data'], + 'external' => &$rows[$id]['data'][6]['data'], + ); + } + + // Add the table to the form. + $form['global_container']['global']['resource_hints']['preload']['advagg_resource_hints_preload_settings']['advagg_table'] = array( + '#theme' => 'table', + // The row form elements need to be processed and build, + // therefore pass them as element children. + 'elements' => $row_elements, + '#header' => array( + // We need two empty columns for the weight field and the cross. + array('data' => NULL, 'colspan' => 2), + t('Name'), + t('Enabled'), + t('HTTP/2 Push'), + t('Local'), + t('External'), + ), + '#rows' => $rows, + '#attributes' => array('id' => 'entry-order'), + ); + drupal_add_tabledrag('entry-order', 'order', 'sibling', 'entry-order-weight'); + $form['global_container']['global']['resource_hints']['preload']['advagg_resource_hints_preload_settings']['advagg_resource_hints_preload_reset'] = array( + '#type' => 'submit', + '#value' => t('Reset'), + '#submit' => array('advagg_admin_resource_hints_preload_reset'), + ); + $form['global_container']['global']['resource_hints']['advagg_resource_hints_use_immutable'] = array( + '#type' => 'checkbox', + '#title' => t('Send immutable header for all aggregated files (recommended).'), + '#default_value' => variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE), + '#description' => t('Have Apache send Cache-Control: immutable for all aggregated files. Current browser support', array( + '@url1' => 'http://bitsup.blogspot.de/2016/05/cache-control-immutable.html', + '@url2' => 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Browser_compatibility', + )), + '#recommended_value' => TRUE, + ); + + $form['global_container']['global']['cron'] = array( + '#type' => 'fieldset', + '#title' => t('Cron Options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Unless you have a good reason to adjust these values you should leave them alone.'), + ); + // @codingStandardsIgnoreStart + $short_times = drupal_map_assoc(array( + 60 * 15, // 15 min. + 60 * 30, // 30 min. + 60 * 45, // 45 min. + 60 * 60, // 1 hour. + 60 * 60 * 2, // 2 hours. + 60 * 60 * 4, // 4 hours. + 60 * 60 * 6, // 6 hours. + 60 * 60 * 8, // 8 hours. + 60 * 60 * 10, // 10 hours. + 60 * 60 * 12, // 12 hours. + 60 * 60 * 18, // 18 hours. + 60 * 60 * 23, // 23 hours. + 60 * 60 * 24, // 1 day. + 60 * 60 * 24 * 2, // 2 days. + ), 'format_interval'); + $long_times = drupal_map_assoc(array( + 60 * 60 * 24 * 2, // 2 days. + 60 * 60 * 24 * 3, // 3 days. + 60 * 60 * 24 * 4, // 4 days. + 60 * 60 * 24 * 5, // 5 days. + 60 * 60 * 24 * 6, // 6 days. + 60 * 60 * 24 * 7, // 1 week. + 60 * 60 * 24 * 7 * 2, // 2 weeks. + 60 * 60 * 24 * 7 * 3, // 3 weeks. + 60 * 60 * 24 * 30, // 1 month. + 60 * 60 * 24 * 45, // 1 month 2 weeks. + 60 * 60 * 24 * 60, // 2 months. + ), 'format_interval'); + // @codingStandardsIgnoreEnd + $last_ran = variable_get('advagg_cron_timestamp', 0); + if (!empty($last_ran)) { + $last_ran = t('@time ago', array('@time' => format_interval(REQUEST_TIME - $last_ran))); + } + else { + $last_ran = t('never'); + } + + $form['global_container']['global']['cron']['advagg_cron_frequency'] = array( + '#type' => 'select', + '#options' => $short_times, + '#title' => 'Minimum amount of time between advagg_cron() runs.', + '#default_value' => variable_get('advagg_cron_frequency', ADVAGG_CRON_FREQUENCY), + '#description' => t('The default value for this is %value. The last time advagg_cron was ran is %time.', array( + '%value' => format_interval(ADVAGG_CRON_FREQUENCY), + '%time' => $last_ran, + )), + ); + $form['global_container']['global']['cron']['drupal_stale_file_threshold'] = array( + '#type' => 'select', + '#options' => $long_times, + '#title' => 'Delete aggregates accessed/modified more than a set time ago.', + // @codingStandardsIgnoreLine + '#default_value' => variable_get('drupal_stale_file_threshold', 2592000), + '#description' => t('The default value for this is %value.', array('%value' => format_interval(2592000))), + ); + $form['global_container']['global']['cron']['advagg_remove_missing_files_from_db_time'] = array( + '#type' => 'select', + '#options' => $long_times, + '#title' => 'How long to wait until unaccessed aggregates with missing files are removed from the database.', + '#default_value' => variable_get('advagg_remove_missing_files_from_db_time', ADVAGG_REMOVE_MISSING_FILES_FROM_DB_TIME), + '#description' => t('The default value for this is %value.', array('%value' => format_interval(ADVAGG_REMOVE_MISSING_FILES_FROM_DB_TIME))), + ); + $form['global_container']['global']['cron']['advagg_remove_old_unused_aggregates_time'] = array( + '#type' => 'select', + '#options' => $long_times, + '#title' => 'How long to wait until unaccessed aggregates are removed from the database.', + '#default_value' => variable_get('advagg_remove_old_unused_aggregates_time', ADVAGG_REMOVE_OLD_UNUSED_AGGREGATES_TIME), + '#description' => t('The default value for this is %value.', array('%value' => format_interval(ADVAGG_REMOVE_OLD_UNUSED_AGGREGATES_TIME))), + ); + + $form['global_container']['global']['obscure'] = array( + '#type' => 'fieldset', + '#title' => t('Obscure Options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#description' => t('Some of the more obscure AdvAgg settings. Odds are you do not need to change anything in here.'), + ); + $form['global_container']['global']['obscure']['advagg_gzip'] = array( + '#type' => 'checkbox', + '#title' => t('Create .gz files'), + '#default_value' => variable_get('advagg_gzip', ADVAGG_GZIP), + '#description' => t('All aggregated files can be pre-compressed into a .gz file and + served from Apache. This is faster then gzipping the file on each request.'), + ); + $form['global_container']['global']['obscure']['advagg_brotli'] = array( + '#type' => 'checkbox', + '#title' => t('Create .br files'), + '#default_value' => variable_get('advagg_brotli', ADVAGG_BROTLI), + '#description' => t('All aggregated files can be pre-compressed into a .br file and + served from Apache. This is faster then brotli compressing the file on each request.'), + ); + if (!function_exists('brotli_compress') || !defined('BROTLI_TEXT')) { + $form['global_container']['global']['obscure']['advagg_brotli']['#default_value'] = FALSE; + $form['global_container']['global']['obscure']['advagg_brotli']['#disabled'] = TRUE; + $form['global_container']['global']['obscure']['advagg_brotli']['#description'] .= ' ' . t('The PHP brotli extension is needed in order to enable this feature.', array('@url' => 'https://github.com/kjdev/php-ext-brotli')); + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $form['global_container']['global']['obscure']['advagg_brotli']['#description'] .= ' ' . t('If running on windows this is a helpful guide on how to compile PHP extensions. Also noted that IIS has a Brotli extension,', array( + '@url' => 'https://wiki.php.net/internals/windows/stepbystepbuild', + '@iis' => 'https://www.iis.net/downloads/community/2016/03/iis-brotli', + )); + } + } + $form['global_container']['global']['obscure']['advagg_ajax_render_alter'] = array( + '#type' => 'checkbox', + '#title' => t('Run advagg_ajax_render_alter()'), + '#default_value' => variable_get('advagg_ajax_render_alter', ADVAGG_AJAX_RENDER_ALTER), + '#description' => t('If disabled, AdvAgg will not alter the aggregates returned by ajax requests.'), + ); + $form['global_container']['global']['obscure']['advagg_include_base_url'] = array( + '#type' => 'checkbox', + '#title' => t('Include the base_url variable in the hooks hash array.'), + '#default_value' => variable_get('advagg_include_base_url', ADVAGG_INCLUDE_BASE_URL), + '#description' => t('If you would like a unique set of aggregates for every permutation of the base_url (current value: %value) then enable this setting. Read more.', array( + '%value' => $GLOBALS['base_url'], + '@issue' => 'https://www.drupal.org/node/2353811', + )), + ); + $form['global_container']['global']['obscure']['advagg_convert_absolute_to_relative_path'] = array( + '#type' => 'checkbox', + '#title' => t('Convert absolute paths to be self references.'), + '#default_value' => variable_get('advagg_convert_absolute_to_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_RELATIVE_PATH), + '#description' => t('If the src to a CSS/JS file points to this domain, convert the absolute path to a relative path. Will also convert url() references inside of css files. This is generally safe unless the drupal page will be embedded, like it is when used as an iframe.'), + ); + $form['global_container']['global']['obscure']['advagg_convert_absolute_to_protocol_relative_path'] = array( + '#type' => 'checkbox', + '#title' => t('Convert absolute paths to be protocol relative paths.'), + '#default_value' => variable_get('advagg_convert_absolute_to_protocol_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_PROTOCOL_RELATIVE_PATH), + '#description' => t('If the src to a CSS/JS file points starts with http:// or https://, convert it to use a protocol relative path //. Will also convert url() references inside of css files. This is incompatible with IE6 and might cause extra bandwidth usage in IE7 and IE8 in regards to url() statements inside css files. According to current web browser market share IE 6 is less than 0.2% of the total number of browsers in use and IE 7+8 make up under 5% of browsers in use.', array( + '@url' => 'http://www.w3counter.com/trends', + )), + '#states' => array( + 'enabled' => array( + '#edit-advagg-force-https-path' => array('checked' => FALSE), + ), + ), + ); + $form['global_container']['global']['obscure']['advagg_force_https_path'] = array( + '#type' => 'checkbox', + '#title' => t('Convert http:// to https://.'), + '#default_value' => variable_get('advagg_force_https_path', ADVAGG_FORCE_HTTPS_PATH), + '#description' => t('If the src to a CSS/JS file starts with http:// convert it https://. Will also convert url() references inside of css files.'), + '#states' => array( + 'enabled' => array( + '#edit-advagg-convert-absolute-to-protocol-relative-path' => array('checked' => FALSE), + ), + ), + ); + $form['global_container']['global']['obscure']['advagg_css_absolute_path'] = array( + '#type' => 'checkbox', + '#title' => t('Convert CSS url() to absolute paths if file is outside of the public:// dir.'), + '#default_value' => variable_get('advagg_css_absolute_path', ADVAGG_CSS_ABSOLUTE_PATH), + '#description' => t('Useful for CSS files hosted on S3 where the referenced files inside of url() is not in the same location as the aggregated CSS file.'), + ); + $form['global_container']['global']['obscure']['advagg_skip_file_create_url_inside_css'] = array( + '#type' => 'checkbox', + '#title' => t('Do not run CSS url() values through file_create_url().'), + '#default_value' => variable_get('advagg_skip_file_create_url_inside_css', ADVAGG_SKIP_FILE_CREATE_URL_INSIDE_CSS), + '#description' => t('If checked, the processing of url() values within CSS will be handled more like core than the CDN module. This will not convert relative paths in your source CSS to absolute paths in the aggregated CSS.'), + ); + $form['global_container']['global']['obscure']['advagg_htaccess_symlinksifownermatch'] = array( + '#type' => 'checkbox', + '#title' => t('Use "Options +SymLinksIfOwnerMatch"'), + '#default_value' => variable_get('advagg_htaccess_symlinksifownermatch', ADVAGG_HTACCESS_SYMLINKSIFOWNERMATCH), + '#description' => t('By default the custom .htaccess files are configured to use "Options +FollowSymLinks". Some hosting companies do not support this so "Options +SymLinksIfOwnerMatch" must be used instead.'), + ); + $form['global_container']['global']['obscure']['advagg_disable_on_admin'] = array( + '#type' => 'checkbox', + '#title' => t('Do not use AdvAgg or any aggregation in the admin section.'), + '#default_value' => variable_get('advagg_disable_on_admin', ADVAGG_DISABLE_ON_ADMIN), + '#description' => t('If checked, AdvAgg will not be used in the /admin section as well as any place the admin theme is used.'), + ); + $form['global_container']['global']['obscure']['advagg_disable_on_listed_pages'] = array( + '#type' => 'textarea', + '#title' => t('Do not use AdvAgg or any aggregation in the following pages'), + '#default_value' => variable_get('advagg_disable_on_listed_pages', ADVAGG_DISABLE_ON_LISTED_PAGES), + '#description' => t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog.", array( + '%blog' => 'blog', + '%blog-wildcard' => 'blog/*', + )), + ); + // Get core htaccess files. + $core_htaccess = advagg_htaccess_rewritebase(); + $advagg_htaccess_rewritebase = variable_get('advagg_htaccess_rewritebase', ADVAGG_HTACCESS_REWRITEBASE); + if (!empty($core_htaccess)) { + // Get advagg htaccess files. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $advagg_css_htaccess = advagg_htaccess_rewritebase($css_path[1]); + $advagg_js_htaccess = advagg_htaccess_rewritebase($js_path[1]); + // Build best guess dir name. + $core_htaccess_directive = trim(str_ireplace('RewriteBase ', '', $core_htaccess)); + $new_dir = str_replace('//', '/', $core_htaccess_directive . '/' . dirname($css_path[1])); + + $form['global_container']['global']['obscure']['advagg_htaccess_rewritebase'] = array( + '#type' => 'textfield', + '#title' => t('AdvAgg RewriteBase Directive in .htaccess files.'), + '#default_value' => $advagg_htaccess_rewritebase, + '#description' => t('If gzip and/or brotli files are getting a 307 instead of a 200, this might fix the issue. This Drupal install is using "@core_htaccess" in the webroot .htaccess file. Based off of this and the current location of the advagg css/js directories (@css_path, @js_path) this is the recommended value for this field:

@best_guess

Current advagg htaccess values:

@advagg_css_htaccess
@advagg_js_htaccess

Note that the advagg CSS/JS directories are automatically added onto the end of the given input value.', array( + '@core_htaccess' => $core_htaccess, + '@best_guess' => $new_dir, + '@css_path' => $css_path[1], + '@js_path' => $js_path[1], + '@advagg_css_htaccess' => $advagg_css_htaccess, + '@advagg_js_htaccess' => $advagg_js_htaccess, + )), + ); + } + elseif (!empty($advagg_htaccess_rewritebase)) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $advagg_css_htaccess = advagg_htaccess_rewritebase($css_path[1]); + $advagg_js_htaccess = advagg_htaccess_rewritebase($js_path[1]); + // Offer advice if this setting should be disabled. + $form['global_container']['global']['obscure']['advagg_htaccess_rewritebase'] = array( + '#type' => 'textfield', + '#title' => t('AdvAgg RewriteBase Directive in .htaccess files.'), + '#default_value' => $advagg_htaccess_rewritebase, + '#description' => t('This Drupal install is no longer using RewriteBase in the webroot .htaccess file. The recommended value for this field is a blank string (empty)
Current advagg htaccess values:

@advagg_css_htaccess
@advagg_js_htaccess

', array( + '@advagg_css_htaccess' => $advagg_css_htaccess, + '@advagg_js_htaccess' => $advagg_js_htaccess, + )), + ); + } + + $form['global_container']['css'] = array( + '#type' => 'fieldset', + '#title' => t('CSS Options'), + ); + $form['global_container']['css']['advagg_combine_css_media'] = array( + '#type' => 'checkbox', + '#title' => t('Combine CSS files by using media queries (recommended)'), + '#default_value' => variable_get('advagg_combine_css_media', ADVAGG_COMBINE_CSS_MEDIA), + '#description' => t('Will combine more CSS files together because different CSS media types can be used in the same file by using media queries. Use cores grouping logic needs to be unchecked in order for this to work. Also noted is that due to an issue with IE9, compatibility mode is forced off if this is enabled.'), + '#states' => array( + 'disabled' => array( + '#edit-advagg-core-groups' => array('checked' => TRUE), + ), + ), + '#recommended_value' => TRUE, + ); + $form['global_container']['css']['advagg_ie_css_selector_limiter'] = array( + '#type' => 'checkbox', + '#title' => t('Prevent more than %limit CSS selectors in an aggregated CSS file', array('%limit' => variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE))), + '#default_value' => variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER), + '#description' => t('Internet Explorer before version 10; IE9, IE8, IE7, and IE6 all have 4095 as the limit for the maximum number of css selectors that can be in a file. Enabling this will prevent CSS aggregates from being created that exceed this limit. More info. Use cores grouping logic needs to be unchecked in order for this to work.', array('@link' => 'http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/10164546.aspx')), + '#states' => array( + 'disabled' => array( + '#edit-advagg-core-groups' => array('checked' => TRUE), + ), + ), + '#recommended_value' => FALSE, + ); + $form['global_container']['css']['advagg_ie_css_selector_limiter_value'] = array( + '#type' => 'textfield', + '#title' => t('The selector count the IE CSS limiter should use'), + '#default_value' => variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE), + '#description' => t('Internet Explorer before version 10; IE9, IE8, IE7, and IE6 all have 4095 as the limit for the maximum number of css selectors that can be in a file. Use this field to modify the value used; 4095 sometimes may be still be too many with media queries.'), + '#states' => array( + 'visible' => array( + '#edit-advagg-ie-css-selector-limiter' => array('checked' => TRUE), + ), + 'disabled' => array( + '#edit-advagg-ie-css-selector-limiter' => array('checked' => FALSE), + ), + ), + ); + $form['global_container']['css']['advagg_css_fix_type'] = array( + '#type' => 'checkbox', + '#title' => t('Fix improperly set type (recommended)'), + '#default_value' => variable_get('advagg_css_fix_type', ADVAGG_CSS_FIX_TYPE), + '#description' => t('If type is external but does not start with http, https, or // change it to be type file. If type is file but it starts with http, https, or // change type to be external. Note that if this is causing issues, odds are you have a double slash when there should be a single; see this issue', array( + '@link' => 'https://www.drupal.org/node/2336217', + )), + '#recommended_value' => TRUE, + ); + $form['global_container']['css']['advagg_css_remove_empty_files'] = array( + '#type' => 'checkbox', + '#title' => t('Remove empty CSS files from aggregates (recommended)'), + '#default_value' => variable_get('advagg_css_remove_empty_files', ADVAGG_CSS_REMOVE_EMPTY_FILES), + '#description' => t('If an empty CSS file ends up being in its own aggregate, that aggregate can end up as a 404.'), + '#recommended_value' => TRUE, + ); + + $form['global_container']['js'] = array( + '#type' => 'fieldset', + '#title' => t('JS Options'), + ); + $form['global_container']['js']['advagg_js_fix_type'] = array( + '#type' => 'checkbox', + '#title' => t('Fix improperly set type (recommended)'), + '#default_value' => variable_get('advagg_js_fix_type', ADVAGG_JS_FIX_TYPE), + '#description' => t('If type is external but does not start with http, https, or // change it to be type file. If type is file but it starts with http, https, or // change type to be external. Note that if this is causing issues, odds are you have a double slash when there should be a single; see this issue', array( + '@link' => 'https://www.drupal.org/node/2336217', + )), + '#recommended_value' => TRUE, + ); + $form['global_container']['js']['advagg_js_remove_empty_files'] = array( + '#type' => 'checkbox', + '#title' => t('Remove empty JS files from aggregates (recommended)'), + '#default_value' => variable_get('advagg_js_remove_empty_files', ADVAGG_JS_REMOVE_EMPTY_FILES), + '#description' => t('If an empty JS file ends up being in its own aggregate, that aggregate can end up as a 404.'), + '#recommended_value' => TRUE, + ); + $form['global_container']['js']['advagg_scripts_scope_anywhere'] = array( + '#type' => 'checkbox', + '#title' => t('Allow for JS to be added to blocks and views'), + '#default_value' => variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE), + '#description' => t('The scope key when using drupal_add_js and friends can be used to insert script tags throughout the html body. To see possible insertion points set @theme_debug in your settings.php file and view the page source while looking for @comment html comments. Some example scopes:

!example

', array( + '@theme_debug' => '$conf[\'theme_debug\'] = TRUE;', + '@comment' => '', + '!example' => "page_top:prefix
block:prefix:system:powered-by
view:suffix:frontpage:page", + )), + ); + + // Clear the cache bins on submit. + $form['#submit'][] = 'advagg_admin_settings_form_submit'; + + return system_settings_form($form); +} + +/** + * Form builder; Do advagg operations. + */ +function advagg_admin_operations_form($form, $form_state) { + drupal_set_title(t('AdvAgg: Operations')); + advagg_display_message_if_requirements_not_met(); + + // Explain what can be done on this page. + $form['tip'] = array( + '#markup' => '

' . t('This is a collection of commands to control the cache and to manage testing of this module. In general this page is useful when troubleshooting some aggregation issues. For normal operations, you do not need to do anything on this page below the Smart Cache Flush. There are no configuration options here.') . '

', + ); + + // Buttons to do stuff. + // AdvAgg smart cache flushing. + $form['smart_flush'] = array( + '#type' => 'fieldset', + '#title' => t('Smart Cache Flush'), + '#description' => t('Scan all files referenced in aggregated files. If any of them have changed, clear that cache so the changes will go out.'), + ); + $form['smart_flush']['advagg_flush'] = array( + '#type' => 'submit', + '#value' => t('Flush AdvAgg Cache'), + '#submit' => array('advagg_admin_flush_cache_button'), + ); + + // Set/Remove Bypass Cookie. + $form['bypass'] = array( + '#type' => 'fieldset', + '#title' => t('Aggregation Bypass Cookie'), + '#description' => t('This will set or remove a cookie that disables aggregation for a set period of time.'), + ); + $bypass_length = drupal_map_assoc(array( + 60 * 60 * 6, + 60 * 60 * 12, + 60 * 60 * 24, + 60 * 60 * 24 * 2, + 60 * 60 * 24 * 7, + 60 * 60 * 24 * 30, + 60 * 60 * 24 * 365, + ), 'format_interval'); + $form['bypass']['timespan'] = array( + '#type' => 'select', + '#title' => 'Bypass length', + '#options' => $bypass_length, + ); + $form['bypass']['submit'] = array( + '#type' => 'submit', + '#value' => t('Toggle The "aggregation bypass cookie" For This Browser'), + '#attributes' => array('onclick' => 'javascript:return advagg_toggle_cookie()'), + '#submit' => array('advagg_admin_toggle_bypass_cookie'), + ); + // Add in aggregation bypass cookie javascript. + $form['#attached']['js'][] = array( + 'data' => array( + 'advagg' => array( + 'key' => drupal_hmac_base64('advagg_cookie', drupal_get_private_key() . drupal_get_hash_salt() . variable_get('cron_key', 'drupal')), + ), + ), + 'type' => 'setting', + ); + $form['#attached']['js'][] = drupal_get_path('module', 'advagg') . '/advagg.admin.js'; + + // Regenerate .htaccess files. + if (variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE)) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $form['htaccess'] = array( + '#type' => 'fieldset', + '#title' => t('Regenerate .htaccess files'), + '#description' => t('@advagg_css_htaccess
@advagg_js_htaccess', array( + '@advagg_css_htaccess' => $css_path[1] . '/.htaccess', + '@advagg_js_htaccess' => $js_path[1] . '/.htaccess', + )), + ); + $form['htaccess']['advagg_regenerate_htaccess'] = array( + '#type' => 'submit', + '#value' => t('Recreate htaccess files'), + '#submit' => array('advagg_admin_regenerate_htaccess_button'), + ); + } + + // Tasks run by cron. + $form['cron'] = array( + '#type' => 'fieldset', + '#title' => t('Cron Maintenance Tasks'), + '#description' => t('The following 7 operations are ran on cron but you can run them manually here.'), + ); + $form['cron']['smart_file_flush'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Clear All Stale Files'), + '#description' => t('Remove all stale files. Scan all files in the advagg_css/js directories and remove the ones that have not been accessed in the last 30 days.'), + ); + $form['cron']['smart_file_flush']['advagg_flush_stale_files'] = array( + '#type' => 'submit', + '#value' => t('Remove All Stale Files'), + '#submit' => array('advagg_admin_flush_stale_files_button'), + ); + + $form['cron']['delete_empty_aggregates'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete empty aggregates'), + '#description' => t('Delete all aggregates that have no content so that they can be regenerated.'), + ); + $form['cron']['delete_empty_aggregates']['advagg_delete_empty_aggregates'] = array( + '#type' => 'submit', + '#value' => t('Delete empty aggregates'), + '#submit' => array('advagg_delete_empty_aggregates_button'), + ); + + $form['cron']['delete_orphaned_aggregates'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete orphaned aggregates'), + '#description' => t('Scan CSS/JS advagg dir and remove file if there is no associated db record.'), + ); + $form['cron']['delete_orphaned_aggregates']['advagg_delete_orphaned_aggregates'] = array( + '#type' => 'submit', + '#value' => t('Delete orphaned aggregates'), + '#submit' => array('advagg_admin_delete_orphaned_aggregates_button'), + ); + + $form['cron']['remove_missing_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Clear Missing Files From Database'), + '#description' => t('Scan for missing files and remove the associated entries in the database.'), + ); + $form['cron']['remove_missing_files']['advagg_remove_missing_files_from_db'] = array( + '#type' => 'submit', + '#value' => t('Clear Missing Files From Database'), + '#submit' => array('advagg_admin_remove_missing_files_from_db_button'), + ); + + $form['cron']['remove_old_aggregates'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete Unused Aggregates From Database'), + '#description' => t('Delete aggregates that have not been accessed in the last 6 weeks.'), + ); + $form['cron']['remove_old_aggregates']['advagg_remove_old_unused_aggregates'] = array( + '#type' => 'submit', + '#value' => t('Delete Unused Aggregates From Database'), + '#submit' => array('advagg_admin_remove_old_unused_aggregates_button'), + ); + + $form['cron']['cleanup_semaphore_table'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete Orphaned Semaphore Locks'), + '#description' => t('Delete orphaned/expired advagg locks from the semaphore database table.'), + ); + $form['cron']['cleanup_semaphore_table']['advagg_cleanup_semaphore_table'] = array( + '#type' => 'submit', + '#value' => t('Delete Orphaned Semaphore Locks'), + '#submit' => array('advagg_admin_cleanup_semaphore_table_button'), + ); + + $form['cron']['cleanup_temp_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete leftover temporary files'), + '#description' => t('Delete old temporary files from the filesystem.'), + ); + $form['cron']['cleanup_temp_files']['advagg_remove_temp_files'] = array( + '#type' => 'submit', + '#value' => t('Delete leftover temporary files'), + '#submit' => array('advagg_admin_cleanup_temp_files_button'), + ); + + if (module_exists('locale')) { + $form['cron']['refresh_locale_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Refresh all locale files'), + '#description' => t('Run all js files in the advagg_files table through locale_js_alter; loop for each enabled language.'), + ); + $form['cron']['refresh_locale_files']['advagg_refresh_all_locale_files'] = array( + '#type' => 'submit', + '#value' => t('Refresh all locale files'), + '#submit' => array('advagg_admin_refresh_locale_files_button'), + ); + } + + // Hide drastic measures as they should not be done unless you really need it. + $form['drastic_measures'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Drastic Measures'), + '#description' => t('The options below should normally never need to be done.'), + ); + $form['drastic_measures']['dumb_cache_flush'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Clear All Caches'), + '#description' => t('Remove all entries from the advagg cache bins. Useful if you suspect a cache is not getting cleared.'), + ); + $form['drastic_measures']['dumb_cache_flush']['advagg_flush_all_caches'] = array( + '#type' => 'submit', + '#value' => t('Clear All Caches'), + '#submit' => array('advagg_admin_clear_all_caches_button'), + ); + $form['drastic_measures']['dumb_file_flush'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Clear All Files'), + '#description' => t('Remove all generated files. Useful if you think some of the generated files got corrupted and thus need to be deleted.'), + ); + $form['drastic_measures']['dumb_file_flush']['advagg_flush_all_files'] = array( + '#type' => 'submit', + '#value' => t('Remove All Generated Files'), + '#submit' => array('advagg_admin_clear_all_files_button'), + ); + $form['drastic_measures']['force_change'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Force new aggregates'), + '#description' => t('Force the creation of all new aggregates by incrementing a global counter. Current value of counter: %value. This is useful if a CDN has cached an aggregate incorrectly as it will force new ones to be used even if nothing else has changed.', array('%value' => advagg_get_global_counter())), + ); + $form['drastic_measures']['force_change']['increment_global_counter'] = array( + '#type' => 'submit', + '#value' => t('Increment Global Counter'), + '#submit' => array('advagg_admin_increment_global_counter'), + ); + $form['drastic_measures']['rescan_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Rescan all files'), + '#description' => t('Force rescan all files and clear the cache. Useful if a css/js change from a deployment did not work.'), + ); + $form['drastic_measures']['rescan_files']['reset_mtime'] = array( + '#type' => 'submit', + '#value' => t('Reset All mtime Values'), + '#submit' => array('advagg_admin_reset_mtime'), + ); + $form['drastic_measures']['remove_empty_advagg_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Remove deleted files in the advagg_files table'), + '#description' => t('Remove entries in the advagg_files table that have a filesize of 0 and delete the javascript_parsed variable. This gets around the grace period that the cron cleanup does.'), + ); + $form['drastic_measures']['remove_empty_advagg_files']['advagg_admin_remove_empty_advagg_files'] = array( + '#type' => 'submit', + '#value' => t('Remove deleted files from advagg_files'), + '#submit' => array('advagg_admin_remove_empty_advagg_files'), + ); + $form['drastic_measures']['reset_advagg_files'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Reset the AdvAgg Files table'), + '#description' => t('Truncate the advagg_files table and delete the javascript_parsed variable. This may cause some 404s for CSS/JS assets for a short amount of time (seconds). Useful if you really want to reset some stuff. Might be best to put the site into maintenance mode before doing this.'), + ); + $form['drastic_measures']['reset_advagg_files']['truncate_advagg_files'] = array( + '#type' => 'submit', + '#value' => t('Truncate advagg_files'), + '#submit' => array('advagg_admin_truncate_advagg_files'), + ); + + $form['drastic_measures']['clear_base_file'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Delete all aggregates containing this base file.'), + '#description' => t('Pinpoint clearing of all aggregates that contain a file.'), + ); + $form['drastic_measures']['clear_base_file']['filename'] = array( + '#type' => 'textfield', + '#size' => 170, + '#maxlength' => 256, + '#default_value' => '', + '#title' => t('Filename'), + ); + $form['drastic_measures']['clear_base_file']['submit'] = array( + '#type' => 'submit', + '#value' => t('Delete Select Aggregates'), + '#validate' => array('advagg_admin_clear_file_aggregate_validate'), + '#submit' => array('advagg_admin_clear_file_aggregate_submit'), + '#ajax' => array( + 'callback' => 'advagg_admin_clear_file_aggregate_callback', + 'wrapper' => 'advagg-clear-file-aggregate-ajax', + 'effect' => 'fade', + ), + ); + $types = array('css', 'js'); + $css_file = ''; + $js_file = ''; + foreach ($types as $type) { + // Get valid filename. + $results = db_select('advagg_files', 'af') + ->fields('af', array('filename')) + ->condition('filetype', $type) + ->orderBy('filename', 'ASC') + ->execute(); + while ($row = $results->fetchAssoc()) { + if (empty($css_file) && $type === 'css') { + $css_file = $row['filename']; + break; + } + if (empty($js_file) && $type === 'js') { + $js_file = $row['filename']; + break; + } + } + } + $form['drastic_measures']['clear_base_file']['tip'] = array( + '#markup' => '

' . t('Takes input like "@css_file" or "@advagg_js"', array( + '@css_file' => $css_file, + '@advagg_js' => $js_file, + )) . '

', + ); + $form['drastic_measures']['clear_base_file']['wrapper'] = array( + '#markup' => "
", + ); + + return $form; +} + +/** + * Form builder; Show info about advagg and advagg settings. + * + * @see system_settings_form() + */ +function advagg_admin_info_form($form, $form_state) { + drupal_set_title(t('AdvAgg: Information')); + advagg_display_message_if_requirements_not_met(); + + // Explain what can be done on this page. + $form['tip'] = array( + '#markup' => '

' . t('This page provides debugging information. There are no configuration options here.') . '

', + ); + + // Get all hooks and variables. + drupal_theme_initialize(); + $core_hooks = theme_get_registry(); + $advagg_hooks = advagg_hooks_implemented(); + list(, $js_path) = advagg_get_root_files_dir(); + + // Output html process functions hooks. + $form['theme_info'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Hook Theme Info'), + ); + $data = implode("\n", $core_hooks['html']['process functions']); + $form['theme_info']['advagg_theme_info'] = array( + '#markup' => '
' . $data . '
', + ); + + $form['render_info'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Hook Render Info'), + ); + $element_info = array(); + $element_info['scripts'] = element_info('scripts'); + $element_info['styles'] = element_info('styles'); + $output = ''; + foreach ($element_info as $type => &$values) { + $output .= $type . ":\n"; + foreach ($values as $key => &$value) { + if ($key === '#items' || $key === '#type') { + continue; + } + if (empty($value)) { + $output .= ' ' . $key . ' - ' . str_replace(array("\r", "\n"), '', (string) var_export($value, TRUE)) . "\n"; + } + elseif (is_array($value)) { + $output .= ' ' . $key . ' - ' . implode(', ', $value) . "\n"; + } + else { + $output .= ' ' . $key . ' - ' . print_r($value, TRUE) . "\n"; + } + } + } + $form['render_info']['advagg_render_info'] = array( + '#markup' => '
' . $output . '
', + ); + + // Get all parent css and js files. + $types = array('css', 'js'); + $css_file = ''; + foreach ($types as $type) { + $form[$type] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('@type files', array('@type' => drupal_strtoupper($type))), + ); + // Get filename, filename_hash, and changes. + $results = db_select('advagg_files', 'af') + ->fields('af', array('filename', 'filename_hash', 'changes')) + ->condition('filetype', $type) + ->orderBy('filename', 'ASC') + ->execute(); + while ($row = $results->fetchAssoc()) { + if (empty($css_file) && $type === 'css') { + $css_file = basename($row['filename']); + } + + $form[$type][$row['filename_hash']] = array( + '#markup' => '
' . format_plural($row['changes'], 'changed 1 time - %file
', 'changed %changes times - %file
', array( + '%changes' => $row['changes'], + '%file' => $row['filename'], + )), + ); + } + } + + // Display as module -> hook instead of hook -> module. + ksort($advagg_hooks); + $module_hooks = array(); + foreach ($advagg_hooks as $hook => $values) { + if (!empty($values)) { + foreach ($values as $module_name) { + if (!isset($module_hooks[$module_name])) { + $module_hooks[$module_name] = array(); + } + $module_hooks[$module_name][] = $hook; + } + } + else { + $module_hooks['not in use'][] = $hook; + } + } + ksort($module_hooks); + + // Output all advagg hooks implemented. + foreach ($module_hooks as $hook => $values) { + if (empty($values)) { + $form['modules_implementing_advagg'][$hook] = array( + '#markup' => '
' . check_plain($hook) . ': 0
', + ); + } + else { + $form['modules_implementing_advagg'][$hook] = array( + '#markup' => '
' . check_plain($hook) . ': ' . count($values) . '
  ' . filter_xss(implode('
  ', $values), array('br')) . '
', + ); + } + } + $form['modules_implementing_advagg'] += array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Modules implementing AdvAgg CSS/JS hooks'), + ); + + // Output all advagg hooks implemented. + foreach ($advagg_hooks as $hook => $values) { + if (empty($values)) { + $form['hooks_implemented'][$hook] = array( + '#markup' => '
' . check_plain($hook) . ': 0
', + ); + } + else { + $form['hooks_implemented'][$hook] = array( + '#markup' => '
' . check_plain($hook) . ': ' . count($values) . '
  ' . filter_xss(implode('
  ', $values), array('br')) . '
', + ); + } + } + $form['hooks_implemented'] += array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('AdvAgg CSS/JS hooks implemented by modules'), + ); + + // Output what is used inside of the advagg_get_current_hooks_hash() function. + $form['hooks_variables_hash'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Hooks And Variables Used In Hash'), + ); + $form['hooks_variables_hash']['description'] = array( + '#markup' => t('Current Value: %value. Below is the listing of variables and hooks used to generate the 3rd hash of an aggregates filename.', array('%value' => advagg_get_current_hooks_hash())), + ); + $form['hooks_variables_hash']['output'] = array( + // @ignore production_php + '#markup' => '
' . print_r(advagg_current_hooks_hash_array(), TRUE) . '
', + ); + + // Get info about a file. + $form['get_info_about_agg'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#title' => t('Get detailed info about an aggregate file'), + ); + $form['get_info_about_agg']['filename'] = array( + '#type' => 'textfield', + '#size' => 170, + '#maxlength' => 256, + '#default_value' => '', + '#title' => t('Filename'), + ); + $form['get_info_about_agg']['submit'] = array( + '#type' => 'submit', + '#value' => t('Lookup Details'), + '#submit' => array('advagg_admin_get_file_info_submit'), + '#validate' => array('advagg_admin_get_file_info_validate'), + '#ajax' => array( + 'callback' => 'advagg_admin_get_file_info_callback', + 'wrapper' => 'advagg-file-info-ajax', + 'effect' => 'fade', + ), + ); + module_load_include('install', 'advagg', 'advagg'); + $first_file_path = (advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) ? $js_path[0] : $js_path[1]; + $form['get_info_about_agg']['tip'] = array( + '#markup' => '

' . t('Takes input like "@css_file" or a full aggregate name like "@advagg_js"', array( + '@css_file' => $css_file, + '@advagg_js' => advagg_install_get_first_advagg_file($first_file_path, 'js'), + )) . '

', + ); + $form['get_info_about_agg']['wrapper'] = array( + '#markup' => "
", + ); + + return $form; +} + +/** + * Add various advagg settings to the system_performance_settings form. + */ +function advagg_admin_system_performance_settings_form(&$form, $form_state) { + $msg = t('NOTE: If you wish to bypass aggregation for a set amount of time, you can go to the AdvAgg operations page and press the "aggregation bypass cookie" button.', array( + '@operations' => url('admin/config/development/performance/advagg/operations'), + )); + $msg .= ' '; + if (user_access('bypass advanced aggregation')) { + $msg .= t('You can also selectively bypass aggregation by adding @code to the URL of any page.', array( + '@code' => '?advagg=0', + )); + } + else { + $msg .= t('You do not have the bypass advanced aggregation permission so adding @code to the URL will not work at this time for you; either grant this permission to your user role or use the bypass cookie if you wish to selectively bypass aggregation.', array( + '@permission' => url('admin/people/permissions', array('fragment' => 'module-advagg')), + '@code' => '?advagg=0', + )); + } + if (is_callable('omega_extension_enabled') + && is_callable('omega_theme_get_setting') + && omega_extension_enabled('development') + && omega_theme_get_setting('omega_livereload', TRUE) + ) { + $msg .= ' ' . t('The omega theme is in development mode and livereload is also enabled. This setting combination disables CSS aggregation. If you wish to have CSS aggregation turned on, go to the theme settings page, select the Omega theme/subtheme and turn off LiveReload and/or turn off the Development extension.', array('@url' => url('admin/appearance/settings'))); + } + + $form['bandwidth_optimization']['advagg_note'] = array( + '#markup' => $msg, + ); +} + +/** + * @} End of "defgroup advagg_forms". + */ + +/** + * @defgroup advagg_forms_callback AdvAgg forms callbacks and validation. + * @{ + * Functions that handle user input from forms. + */ + +/** + * Clear out the advagg cache bin when the save configuration button is pressed. + */ +function advagg_admin_settings_form_submit($form, &$form_state) { + // Remove non variables. + if (isset($form_state['values']['advagg_resource_hints_preload_reset'])) { + unset($form_state['values']['advagg_resource_hints_preload_reset']); + } + + // Reset this form to defaults or recommended values; also show what changed. + advagg_set_admin_form_defaults_recommended($form_state, 'advagg_admin_mode'); + + // Sort and fix values before saving to the db. + foreach ($form_state['values']['advagg_resource_hints_preload_settings'] as &$entry) { + if (isset($entry['weight'])) { + $entry['#weight'] = $entry['weight']; + unset($entry['weight']); + } + ksort($entry); + } + uasort($form_state['values']['advagg_resource_hints_preload_settings'], 'element_sort'); + + // Make sure .htaccess file exists in the advagg dir. + if (isset($form_state['values']['advagg_resource_hints_use_immutable']) + && variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE) != $form_state['values']['advagg_resource_hints_use_immutable'] + && variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE) + ) { + // Set variable. + variable_set('advagg_resource_hints_use_immutable', $form_state['values']['advagg_resource_hints_use_immutable']); + unset($form_state['values']['advagg_resource_hints_use_immutable']); + + // Get paths to .htaccess file. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files['css'] = $css_path[0] . '/.htaccess'; + $files['js'] = $js_path[0] . '/.htaccess'; + + // Make the advagg_htaccess_check_generate() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + + // Generate new .htaccess files in advagg dirs. + foreach ($files as $type => $uri) { + advagg_htaccess_check_generate(array($uri => $type), $type, TRUE); + } + } + + // Alter htaccess if needed. + if (isset($form_state['values']['advagg_htaccess_rewritebase']) + && variable_get('advagg_htaccess_rewritebase', ADVAGG_HTACCESS_REWRITEBASE) != $form_state['values']['advagg_htaccess_rewritebase'] + && variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE) + ) { + // Set variable. + variable_set('advagg_htaccess_rewritebase', trim($form_state['values']['advagg_htaccess_rewritebase'])); + unset($form_state['values']['advagg_htaccess_rewritebase']); + + // Get paths to .htaccess file. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files['css'] = $css_path[0] . '/.htaccess'; + $files['js'] = $js_path[0] . '/.htaccess'; + + // Make the advagg_htaccess_check_generate() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + + // Generate new .htaccess files in advagg dirs. + foreach ($files as $type => $uri) { + advagg_htaccess_check_generate(array($uri => $type), $type, TRUE); + } + } + + if (variable_get('advagg_htaccess_symlinksifownermatch', ADVAGG_HTACCESS_SYMLINKSIFOWNERMATCH) != $form_state['values']['advagg_htaccess_symlinksifownermatch'] + && variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE) + ) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files['css'] = $css_path[0] . '/.htaccess'; + $files['js'] = $js_path[0] . '/.htaccess'; + + // Make the advagg_htaccess_check_generate() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + + // Push out new setting. + variable_set('advagg_htaccess_symlinksifownermatch', $form_state['values']['advagg_htaccess_symlinksifownermatch']); + unset($form_state['values']['advagg_htaccess_symlinksifownermatch']); + foreach ($files as $type => $uri) { + advagg_htaccess_check_generate(array($uri => $type), $type, TRUE); + } + } + + // Clear caches. + advagg_cache_clear_admin_submit(); +} + +/** + * Set or remove the AdvAggDisabled cookie. + */ +function advagg_admin_toggle_bypass_cookie($form, &$form_state) { + $cookie_name = 'AdvAggDisabled'; + $key = drupal_hmac_base64('advagg_cookie', drupal_get_private_key() . drupal_get_hash_salt() . variable_get('cron_key', 'drupal')); + + // If the cookie does exist then remove it. + if (!empty($_COOKIE[$cookie_name]) && $_COOKIE[$cookie_name] == $key) { + setcookie($cookie_name, '', -1, $GLOBALS['base_path'], '.' . $_SERVER['HTTP_HOST']); + unset($_COOKIE[$cookie_name]); + drupal_set_message(t('AdvAgg Bypass Cookie Removed.')); + } + // If the cookie does not exist then set it. + else { + // Cookie will last for 12 hours. + setcookie($cookie_name, $key, REQUEST_TIME + $form_state['values']['timespan'], $GLOBALS['base_path'], '.' . $_SERVER['HTTP_HOST']); + $_COOKIE[$cookie_name] = $key; + drupal_set_message(t('AdvAgg Bypass Cookie Set for %time.', array('%time' => format_interval($form_state['values']['timespan'])))); + } +} + +/** + * Display file info in a drupal message. + */ +function advagg_admin_get_file_info_submit($form, &$form_state) { + // @codingStandardsIgnoreLine + if (!empty($form_state['input']['ajax_page_state'])) { + return; + } + $info = advagg_admin_get_file_info($form_state['values']['filename']); + if (module_exists('httprl')) { + $output = httprl_pr($info); + } + else { + $output = '
' . check_plain(print_r($info, TRUE)) . '
'; + } + // @ignore security_dsm + drupal_set_message($output); +} + +/** + * Display file info via ajax callback. + */ +function advagg_admin_get_file_info_callback($form, &$form_state) { + if (!empty($form_state['values']['error'])) { + return '
'; + } + $info = advagg_admin_get_file_info($form_state['values']['filename']); + if (empty($info)) { + form_set_error('filename', t('Please input a valid aggregate filename.')); + return '
'; + } + else { + if (module_exists('httprl')) { + $output = httprl_pr($info); + } + else { + $output = '
' . print_r($info, TRUE) . '
'; + } + return '
' . $output . '
'; + } +} + +/** + * Verify that the filename is correct. + */ +function advagg_admin_get_file_info_validate($form, &$form_state) { + if (empty($form_state['values']['filename'])) { + form_set_error('filename', t('Please input an aggregate filename.')); + $form_state['values']['error'] = TRUE; + } +} + +/** + * Get detailed info about the given filename. + * + * @param string $filename + * Name of file to lookup. + * + * @return array + * Returns an array of detailed info about this file. + */ +function advagg_admin_get_file_info($filename) { + module_load_include('inc', 'advagg', 'advagg.missing'); + module_load_include('inc', 'advagg', 'advagg'); + + // Strip quotes and trim. + $filename = trim(str_replace(array('"', "'"), '', $filename)); + + $data = advagg_get_hashes_from_filename(basename($filename)); + $output = array(); + if (is_array($data)) { + list($type, $aggregate_filenames_hash, $aggregate_contents_hash) = $data; + + // Get a list of files. + $files = advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash); + if (empty($files)) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + // Skip if the file exists. + if ($type === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + if (file_exists($uri)) { + $atime = advagg_get_atime($aggregate_filenames_hash, $aggregate_contents_hash, $uri); + if (REQUEST_TIME - $atime > variable_get('drupal_stale_file_threshold', 2592000)) { + $files = t('This is an old aggregate, it should be deleted on the next cron run.'); + } + else { + $files = t('This is an old aggregate, it should be deleted on the cron run after !time.', array('!time' => format_interval(variable_get('drupal_stale_file_threshold', 2592000) - (REQUEST_TIME - $atime)))); + } + } + else { + $files = t('This aggregate file no longer exists.'); + } + } + $data['files'] = $files; + + // Get detailed info on each file. + $files_info_filenames = array(); + foreach ($data['files'] as $filename => &$info) { + $files_info_filenames[] = $filename; + } + unset($info); + + // Get filesystem data. + $files_info = advagg_get_info_on_files($files_info_filenames); + + foreach ($data['files'] as $filename => &$info) { + $info += $files_info[$filename]; + if (module_exists('advagg_bundler')) { + $bundler = advagg_bundler_analysis($filename); + $info['group_hash'] = $bundler['group_hash']; + } + } + unset($info); + $output = $data; + } + else { + $results = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename', '%' . db_like($filename), 'LIKE') + ->execute(); + while ($row = $results->fetchAssoc()) { + $row += advagg_get_info_on_file($row['filename']); + if (module_exists('advagg_bundler')) { + $bundler = advagg_bundler_analysis($row['filename']); + $row['group_hash'] = $bundler['group_hash']; + } + $output[] = $row; + } + } + return $output; +} + +/** + * Reset the advagg_resource_hints_preload_settings variable. + */ +function advagg_admin_resource_hints_preload_reset() { + variable_del('advagg_resource_hints_preload_settings'); +} + +/** + * Recreate the advagg htaccess files. + */ +function advagg_admin_regenerate_htaccess_button() { + // Get paths to .htaccess file. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files['css'] = $css_path[0] . '/.htaccess'; + $files['js'] = $js_path[0] . '/.htaccess'; + + // Make the advagg_htaccess_check_generate() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + + // Generate new .htaccess files in advagg dirs. + $errors = array(); + foreach ($files as $type => $uri) { + $errors += advagg_htaccess_check_generate(array($uri => $type), $type, TRUE); + } + if (empty($errors)) { + drupal_set_message(t('The .htaccess files have been regenerated.')); + } + else { + drupal_set_message(t('The .htaccess files failed to be regenerated. @errors', array('@errors' => implode(', ', $errors)))); + } +} + +/** + * Perform a smart flush. + */ +function advagg_admin_flush_cache_button() { + // Clear the libraries cache. + if (function_exists('libraries_flush_caches')) { + $cache_tables = libraries_flush_caches(); + foreach ($cache_tables as $table) { + cache_clear_all('*', $table, TRUE); + } + } + + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + $flushed = advagg_push_new_changes(); + + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 0) { + // Display a simple message if not in Development mode. + drupal_set_message(t('Advagg Cache Cleared')); + } + else { + list($css_path) = advagg_get_root_files_dir(); + $parts_uri = $css_path[1] . '/parts'; + + // Report back the results. + foreach ($flushed as $filename => $data) { + if (strpos($filename, $parts_uri) === 0) { + // Do not report on css files manged in the parts directory. + unset($flushed[$filename]); + continue; + } + $ext = pathinfo($filename, PATHINFO_EXTENSION); + drupal_set_message(t('The file %filename has changed. %db_usage aggregates are using this file. %db_count db cache entries and all %type full cache entries have been flushed from the cache bins. Trigger: @changes', array( + '%filename' => $filename, + '%db_usage' => $data[0], + '%db_count' => $data[1], + '@changes' => print_r($data[2], TRUE), + '%type' => $ext, + ))); + } + + if (empty($flushed)) { + drupal_set_message(t('No changes found. Nothing was cleared.')); + return; + } + } +} + +/** + * Clear out all advagg cache bins. + */ +function advagg_admin_clear_all_caches_button() { + // Clear the libraries cache. + if (function_exists('libraries_flush_caches')) { + $cache_tables = libraries_flush_caches(); + foreach ($cache_tables as $table) { + cache_clear_all('*', $table, TRUE); + } + } + + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + advagg_flush_all_cache_bins(); + + // Report back the results. + drupal_set_message(t('All AdvAgg cache bins have been cleared.')); + // Clear it again at the end of the request to be sure. + drupal_register_shutdown_function('advagg_flush_all_cache_bins', FALSE); + drupal_register_shutdown_function('advagg_push_new_changes'); +} + +/** + * Clear out all advagg aggregated files. + */ +function advagg_admin_clear_all_files_button() { + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + list($css_files, $js_files) = advagg_remove_all_aggregated_files(); + + // Report back the results. + drupal_set_message(t('All AdvAgg files have been deleted. %css_count CSS files and %js_count JS files have been removed.', array( + '%css_count' => count($css_files), + '%js_count' => count($js_files), + ))); +} + +/** + * Clear out all stale advagg aggregated files. + */ +function advagg_admin_flush_stale_files_button() { + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + list($css_files, $js_files) = advagg_delete_stale_aggregates(); + + // Report back the results. + if (count($css_files) > 0 || count($js_files) > 0) { + drupal_set_message(t('All stale aggregates have been deleted. %css_count CSS files and %js_count JS files have been removed.', array( + '%css_count' => count($css_files), + '%js_count' => count($js_files), + ))); + } + else { + drupal_set_message(t('No stale aggregates found. Nothing was deleted.')); + } +} + +/** + * Delete all empty advagg aggregated files. + */ +function advagg_delete_empty_aggregates_button() { + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + list($css_files, $js_files) = advagg_delete_empty_aggregates(); + + // Report back the results. + if (count($css_files) > 0 || count($js_files) > 0) { + drupal_set_message(t('All empty aggregates have been deleted. %css_count CSS files and %js_count JS files have been removed.', array( + '%css_count' => count($css_files), + '%js_count' => count($js_files), + ))); + } + else { + drupal_set_message(t('No empty aggregates found. Nothing was deleted.')); + } +} + +/** + * Clear out all advagg cache bins and increment the counter. + */ +function advagg_admin_increment_global_counter() { + // Clear out the cache. + advagg_admin_clear_all_caches_button(); + + // Increment counter. + module_load_include('inc', 'advagg', 'advagg.cache'); + $new_value = advagg_increment_global_counter(); + drupal_set_message(t('Global counter is now set to %new_value', array('%new_value' => $new_value))); +} + +/** + * Clear out all advagg cache bins and increment the counter. + */ +function advagg_admin_reset_mtime() { + // Reset mtime. + db_update('advagg_files') + ->fields(array( + 'mtime' => 0, + )) + ->execute(); + drupal_set_message(t('All files have been rescanned.')); + + // Smart cache clear. + advagg_admin_flush_cache_button(); + + // Clear out the cache. + advagg_admin_clear_all_caches_button(); +} + +/** + * Remove filesize zero files from the advagg_files table and clear caches. + */ +function advagg_admin_remove_empty_advagg_files() { + // Remove dead files from the advagg_files table. + db_delete('advagg_files') + ->condition('filesize', 0) + ->execute(); + variable_del('javascript_parsed'); + drupal_set_message(t('All empty files in the advagg_files table have been removed.')); + + // Clear out the cache. + advagg_admin_clear_all_caches_button(); +} + +/** + * Truncate the advagg_files table and clear caches. + */ +function advagg_admin_truncate_advagg_files() { + // Truncate advagg_files table. + db_truncate('advagg_files')->execute(); + variable_del('javascript_parsed'); + drupal_set_message(t('All files from the advagg_files table have been removed.')); + + // Clear out the cache. + advagg_admin_clear_all_caches_button(); +} + +/** + * Scan CSS/JS advagg dir and remove file if there is no associated db record. + */ +function advagg_admin_delete_orphaned_aggregates_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Remove aggregates that include missing files. + $deleted = advagg_delete_orphaned_aggregates(); + if (empty($deleted[0]) && empty($deleted[1])) { + drupal_set_message(t('All files have an associated db record; nothing was deleted.')); + } + else { + drupal_set_message(t('Some files had no associated db record and could be safely deleted from the file system. @raw', array('@raw' => print_r($deleted[1], TRUE)))); + } +} + +/** + * Scan for missing files and remove the associated entries in the database. + */ +function advagg_admin_remove_missing_files_from_db_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Remove aggregates that include missing files. + $deleted = advagg_remove_missing_files_from_db(); + if (empty($deleted)) { + drupal_set_message(t('No missing files found or they could not be safely cleared out of the database.')); + } + else { + drupal_set_message(t('Some missing files were found and could be safely cleared out of the database.
 @raw 
', array('@raw' => print_r($deleted, TRUE)))); + } +} + +/** + * Delete aggregates that have not been accessed in the last 6 weeks. + */ +function advagg_admin_remove_old_unused_aggregates_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Remove unused aggregates. + $count = advagg_remove_old_unused_aggregates(); + if (empty($count)) { + drupal_set_message(t('No old and unused aggregates found. Nothing was deleted.')); + } + else { + drupal_set_message(t('Some old and unused aggregates were found. A total of %count database entries were removed.', array('%count' => $count))); + } +} + +/** + * Delete orphaned/expired advagg locks from the semaphore database table. + */ +function advagg_admin_cleanup_semaphore_table_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Delete orphaned/expired advagg locks from the semaphore database table. + $count = advagg_cleanup_semaphore_table(); + if (empty($count)) { + drupal_set_message(t('No orphaned advagg semaphore database table locks discovered. Nothing was deleted.')); + } + else { + drupal_set_message(format_plural($count, '1 orphaned advagg semaphore database table lock was found. A total of 1 database entry was removed.', 'Some orphaned advagg semaphore database table locks were found. A total of @count database entries were removed.')); + } +} + +/** + * Delete orphaned/expired advagg locks from the semaphore database table. + */ +function advagg_admin_refresh_locale_files_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Refresh all locale files. + $locale_files = advagg_refresh_all_locale_files(); + if (empty($locale_files)) { + drupal_set_message(t('No locale files are being used.')); + } + else { + drupal_set_message(t('The following locale files are being used:
@files
', array('@files' => print_r($locale_files, TRUE)))); + } +} + +/** + * Delete leftover temp files. + */ +function advagg_admin_cleanup_temp_files_button() { + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Delete orphaned/expired advagg locks from the semaphore database table. + $count = advagg_remove_temp_files(); + if (empty($count)) { + drupal_set_message(t('No leftover temp files found. Nothing was deleted.')); + } + else { + drupal_set_message(format_plural($count, '1 leftover temp files from advagg was found. A total of 1 temp files was removed.', 'Some leftover temp files from advagg were found. A total of @count temp files were removed.')); + } +} + +/** + * Verify that the filename is correct. + */ +function advagg_admin_clear_file_aggregate_validate($form, &$form_state) { + if (empty($form_state['values']['filename'])) { + form_set_error('filename', t('Please input a filename.')); + $form_state['values']['error'] = TRUE; + } +} + +/** + * Display what files where deleted in a drupal message. + */ +function advagg_admin_clear_file_aggregate_submit($form, &$form_state) { + // @codingStandardsIgnoreLine + if (!empty($form_state['input']['ajax_page_state'])) { + return; + } + $info = advagg_admin_clear_file_aggregates($form_state['values']['filename']); + if (module_exists('httprl')) { + $output = httprl_pr($info); + } + else { + $output = '
' . check_plain(print_r($info, TRUE)) . '
'; + } + // @ignore security_dsm + drupal_set_message($output); +} + +/** + * Display what files where deleted via ajax callback. + */ +function advagg_admin_clear_file_aggregate_callback($form, &$form_state) { + if (!empty($form_state['values']['error'])) { + return '
'; + } + $info = advagg_admin_clear_file_aggregates($form_state['values']['filename']); + if (empty($info)) { + form_set_error('filename', t('Please input a valid filename.')); + return '
'; + } + else { + if (module_exists('httprl')) { + $output = httprl_pr($info); + } + else { + $output = '
' . print_r($info, TRUE) . '
'; + } + return '
' . $output . '
'; + } +} + +/** + * Remove the aggregates that contain the given filename. + * + * @param string $filename + * Name of file to lookup. Can be a comma separated list. + * @param bool $dry_run + * If TRUE, return the regex search string. + * + * @return array + * Returns an array of the parent file and what children where deleted. + */ +function advagg_admin_clear_file_aggregates($filename, $dry_run = FALSE) { + module_load_include('inc', 'advagg', 'advagg.cache'); + list($css_path, $js_path) = advagg_get_root_files_dir(); + $space = ADVAGG_SPACE; + $output = array(); + $options = array('callback' => 'file_unmanaged_delete'); + if ($dry_run) { + $options = array(); + } + + // Strip quotes and trim. + $filenames = array_map('trim', explode(',', trim(str_replace(array('"', "'"), '', $filename)))); + + foreach ($filenames as $filename) { + $results = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename', $filename) + ->execute(); + while ($row = $results->fetchAssoc()) { + // Get aggregates that use this file. + $row['aggregates_using_file'] = advagg_get_aggregates_using_file($row['filename_hash']); + // Get dir and other info from file. + if ($row['filetype'] === 'css') { + $dirname = $css_path[0]; + $basename_prefix = "{$row['filetype']}"; + } + if ($row['filetype'] === 'js') { + $dirname = $js_path[0]; + $basename_prefix = "{$row['filetype']}"; + } + + // Build regex search string for file_scan_directory(). + $regex_search = array(); + foreach ($row['aggregates_using_file'] as $values) { + $regex_search[] = preg_quote("{$basename_prefix}{$space}{$values['aggregate_filenames_hash']}{$space}") . '.*'; + } + $regex_search = array_unique($regex_search); + $regex_search_string = '/(' . implode('|', $regex_search) . ')/'; + $files = file_scan_directory($dirname, $regex_search_string, $options); + + // List what files were deleted. + $row['aggregates_deleted'] = array(); + $files_deleted = array_keys($files); + if (!empty($files_deleted)) { + $row['aggregates_deleted'][] = $files_deleted; + } + + $output[$filename] = $row['aggregates_deleted']; + } + } + + return $output; +} + +/** + * @} End of "defgroup advagg_forms_callback". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.admin.js b/frontend/drupal/sites/all/modules/advagg/advagg.admin.js new file mode 100644 index 000000000..5a4e29ec2 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.admin.js @@ -0,0 +1,132 @@ +/** + * @file + * Used to toggle the AdvAgg Bypass Cookie client side. + */ + +/* global Drupal:false */ +/* eslint-disable no-unused-vars */ + +/** + * Test to see if the given string contains unicode. + * + * @param {int} interval + * String to test. + * @param {int} granularity + * String to test. + * @param {string} langcode + * Language used in translation. + * + * @return {bool} + * true if string contains non ASCII characters. + * false if string only contains ASCII characters. + */ +Drupal.formatInterval = function (interval, granularity, langcode) { + 'use strict'; + granularity = typeof granularity !== 'undefined' ? granularity : 2; + langcode = typeof langcode !== 'undefined' ? langcode : null; + var output = ''; + + /* eslint-disable key-spacing */ + while (granularity > 0) { + var value = 0; + if (interval >= 31536000) { + value = 31536000; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 year', '@count years', {langcode : langcode}); + } + else if (interval >= 2592000) { + value = 2592000; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 month', '@count months', {langcode : langcode}); + } + else if (interval >= 604800) { + value = 604800; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 week', '@count weeks', {langcode : langcode}); + } + else if (interval >= 86400) { + value = 86400; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 day', '@count days', {langcode : langcode}); + } + else if (interval >= 3600) { + value = 3600; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 hour', '@count hours', {langcode : langcode}); + } + else if (interval >= 60) { + value = 60; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 min', '@count min', {langcode : langcode}); + } + else if (interval >= 1) { + value = 1; + output += (output.length ? ' ' : '') + Drupal.formatPlural(Math.floor(interval / value), '1 sec', '@count sec', {langcode : langcode}); + } + + interval %= value; + granularity--; + } + + return output.length ? output : Drupal.t('0 sec', {}, {langcode : langcode}); + /* eslint-enable key-spacing */ +}; + +/** + * Test to see if the given string contains unicode. + * + * @param {string} str + * String to test. + * + * @return {bool} + * true if string contains non ASCII characters. + * false if string only contains ASCII characters. + */ +function advagg_is_unicode(str) { + 'use strict'; + for (var i = 0, n = str.length; i < n; i++) { + if (str.charCodeAt(i) > 255) { + return true; + } + } + return false; +} + +/** + * Toggle the advagg cookie. + * + * @return {bool} + * true if hostname contains unicode. + * false so the form does not get submitted. + */ +function advagg_toggle_cookie() { + 'use strict'; + // Fallback to submitting the form for Unicode domains like ".рф". + if (advagg_is_unicode(document.location.hostname)) { + return true; + } + + var cookie_name = 'AdvAggDisabled'; + + // See if the cookie exists. + var cookie_pos = document.cookie.indexOf(cookie_name + '=' + Drupal.settings.advagg.key); + + // If the cookie does exist then remove it. + if (cookie_pos !== -1) { + document.cookie = + cookie_name + '=' + + '; expires=Thu, 01 Jan 1970 00:00:00 GMT' + + '; path=' + Drupal.settings.basePath + + '; domain=.' + document.location.hostname + ';'; + alert(Drupal.t('AdvAgg Bypass Cookie Removed')); + } + // If the cookie does not exist then set it. + else { + var bypass_length = document.getElementById('edit-timespan').value; + var expire_date = new Date(new Date().getTime() + bypass_length * 1000); + + document.cookie = + cookie_name + '=' + Drupal.settings.advagg.key + + '; expires=' + expire_date.toGMTString() + + '; path=' + Drupal.settings.basePath + + '; domain=.' + document.location.hostname + ';'; + alert(Drupal.t('AdvAgg Bypass Cookie Set for @time.', {'@time': Drupal.formatInterval(bypass_length)})); + } + + // Must return false, if returning true then form gets submitted. + return false; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.advagg.inc b/frontend/drupal/sites/all/modules/advagg/advagg.advagg.inc new file mode 100644 index 000000000..bd6aba380 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.advagg.inc @@ -0,0 +1,662 @@ + $contents). + // * @param array $aggregate_settings + // * Array of settings. + // * @param array $other_parameters + // * Array of containing $files and $type. + $file_types = array(); + // Handle gzip. + if (!empty($aggregate_settings['variables']['advagg_gzip'])) { + // Use zopfli_encode if it exists. + // See https://github.com/kjdev/php-ext-zopfli + if (function_exists('zopfli_encode') + && defined('ZOPFLI_GZIP') + && empty($aggregate_settings['variables']['advagg_no_zopfli']) + ) { + $file_types['.gz'] = array('zopfli_encode', 15, constant('ZOPFLI_GZIP')); + } + else { + $file_types['.gz'] = array('gzencode', 9, FORCE_GZIP); + } + } + // Handle brotli. + // See https://github.com/kjdev/php-ext-brotli + if (!empty($aggregate_settings['variables']['advagg_brotli']) + && defined('BROTLI_TEXT') + && function_exists('brotli_compress') + ) { + $file_types['.br'] = array('brotli_compress', 11, constant('BROTLI_TEXT')); + } + + if (empty($file_types)) { + return; + } + + // Special S3 handling. + $s3fs = FALSE; + $files_to_save_keys = array_keys($files_to_save); + foreach ($files_to_save_keys as $uri) { + $wrapper = file_stream_wrapper_get_instance_by_uri($uri); + if ($wrapper && get_class($wrapper) === 'S3fsStreamWrapper') { + $s3fs = TRUE; + break; + } + } + + foreach ($file_types as $ext => $settings) { + // See if a file already exists with this extension. + $ext_exists = FALSE; + foreach ($files_to_save as $uri => $contents) { + // See if this uri contains $ext near the end of it. + if (strlen($uri) > 91 + strlen(ADVAGG_SPACE) * 3) { + $pos = strripos($uri, $ext, 91 + strlen(ADVAGG_SPACE) * 3); + } + else { + $pos = strripos($uri, $ext); + } + if (!empty($pos)) { + $len = strlen($uri); + // $ext file exists, exit loop. + if ($pos == $len - 3) { + $ext_exists = TRUE; + break; + } + } + } + + // If a $ext file does not exist, create one. + if (!$ext_exists) { + // Use the first file in the array. + $data = reset($files_to_save); + $uri = key($files_to_save); + // Compress it and add it to the $files_to_save array. + $callback = $settings[0]; + $settings[0] = $data; + $compressed = call_user_func_array($callback, $settings); + if (!empty($compressed)) { + if ($s3fs && $ext === '.gz') { + // Only serve gzip files from S3. + $files_to_save[$uri] = $compressed; + } + elseif ($s3fs && $ext === '.br' && !isset($file_types['.gz'])) { + // Only serve br files from S3. + $files_to_save[$uri] = $compressed; + } + else { + $files_to_save[$uri . $ext] = $compressed; + } + } + } + } +} + +/** + * Implements hook_advagg_build_aggregate_plans_alter(). + * + * Used to alter the plan so it has the same grouping as cores. + */ +function advagg_advagg_build_aggregate_plans_alter(array &$files, &$modified, $type) { + // * @param array $files + // * List of files in the aggregate as well as the aggregate name. + // * @param bool $modified + // * Change this to TRUE if $files has been changed. + // * @param string $type + // * String containing css or js. + // + // Do nothing if core grouping is disabled. + if (!variable_get('advagg_core_groups', ADVAGG_CORE_GROUPS)) { + return; + } + + $temp_new_files = array(); + $counter = 0; + foreach ($files as $data) { + $group = NULL; + $every_page = NULL; + foreach ($data['files'] as $fileinfo) { + // Grouped by group and every_page variables. + if (is_null($group)) { + $group = $fileinfo['group']; + } + if (is_null($every_page)) { + $every_page = $fileinfo['every_page']; + } + + // Bump Counter if group/every_page has changed from the last one. + if ($group != $fileinfo['group'] || $every_page != $fileinfo['every_page']) { + ++$counter; + $group = $fileinfo['group']; + $every_page = $fileinfo['every_page']; + $modified = TRUE; + } + $temp_new_files[$counter][] = $fileinfo; + } + ++$counter; + } + + // Replace $files array with new aggregate filenames. + $files = advagg_generate_filenames(array($temp_new_files), $type); +} + +/** + * Implements hook_advagg_context_alter(). + */ +function advagg_advagg_context_alter(&$original, $aggregate_settings, $mode) { + if ($mode == 0) { + // Change context. + $original['base_root'] = $GLOBALS['base_root']; + $original['base_url'] = $GLOBALS['base_url']; + $original['base_path'] = $GLOBALS['base_path']; + $original['is_https'] = $GLOBALS['is_https']; + $original['language'] = isset($GLOBALS['language']) ? $GLOBALS['language'] : NULL; + + $GLOBALS['is_https'] = $aggregate_settings['variables']['is_https']; + if ($aggregate_settings['variables']['is_https']) { + $GLOBALS['base_root'] = str_replace('http://', 'https://', $GLOBALS['base_root']); + } + else { + $GLOBALS['base_root'] = str_replace('https://', 'http://', $GLOBALS['base_root']); + } + $GLOBALS['base_path'] = $aggregate_settings['variables']['base_path']; + $GLOBALS['base_url'] = rtrim($GLOBALS['base_root'] . $GLOBALS['base_path'], '/'); + + if (isset($aggregate_settings['variables']['language'])) { + $languages = language_list(); + if (isset($languages[$aggregate_settings['variables']['language']])) { + $GLOBALS['language'] = $languages[$aggregate_settings['variables']['language']]; + } + } + } + elseif ($mode == 1) { + // Change context back. + if (isset($original['base_root'])) { + $GLOBALS['base_root'] = $original['base_root']; + } + if (isset($original['base_url'])) { + $GLOBALS['base_url'] = $original['base_url']; + } + if (isset($original['base_path'])) { + $GLOBALS['base_path'] = $original['base_path']; + } + if (isset($original['is_https'])) { + $GLOBALS['is_https'] = $original['is_https']; + } + if (isset($original['language'])) { + $GLOBALS['language'] = $original['language']; + } + } + + // Moved this in here due to incomplete bug reports from + // https://www.drupal.org/node/1942230. I can not reproduce the reported + // issues with the patch for the CDN module so I've moved the code into + // advagg. + if (!function_exists('cdn_advagg_context_alter') && module_exists('cdn')) { + if ($mode == 0) { + // Save original context. + $original[CDN_MODE_VARIABLE] = variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC); + $original[CDN_BASIC_FARFUTURE_VARIABLE] = variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT); + $original[CDN_HTTPS_SUPPORT_VARIABLE] = variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE); + $original[CDN_STATUS_VARIABLE] = variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED); + + // Set context for file_create_url()/cdn_file_url_alter(). + $GLOBALS['conf'][CDN_MODE_VARIABLE] = isset($aggregate_settings['variables'][CDN_MODE_VARIABLE]) + ? $aggregate_settings['variables'][CDN_MODE_VARIABLE] + : variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC); + $GLOBALS['conf'][CDN_BASIC_FARFUTURE_VARIABLE] = isset($aggregate_settings['variables'][CDN_BASIC_FARFUTURE_VARIABLE]) + ? $aggregate_settings['variables'][CDN_BASIC_FARFUTURE_VARIABLE] + : variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT); + $GLOBALS['conf'][CDN_HTTPS_SUPPORT_VARIABLE] = isset($aggregate_settings['variables'][CDN_HTTPS_SUPPORT_VARIABLE]) + ? $aggregate_settings['variables'][CDN_HTTPS_SUPPORT_VARIABLE] + : variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE); + $GLOBALS['conf'][CDN_STATUS_VARIABLE] = isset($aggregate_settings['variables'][CDN_STATUS_VARIABLE]) + ? $aggregate_settings['variables'][CDN_STATUS_VARIABLE] + : variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED); + + // Disable CDN if cdn_check_drupal_path is FALSE. + if (empty($aggregate_settings['variables']['cdn_check_drupal_path'])) { + $original[CDN_STATUS_VARIABLE] = CDN_DISABLED; + } + + // Handle HTTPS. + if (!empty($aggregate_settings['variables']['cdn_request_is_https']) && !cdn_request_is_https()) { + if (isset($_SERVER['HTTPS'])) { + $original['HTTPS'] = $_SERVER['HTTPS']; + } + else { + $original['HTTPS'] = FALSE; + } + $_SERVER['HTTPS'] = 'on'; + } + } + elseif ($mode == 1) { + // Set context back. + $GLOBALS['conf'][CDN_MODE_VARIABLE] = $original[CDN_MODE_VARIABLE]; + $GLOBALS['conf'][CDN_BASIC_FARFUTURE_VARIABLE] = $original[CDN_BASIC_FARFUTURE_VARIABLE]; + $GLOBALS['conf'][CDN_HTTPS_SUPPORT_VARIABLE] = $original[CDN_HTTPS_SUPPORT_VARIABLE]; + $GLOBALS['conf'][CDN_STATUS_VARIABLE] = $original[CDN_STATUS_VARIABLE]; + + // Handle HTTPS. + if (isset($original['HTTPS']) && !is_null($original['HTTPS'])) { + $_SERVER['HTTPS'] = $original['HTTPS']; + } + } + } +} + +/** + * Implements hook_advagg_get_info_on_files_alter(). + * + * Used to make sure the info is up to date in the cache. + */ +function advagg_advagg_get_info_on_files_alter(&$return, $cached_data, $bypass_cache) { + // Check for the ie_css_selector_limiter. + if (variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER)) { + $limit_value = variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE); + + // Get the css path. + list($css_path) = advagg_get_root_files_dir(); + $css_parts_path = (advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) ? $css_path[0] : $css_path[1]; + $parts_path = $css_parts_path . '/parts/'; + + foreach ($return as &$info) { + // Skip if not a css file. + if (empty($info['fileext']) || $info['fileext'] !== 'css') { + continue; + } + + // Check if this is a split css file. + if (strpos($info['data'], $parts_path) !== FALSE) { + $info['split'] = TRUE; + } + // Break large file into multiple small files if needed. + elseif ($info['linecount'] > $limit_value) { + advagg_split_css_file($info); + } + } + unset($info); + } + + // Capture resource_hints. + if (variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT) + || variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD) + ) { + $aggregate_settings = advagg_current_hooks_hash_array(); + foreach ($return as &$info) { + // Skip if not a css file. + if (empty($info['fileext']) || $info['fileext'] !== 'css') { + continue; + } + // Get the file contents. + $file_contents = (string) @advagg_file_get_contents($info['data']); + $file_contents = advagg_load_css_stylesheet($info['data'], FALSE, $aggregate_settings, $file_contents); + + // Get domain names and external assets in this css file. + $hosts = array(); + $urls = array(); + $matches = array(); + $pattern = '%url\(\s*+[\'"]?+(http:\/\/|https:\/\/|\/\/)([^\'"()\s]++)[\'"]?+\s*+\)%i'; + preg_match_all($pattern, $file_contents, $matches); + if (!empty($matches[1])) { + foreach ($matches[1] as $key => $match) { + $url = $match . $matches[2][$key]; + $parse = @parse_url($url); + if (!empty($parse['host'])) { + $extra = ''; + $ext = strtolower(pathinfo($url, PATHINFO_EXTENSION)); + $supported = array( + 'eot', + 'woff2', + 'woff', + 'ttf', + 'otf', + ); + if (in_array($ext, $supported)) { + $extra .= '#crossorigin'; + } + $hosts[$parse['host'] . $extra] = $match . $parse['host'] . '/' . $extra; + $urls[$url] = $url; + } + } + } + if (!empty($hosts) && (variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT) + )) { + $info['dns_prefetch'] = array_values($hosts); + } + + // Get local files to preload in this css file. + if (variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD)) { + $matches = array(); + $pattern = '/url\(\s*+[\'"]?(.*?)[\'"\s]?+\)/i'; + preg_match_all($pattern, $file_contents, $matches); + if (!empty($matches[1])) { + foreach ($matches[1] as $key => $match) { + $url = advagg_convert_abs_to_rel($match); + $parse = @parse_url($url); + if (empty($parse['host'])) { + $urls[$url] = $url; + } + } + } + if (!empty($urls)) { + $info['preload'] = array_values($urls); + } + } + } + unset($info); + } +} + +/** + * Implements hook_advagg_changed_files(). + * + * If the color module is enabled regenerate color module css when it changes. + * If a responsive file inside an adaptive theme has changed, regenerate it. + */ +function advagg_advagg_changed_files(array $files, array $types) { + // * @param array $files + // * List of files that have changed. + // * @param array $types + // * Array with the css and or the js key. + if (module_exists('locale')) { + _advagg_locale_changed_files($files, $types); + } + + // Keep track of what themes have been done. + static $themes_done; + if (!isset($themes_done)) { + $themes_done = array(); + } + + // Skip if no css changed. + if (empty($types['css'])) { + return; + } + + foreach ($files as $filename => $meta_data) { + // Only care about css files. + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if ($ext !== 'css') { + continue; + } + advagg_advagg_scan_for_changes($filename, TRUE); + } + + // Save error states and clear them. + $errors_before = drupal_static('form_set_error', array()); + form_clear_error(); + + // See if the css file is used the theme. + $themes = list_themes(); + $changed_files = array_keys($files); + + $submit_ran = FALSE; + foreach ($themes as $theme_name => $theme_values) { + $files_in_theme = array(); + foreach ($changed_files as $css_file) { + // Skip if we already did a form submit for this theme. + if (!empty($themes_done[$theme_name])) { + continue; + } + + // Skip if the file that was changed is not in this themes directory. + $theme_path = drupal_get_path('theme', $theme_name); + if ((!empty($theme_path)) && strpos($css_file, $theme_path) !== 0) { + continue; + } + $files_in_theme[] = $css_file; + } + + // Skip the theme if none of the changed files live in here. + if (empty($files_in_theme)) { + continue; + } + + // Get the form for this theme. + $router_item = menu_get_item('admin/appearance/settings/' . $theme_name); + if ($router_item['include_file']) { + require_once DRUPAL_ROOT . '/' . $router_item['include_file']; + } + $form = drupal_get_form('system_theme_settings', $theme_name); + // Get the form defaults. + $defaults = array(); + advagg_get_defaults_from_form($defaults, $form); + + $rebuild = FALSE; + if (isset($defaults['atcore_version_test'])) { + // Rebuild if the theme is an adaptive theme. + $rebuild = TRUE; + } + if (!$rebuild && module_exists('color')) { + foreach ($files_in_theme as $css_file) { + if (isset($form['color'])) { + // Rebuild if the file that was changed is a color module file. + foreach ($defaults['info']['css'] as $theme_file) { + if ($theme_path . '/' . $theme_file === $css_file) { + $rebuild = TRUE; + break 2; + } + } + } + } + } + + // Skip if themes css does not need to be generated dynamically. + if (empty($rebuild)) { + continue; + } + + // Build the palette value. + $palette = array(); + if (isset($form['color'])) { + foreach (element_children($form['color']['palette']) as $key) { + $palette[$key] = $form['color']['palette'][$key]['#value']; + } + } + + // Build the form state array. + $form_state = array( + 'values' => array('palette' => $palette), + 'build_info' => array('args' => array(0 => $theme_name)), + ); + $form_state['values'] += $defaults; + + if (isset($defaults['atcore_version_test'])) { + // Validate form. + at_core_settings_validate($form, $form_state); + $errors = form_set_error(); + if (empty($errors)) { + // Only submit if no errors. + at_core_settings_submit($form, $form_state); + $themes_done[$theme_name] = TRUE; + $submit_ran = TRUE; + } + } + elseif (isset($form['color'])) { + // Validate form. + color_scheme_form_validate($form, $form_state); + $errors = form_set_error(); + if (empty($errors)) { + // Only submit if no errors. + color_scheme_form_submit($form, $form_state); + $themes_done[$theme_name] = TRUE; + $submit_ran = TRUE; + } + } + // Reset form errors. + form_clear_error(); + } + // Save error states back. + $form_set_error = &drupal_static('form_set_error', array()); + $form_set_error = $errors_before; + + // Rescan again as the submit will generate new files in the css dir. + if ($submit_ran) { + advagg_push_new_changes(); + } +} + +/** + * Implements hook_advagg_scan_for_changes(). + * + * Used to see if the responsive files inside an adaptive theme has changed. + */ +function advagg_advagg_scan_for_changes($filename, $save_changes = FALSE) { + // Skip if this file is not a css file. + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if ($ext !== 'css') { + return FALSE; + } + + // Skip if the file is not in an adaptive theme. + $adaptivethemes = array(); + $themes = list_themes(); + foreach ($themes as $theme_name => $theme_values) { + $path = variable_get('theme_' . $theme_name . '_files_directory', ''); + if (!empty($path) && strpos($filename, $path) !== FALSE) { + $adaptivethemes[$theme_name] = $path; + } + } + if (empty($adaptivethemes)) { + return; + } + + $file_changed = array(); + foreach ($adaptivethemes as $theme_name => $path) { + // Set up some paths we use to get and save files. + $path_to_responsive_css = drupal_get_path('theme', $theme_name) . '/css/'; + $path_to_panels_css = drupal_get_path('theme', 'adaptivetheme') . '/layouts/css/'; + + // Map files to generated file names. + $file_map = array( + "$path/$theme_name.responsive.styles.css" => array( + $path_to_responsive_css . 'responsive.custom.css', + $path_to_responsive_css . 'responsive.smalltouch.portrait.css', + $path_to_responsive_css . 'responsive.smartphone.portrait.css', + $path_to_responsive_css . 'responsive.smalltouch.landscape.css', + $path_to_responsive_css . 'responsive.smartphone.landscape.css', + $path_to_responsive_css . 'responsive.tablet.portrait.css', + $path_to_responsive_css . 'responsive.tablet.landscape.css', + $path_to_responsive_css . 'responsive.desktop.css', + ), + "$path/$theme_name.lt-ie8.layout.css" => array( + $path_to_panels_css . 'ie_defaults.css', + ), + ); + if (!isset($file_map[$filename])) { + continue; + } + + // See if anything has changed. + $changes = advagg_detect_subfile_changes($filename, $file_map[$filename], 'adaptivetheme', $save_changes); + if (!empty($changes)) { + $file_changed[$path] = $changes; + } + } + + return $file_changed; +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * If the locale module is enabled regenerate locale translations. + * + * @param array $files + * List of files that have changed. + * @param array $types + * Array with the css and or the js key. + */ +function _advagg_locale_changed_files(array $files, array $types) { + // Skip if no js changed. + if (empty($types['js'])) { + return; + } + + $javascript = array(); + foreach ($files as $filename => $meta_data) { + // Only care about js files. + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if ($ext !== 'js') { + continue; + } + $javascript[] = array( + 'type' => 'file', + 'data' => $filename, + ); + } + if (!empty($javascript)) { + $javascript_before = $javascript; + $language_before = $GLOBALS['language']; + $language_list = language_list(); + foreach ($language_list as $lang) { + if ($lang->enabled) { + $GLOBALS['language'] = $lang; + $javascript = $javascript_before; + _advagg_locale_js_alter($javascript); + } + } + $GLOBALS['language'] = $language_before; + } +} + +/** + * Given a form get the default values from it. + * + * @param array $defaults + * An empty array used to populate the default values. + * @param array $form + * The form returned from drupal_get_form(). + * @param string $parent_key + * The key name of the parent. + */ +function advagg_get_defaults_from_form(array &$defaults, array $form, $parent_key = '') { + foreach (element_children($form) as $key) { + $values = $form[$key]; + if (isset($values['#value'])) { + // Grab defaults at this level. + if (!isset($defaults[$key])) { + $defaults[$key] = $values['#value']; + } + else { + $defaults[$parent_key . '-' . $key] = $values['#value']; + } + } + elseif (isset($values['#default_value'])) { + // Grab defaults at this level. + if (!isset($defaults[$key])) { + $defaults[$key] = $values['#default_value']; + } + else { + $defaults[$parent_key . '-' . $key] = $values['#default_value']; + } + } + elseif (is_array($values)) { + // Go deeper if needed. + advagg_get_defaults_from_form($defaults, $values, $key); + } + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.api.php b/frontend/drupal/sites/all/modules/advagg/advagg.api.php new file mode 100644 index 000000000..b0e68d0d9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.api.php @@ -0,0 +1,700 @@ + $data) { + if ($filename) { + // This is the filename. + } + $group = NULL; + $every_page = NULL; + foreach ($data['files'] as $fileinfo) { + // Grouped by group and every_page variables. + if (is_null($group)) { + $group = $fileinfo['group']; + } + if (is_null($every_page)) { + $every_page = $fileinfo['every_page']; + } + + // Bump Counter if group/every_page has changed from the last one. + if ($group != $fileinfo['group'] || $every_page != $fileinfo['every_page']) { + ++$counter; + $group = $fileinfo['group']; + $every_page = $fileinfo['every_page']; + $modified = TRUE; + } + $temp_new_files[$counter][] = $fileinfo; + } + ++$counter; + } + + // Replace $files array with new aggregate filenames. + $files = advagg_generate_filenames(array($temp_new_files), $type); +} + +/** + * Let other modules know about the changed files. + * + * @param array $files + * An associative array. + * filename - meta_data. + * @param array $types + * Array containing css and/or js. + * + * @return array + * Not used currently. + * + * @see advagg_push_new_changes() + * @see advagg_js_compress_advagg_changed_files() + */ +function hook_advagg_changed_files(array $files, array $types) { + // Only care about js files. + if (empty($types['js'])) { + return array(); + } + $return = array(); + foreach ($files as $filename => $meta_data) { + // Only care about js files. + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + if ($ext !== 'js') { + continue; + } + + $return[$filename] = advagg_js_compress_run_test($filename); + } + return $return; +} + +/** + * Allow other modules to add in their own settings and hooks. + * + * @param array $aggregate_settings + * An associative array of hooks and settings used. + * + * @see advagg_current_hooks_hash_array() + * @see advagg_js_compress_advagg_current_hooks_hash_array_alter() + */ +function hook_advagg_current_hooks_hash_array_alter(array &$aggregate_settings) { + $aggregate_settings['variables']['advagg_js_compressor'] = variable_get('advagg_js_compressor', ADVAGG_JS_COMPRESSOR); + $aggregate_settings['variables']['advagg_js_compress_packer'] = variable_get('advagg_js_compress_packer', ADVAGG_JS_COMPRESS_PACKER); + $aggregate_settings['variables']['advagg_js_compress_max_ratio'] = variable_get('advagg_js_compress_max_ratio', ADVAGG_JS_COMPRESS_MAX_RATIO); +} + +/** + * Allow other modules to alter the contents and add new files to save (.gz). + * + * @param array $files_to_save + * Array($uri => $contents). + * @param array $aggregate_settings + * Array of settings. + * @param array $other_parameters + * Array of containing $files and $type. + * + * @see advagg_save_aggregate() + * @see advagg_advagg_save_aggregate_alter() + */ +function hook_advagg_save_aggregate_alter(array &$files_to_save, array $aggregate_settings, array $other_parameters) { + // Return if gzip is disabled. + if (empty($aggregate_settings['variables']['advagg_gzip'])) { + return; + } + + // See if a .gz file already exists. + $gzip_exists = FALSE; + foreach ($files_to_save as $uri => $contents) { + // See if this uri contains .gz near the end of it. + $pos = strripos($uri, '.gz', 91 + strlen(ADVAGG_SPACE) * 3); + if (!empty($pos)) { + $len = strlen($uri); + // .gz file exists, exit loop. + if ($pos == $len - 3) { + $gzip_exists = TRUE; + break; + } + } + } + + // If a .gz file does not exist, create one. + if (!$gzip_exists) { + // Use the first file in the array. + $data = reset($files_to_save); + $uri = key($files_to_save); + // Compress it and add it to the $files_to_save array. + $compressed = gzencode($data, 9, FORCE_GZIP); + $files_to_save[$uri . '.gz'] = $compressed; + } +} + +/** + * Allow other modules to alter css and js paths. + * + * @param array $css_paths + * Array containing the local path and url path. + * @param array $js_paths + * Array containing the local path and url path. + * + * @see advagg_get_root_files_dir() + * @see advagg_mod_advagg_get_root_files_dir_alter() + */ +function hook_advagg_get_root_files_dir(array &$css_paths, array &$js_paths) { + $dir = rtrim(variable_get('advagg_mod_unified_multisite_dir', ''), '/'); + if (empty($dir) || !file_exists($dir) || !is_dir($dir)) { + return; + } + // Change directory. + $css_paths[0] = $dir . '/advagg_css'; + $js_paths[0] = $dir . '/advagg_js'; + + file_prepare_directory($css_paths[0], FILE_CREATE_DIRECTORY); + file_prepare_directory($js_paths[0], FILE_CREATE_DIRECTORY); + + // Set the URI of the directory. + $css_paths[1] = advagg_get_relative_path($css_paths[0]); + $js_paths[1] = advagg_get_relative_path($js_paths[0]); +} + +/** + * Allow other modules to modify this aggregates contents. + * + * @param string $data + * Raw CSS data. + * @param array $files + * List of files used to create this aggregate. + * @param array $aggregate_settings + * An associative array of hooks and settings used. + * + * @see advagg_get_css_aggregate_contents() + * @see advagg_css_compress_advagg_get_css_aggregate_contents_alter() + */ +function hook_advagg_get_css_aggregate_contents_alter(&$data, array $files, array $aggregate_settings) { + if (empty($aggregate_settings['variables']['advagg_css_compressor'])) { + return; + } + + if ($aggregate_settings['variables']['advagg_css_compressor'] == 2) { + advagg_css_compress_yui_cssmin($data); + } +} + +/** + * Allow other modules to modify this aggregates contents. + * + * @param string $data + * Raw JS data. + * @param array $files + * List of files used to create this aggregate. + * @param array $aggregate_settings + * An associative array of hooks and settings used. + * + * @see advagg_get_js_aggregate_contents() + * @see advagg_js_compress_advagg_get_js_aggregate_contents_alter() + */ +function hook_advagg_get_js_aggregate_contents_alter(&$data, array $files, array $aggregate_settings) { + // Do nothing if js file compression is disabled. + if (empty($aggregate_settings['variables']['advagg_js_compressor'])) { + return; + } + + // Compress it. + $filename = drupal_hash_base64(serialize($files)); + advagg_js_compress_prep($data, $filename, $aggregate_settings, FALSE); +} + +/** + * Allow other modules to modify this files contents. + * + * @param string $contents + * Raw file data. + * @param string $file + * Filename. + * @param array $aggregate_settings + * An associative array of hooks and settings used. + * + * @see advagg_get_css_aggregate_contents() + * @see advagg_css_compress_advagg_get_css_aggregate_contents_alter() + */ +function hook_advagg_get_css_file_contents_alter(&$contents, $file, array $aggregate_settings) { + if (empty($aggregate_settings['variables']['advagg_css_compressor'])) { + return; + } + + if ($aggregate_settings['variables']['advagg_css_compressor'] == 2) { + advagg_css_compress_yui_cssmin($contents); + } +} + +/** + * Allow other modules to modify this files contents. + * + * @param string $contents + * Raw file data. + * @param string $filename + * Filename. + * @param array $aggregate_settings + * An associative array of hooks and settings used. + * + * @see advagg_get_css_aggregate_contents() + * @see advagg_css_compress_advagg_get_css_aggregate_contents_alter() + */ +function hook_advagg_get_js_file_contents_alter(&$contents, $filename, array $aggregate_settings) { + // Do nothing if js file compression is disabled. + if (empty($aggregate_settings['variables']['advagg_js_compressor'])) { + return; + } + + // Make sure this file has been tested. + $compressor = $aggregate_settings['variables']['advagg_js_compressor']; + module_load_include('inc', 'advagg', 'advagg'); + $info = advagg_get_info_on_file($filename); + if (!isset($info['advagg_js_compress'][$compressor]['code'])) { + // Test file here on the spot. + $info = advagg_js_compress_run_test($filename); + } + + // Compress it if it passes the test. + if (!empty($info['advagg_js_compress'][$compressor]['code']) && $info['advagg_js_compress'][$compressor]['code'] == 1) { + advagg_js_compress_prep($contents, $filename, $aggregate_settings); + } +} + +/** + * Allow other modules to modify $css_groups right before it is processed. + * + * @param array $css_groups + * An associative array. + * key - group. + * @param bool $preprocess_css + * TRUE if preprocessing is enabled. + * + * @see _advagg_aggregate_css() + * @see advagg_css_cdn_advagg_css_groups_alter() + */ +function hook_advagg_css_groups_alter(array &$css_groups, $preprocess_css) { + // Work around a bug with seven_css_alter. + // http://drupal.org/node/1937860 + $theme_keys[] = $GLOBALS['theme']; + if (!empty($GLOBALS['base_theme_info'])) { + foreach ($GLOBALS['base_theme_info'] as $base) { + $theme_keys[] = $base->name; + } + } + $match = FALSE; + foreach ($theme_keys as $name) { + if ($name === 'seven') { + $match = TRUE; + } + } + if (empty($match)) { + return; + } + + $target = FALSE; + $last_group = FALSE; + $last_key = FALSE; + $kill_key = FALSE; + $replaced = FALSE; + foreach ($css_groups as $key => $group) { + if (empty($target)) { + if ($group['type'] === 'external' && $group['preprocess'] && $preprocess_css) { + foreach ($group['items'] as $k => $value) { + if ($value['data'] === 'themes/seven/jquery.ui.theme.css') { + // Type should be file and not external (core bug). + $value['type'] = 'file'; + $target = $value; + unset($css_groups[$key]['items'][$k]); + if (empty($css_groups[$key]['items'])) { + unset($css_groups[$key]); + $kill_key = $key; + } + } + } + } + } + else { + $diff = array_merge(array_diff_assoc($group['browsers'], $target['browsers']), array_diff_assoc($target['browsers'], $group['browsers'])); + if ($group['type'] != $target['type'] + || $group['group'] != $target['group'] + || $group['every_page'] != $target['every_page'] + || $group['media'] != $target['media'] + || $group['media'] != $target['media'] + || $group['preprocess'] != $target['preprocess'] + || !empty($diff) + ) { + if (!empty($last_group)) { + $diff = array_merge(array_diff_assoc($last_group['browsers'], $target['browsers']), array_diff_assoc($target['browsers'], $last_group['browsers'])); + if ($last_group['type'] != $target['type'] + || $last_group['group'] != $target['group'] + || $last_group['every_page'] != $target['every_page'] + || $last_group['media'] != $target['media'] + || $last_group['media'] != $target['media'] + || $last_group['preprocess'] != $target['preprocess'] + || !empty($diff) + ) { + // Insert New. + $css_groups[$kill_key] = array( + 'group' => $target['group'], + 'type' => $target['type'], + 'every_page' => $target['every_page'], + 'media' => $target['media'], + 'preprocess' => $target['preprocess'], + 'browsers' => $target['browsers'], + 'items' => array($target), + ); + $replaced = TRUE; + } + else { + // Insert above. + $css_groups[$last_key]['items'][] = $target; + $replaced = TRUE; + } + } + } + else { + // Insert below. + array_unshift($css_groups[$key]['items'], $target); + $replaced = TRUE; + } + } + $last_group = $group; + $last_key = $key; + if ($replaced) { + break; + } + } + ksort($css_groups); +} + +/** + * Allow other modules to modify $js_groups right before it is processed. + * + * @param array $js_groups + * An associative array. + * key - group. + * @param bool $preprocess_js + * TRUE if preprocessing is enabled. + * + * @see _advagg_aggregate_js() + * @see labjs_advagg_js_groups_alter() + */ +function hook_advagg_js_groups_alter(array &$js_groups, $preprocess_js) { + if (!$preprocess_js) { + return; + } + $labjs_location = labjs_get_path(); + + foreach ($js_groups as &$data) { + foreach ($data['items'] as &$values) { + if ($values['data'] == $labjs_location) { + // Strictly enforce preprocess = FALSE for labjs. + $values['preprocess'] = FALSE; + $data['preprocess'] = FALSE; + break 2; + } + } + unset($values); + } + unset($data); +} + +/** + * Allow other modules to modify $children and $elements before rendering. + * + * @param array $children + * An array of children elements. + * @param array $elements + * A render array containing: + * - #items: The CSS items as returned by drupal_add_css() and + * altered by drupal_get_css(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @see advagg_modify_css_pre_render() + * @see advagg_css_compress_advagg_modify_css_pre_render_alter() + */ +function hook_advagg_modify_css_pre_render_alter(array &$children, array &$elements) { + // Get variables. + $compressor = variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE); + + // Do nothing if the compressor is disabled. + if (empty($compressor)) { + return; + } + + // Do nothing if the page is not cacheable and inline compress if not + // cacheable is not checked. + if (!variable_get('advagg_css_compress_inline_if_not_cacheable', ADVAGG_CSS_COMPRESS_INLINE_IF_NOT_CACHEABLE) && !drupal_page_is_cacheable()) { + return; + } + + module_load_include('inc', 'advagg_css_compress', 'advagg_css_compress.advagg'); + if ($compressor == 2) { + // Compress any inline CSS with YUI. + foreach ($children as &$values) { + if (!empty($values['#value'])) { + advagg_css_compress_yui_cssmin($values['#value']); + } + } + unset($values); + } +} + +/** + * Allow other modules to modify $children and $elements before rendering. + * + * @param array $children + * An array of children elements. + * @param array $elements + * A render array containing: + * - #items: The JavaScript items as returned by drupal_add_js() and + * altered by drupal_get_js(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @see advagg_modify_js_pre_render() + * @see advagg_js_compress_advagg_modify_js_pre_render_alter() + */ +function hook_advagg_modify_js_pre_render_alter(array &$children, array &$elements) { + // Get variables. + $aggregate_settings['variables']['advagg_js_compressor'] = variable_get('advagg_js_compress_inline', ADVAGG_JS_COMPRESS_INLINE); + $aggregate_settings['variables']['advagg_js_compress_max_ratio'] = variable_get('advagg_js_compress_max_ratio', ADVAGG_JS_COMPRESS_MAX_RATIO); + + // Do nothing if the compressor is disabled. + if (empty($aggregate_settings['variables']['advagg_js_compressor'])) { + return; + } + + // Do nothing if the page is not cacheable and inline compress if not + // cacheable is not checked. + if (!variable_get('advagg_js_compress_inline_if_not_cacheable', ADVAGG_JS_COMPRESS_INLINE_IF_NOT_CACHEABLE) && !drupal_page_is_cacheable()) { + return; + } + + // Compress any inline JS. + module_load_include('inc', 'advagg_js_compress', 'advagg_js_compress.advagg'); + foreach ($children as &$values) { + if (!empty($values['#value'])) { + $contents = $values['#value']; + $filename = drupal_hash_base64($contents); + advagg_js_compress_prep($contents, $filename, $aggregate_settings, FALSE); + $values['#value'] = $contents; + } + } + unset($values); +} + +/** + * Allow other modules to swap important contextual information on generation. + * + * @param array $original + * Array of original settings. + * @param array $aggregate_settings + * Array of contextual settings. + * @param int $mode + * Use 0 to change context to what is inside of $aggregate_settings. + * Use 1 to change context back. + * + * @see advagg_context_switch() + * @see advagg_advagg_context_alter() + */ +function hook_advagg_context_alter(array &$original, array $aggregate_settings, $mode) { + if ($mode == 0) { + // Change context. + $original['base_root'] = $GLOBALS['base_root']; + $original['base_url'] = $GLOBALS['base_url']; + $original['base_path'] = $GLOBALS['base_path']; + $original['is_https'] = $GLOBALS['is_https']; + $GLOBALS['is_https'] = $aggregate_settings['variables']['is_https']; + if ($aggregate_settings['variables']['is_https']) { + $GLOBALS['base_root'] = str_replace('http://', 'https://', $GLOBALS['base_root']); + $GLOBALS['base_url'] = str_replace('http://', 'https://', $GLOBALS['base_url']); + } + else { + $GLOBALS['base_root'] = str_replace('https://', 'http://', $GLOBALS['base_root']); + $GLOBALS['base_url'] = str_replace('https://', 'http://', $GLOBALS['base_url']); + } + $GLOBALS['base_path'] = $aggregate_settings['variables']['base_path']; + } + elseif ($mode == 1) { + // Change context back. + if (isset($original['base_root'])) { + $GLOBALS['base_root'] = $original['base_root']; + } + if (isset($original['base_url'])) { + $GLOBALS['base_url'] = $original['base_url']; + } + if (isset($original['base_path'])) { + $GLOBALS['base_path'] = $original['base_path']; + } + if (isset($original['is_https'])) { + $GLOBALS['is_https'] = $original['is_https']; + } + } +} + +/** + * Let other modules know about the aggregate files that have been removed. + * + * @param array $kill_list + * An array of aggregate files that have been removed. + * + * @see advagg_delete_files_if_stale() + */ +function hook_advagg_removed_aggregates(array $kill_list) { + foreach ($kill_list as $uri) { + if ($uri) { + // This is the uri. + } + // Do something else. + } +} + +/** + * Let other modules tell advagg that a file has changed. + * + * Useful for things like embedded images in CSS; generating a new aggregate + * when the image in the CSS file has changed. + * + * @param string $filename + * Name of the root CSS or JavaScript file. + * + * @return bool + * Set to TRUE to trigger a rebuild of the aggregates that contain this file. + * + * @see advagg_scan_for_changes() + * @see css_emimage_advagg_scan_for_changes() + */ +function hook_advagg_scan_for_changes($filename) { + if ($filename) { + return FALSE; + } +} + +/** + * Let other modules add/alter additional information about files passed in. + * + * @param array $return + * An associative array; filename -> data. + * @param array $cached_data + * What data was found in the cache; cache_id -> data. + * @param bool $bypass_cache + * If TRUE the loaded data did not come from the cache. + * + * @see advagg_get_info_on_files() + * @see advagg_advagg_get_info_on_files_alter() + */ +function hook_advagg_get_info_on_files_alter(array &$return, array $cached_data, $bypass_cache) { + if (!variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER)) { + return; + } + $limit_value = variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE); + list($css_path, $js_path) = advagg_get_root_files_dir(); + if ($js_path) { + // This is the js_path array. + } + $parts_path = $css_path[1] . '/parts'; + + foreach ($return as $filename => &$info) { + if ($filename) { + // This is the filename. + } + if (empty($info['fileext']) || $info['fileext'] !== 'css') { + continue; + } + + // Break large file into multiple small files. + if ($info['linecount'] > $limit_value) { + advagg_split_css_file($info); + } + elseif (strpos($info['data'], $parts_path) === 0) { + $info['split'] = TRUE; + } + } + unset($info); +} + +/** + * Tell advagg about other hooks related to advagg. + * + * @param array $hooks + * Array of hooks related to advagg. + * @param bool $all + * If FALSE get only the subset of hooks that alter the filename/contents. + * + * @see advagg_hooks_implemented() + * @see advagg_bundler_advagg_hooks_implemented_alter() + */ +function hook_advagg_hooks_implemented_alter(array &$hooks, $all) { + if ($all) { + $hooks['advagg_bundler_analysis_alter'] = array(); + } +} + +/** + * Let other modules modify the analysis array before it is used. + * + * @param array $analysis + * An associative array; filename -> data. + * + * @see advagg_bundler_analysis() + */ +function hook_advagg_bundler_analysis_alter(array &$analysis) { + foreach ($analysis as $filename => &$data) { + if ($filename) { + // This is the filename. + } + + // This changes often; 604800 is 1 week. + if ($data['changes'] > 10 && $data['mtime'] >= REQUEST_TIME - 604800) { + // Modify the group hash so this doesn't end up in a big aggregate. + $data['group_hash']; + } + } + unset($data); +} + +/** + * @} End of "defgroup advagg_hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.cache.inc b/frontend/drupal/sites/all/modules/advagg/advagg.cache.inc new file mode 100644 index 000000000..8d1d1586c --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.cache.inc @@ -0,0 +1,945 @@ +fields('af') + ->execute(); + if (!empty($result)) { + module_load_include('inc', 'advagg', 'advagg'); + $filenames = array(); + + $data = array(); + foreach ($result as $row) { + $filenames[] = $row->filename; + $data[$row->filename] = (array) $row; + } + + // Get filesystem data. + $files_info = advagg_get_info_on_files($filenames, TRUE); + foreach ($files_info as $info) { + if (!isset($data[$info['data']])) { + continue; + } + $row = $data[$info['data']]; + + // Select the keys to compare. + $keys_to_compare = array( + 'filesize', + 'content_hash', + 'linecount', + ); + $changed = array(); + foreach ($keys_to_compare as $key) { + if ($row[$key] != $info[$key]) { + $changed[] = $key . ' db:' . $row[$key] . ' file:' . $info[$key]; + break; + } + } + // Compare mtime if it is not zero. + if (empty($info['split']) && !empty($info['mtime'])) { + if (variable_get('advagg_strict_mtime_check', ADVAGG_STRICT_MTIME_CHECK) && $row['mtime'] != $info['mtime']) { + $changed[] = 'mtime db:' . $row['mtime'] . ' file:' . $info['mtime']; + } + elseif ($row['mtime'] < $info['mtime']) { + $changed[] = 'mtime db:' . $row['mtime'] . ' file:' . $info['mtime']; + } + } + + if (empty($changed)) { + // Call hook_advagg_scan_for_changes(). + $changes_array = module_invoke_all('advagg_scan_for_changes', $row['filename']); + if (is_array($changes_array)) { + foreach ($changes_array as $value) { + if (!empty($value)) { + $changed[] = $value; + break; + } + } + } + } + + // If file has changed, add it to the array. + if (!empty($changed)) { + $info['changes'] = $changed; + $files_that_have_changed[$row['filename']] = $info; + } + } + } + + return $files_that_have_changed; +} + +/** + * Flush the correct caches so CSS/JS changes go live. + * + * @return array + * Array of files that have changed and caches flushed. + */ +function advagg_push_new_changes(array $files = array()) { + $results = array(); + // Scan the file system for changes to CSS/JS files. + if (empty($files)) { + $files = advagg_scan_for_changes(); + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Changes detected in
@files
.', array('@files' => print_r($files, TRUE)), WATCHDOG_DEBUG); + } + } + + // Clear some static caches. + drupal_static_reset('advagg_get_info_on_file'); + drupal_static_reset('advagg_drupal_hash_base64'); + drupal_static_reset('advagg_current_hooks_hash_array'); + drupal_static_reset('advagg_get_current_hooks_hash'); + + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + // Exception used to get a compact stack trace. + $e = new Exception(); + watchdog('advagg-debug', 'New changes called by:
@changes
', array('@changes' => print_r($e->getTraceAsString(), TRUE)), WATCHDOG_DEBUG); + } + + // If something changed, flush the correct caches so that change goes out. + if (!empty($files)) { + $types = array(); + module_load_include('inc', 'advagg', 'advagg'); + foreach ($files as $filename => $meta_data) { + // Lookup the aggregates/cache ids that use this file. + $cache_ids = advagg_get_aggregates_using_file($meta_data['filename_hash'], TRUE); + $cache_hits = array(); + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $types[$ext] = TRUE; + + if (!empty($cache_ids)) { + $cache_hits = cache_get_multiple($cache_ids, 'cache_advagg_info'); + foreach ($cache_hits as $cid => $data) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Clearing cache @cid.', array('@cid' => $cid), WATCHDOG_DEBUG); + } + cache_clear_all($cid, 'cache_advagg_info', FALSE); + } + } + + $changes = array(); + if (!empty($meta_data['changes'])) { + $changes = $meta_data['changes']; + unset($meta_data['changes']); + } + + $results[$filename] = array( + count($cache_ids), + count($cache_hits), + $changes, + ); + + // Update database. + advagg_insert_update_files(array($filename => $meta_data), $ext); + } + + // Change query-strings on css/js files to enforce reload for all users. + // Change css_js_query_string variable. + _drupal_flush_css_js(); + + // Let other modules know about the changed files. + // Call hook_advagg_changed_files(). + module_invoke_all('advagg_changed_files', $files, $types); + + // Clear out the full aggregates cache. + foreach ($types as $ext => $bool) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Clearing cache advagg:@ext: in cache_advagg_aggregates.', array('@ext' => print_r($ext, TRUE)), WATCHDOG_DEBUG); + } + cache_clear_all('advagg:' . $ext . ':', 'cache_advagg_aggregates', TRUE); + } + } + // Return what was done. + return $results; +} + +/** + * Given a filename hash get back all aggregates that include it. + * + * @param string $filename_hash + * Hash of the filename. + * @param bool $cid_only + * Set to TRUE to only have cache ids returned. + * + * @return array + * Array of aggregates that use this file. + */ +function advagg_get_aggregates_using_file($filename_hash, $cid_only = FALSE) { + // Create main query for the advagg_aggregates table. + $query = db_select('advagg_aggregates', 'aa') + ->condition('aa.filename_hash', $filename_hash); + // Create join query for the advagg_aggregates_versions table. + $query->join('advagg_aggregates_versions', 'aav', 'aa.aggregate_filenames_hash = aav.aggregate_filenames_hash AND aav.atime > 0'); + $query = $query->fields('aav', array('aggregate_filenames_hash', 'aggregate_contents_hash')); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + + // Put results into $aggregates array. + $aggregates = array(); + foreach ($results as $row) { + $row = (array) $row; + $cid = 'advagg:db:' . $row['aggregate_filenames_hash'] . ADVAGG_SPACE . $row['aggregate_contents_hash']; + if ($cid_only) { + $aggregates[] = $cid; + } + else { + $row['cid'] = $cid; + $aggregates[] = $row; + } + } + return $aggregates; +} + +/** + * Get all CSS/JS advagg files. + * + * @param array $options + * Array of options to pass along to file_scan_directory(). + * + * @return array + * Array of css and js files. + */ +function advagg_get_all_files(array $options = array()) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $options += array('nomask' => '/(\.\.?|CVS|\.gz|\.br)$/'); + + // Get a list of files. + $css_files = file_scan_directory($css_path[0], '/.*/', $options); + $js_files = file_scan_directory($js_path[0], '/.*/', $options); + return array($css_files, $js_files); +} + +/** + * Scan CSS/JS advagg dir and remove that file if atime is grater than 30 days. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_stale_aggregates() { + list($css_files, $js_files) = advagg_get_all_files(); + + // Make the advagg_get_hashes_from_filename() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + $css_files = advagg_delete_files_if_stale($css_files); + $js_files = advagg_delete_files_if_stale($js_files); + return array($css_files, $js_files); +} + +/** + * Given an array of files remove that file if atime is grater than 30 days. + * + * @param array $files + * Array of files returned by file_scan_directory. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_files_if_stale(array $files) { + // Array used to record what files were deleted. + $kill_list = array(); + + foreach ($files as $uri => $file) { + // Get info on file. + $filename = $file->filename; + $data = advagg_get_hashes_from_filename($filename); + if (is_array($data)) { + list(, $aggregate_filenames_hash, $aggregate_contents_hash) = $data; + } + else { + // Can not get data on file, remove it. + $kill_list[] = advagg_delete_file_by_uri($uri); + continue; + } + + // Get atime of file. + $atime = advagg_get_atime($aggregate_filenames_hash, $aggregate_contents_hash, $uri); + if (empty($atime)) { + $kill_list[] = advagg_delete_file_by_uri($uri); + continue; + } + + // Default stale file threshold is 30 days. + if (REQUEST_TIME - $atime > variable_get('drupal_stale_file_threshold', 2592000)) { + $kill_list[] = advagg_delete_file_by_uri($uri); + continue; + } + + } + // Let other modules know about the removed files. + // Call hook_advagg_removed_aggregates(). + module_invoke_all('advagg_removed_aggregates', $kill_list); + return $kill_list; +} + +/** + * Scan CSS/JS advagg dir and remove that file if it is empty. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_empty_aggregates() { + list($css_files, $js_files) = advagg_get_all_files(); + $css_files = advagg_delete_files_if_empty($css_files); + $js_files = advagg_delete_files_if_empty($js_files); + return array($css_files, $js_files); +} + +/** + * Given an array of files remove that file if it is empty. + * + * @param array $files + * Array of files returned by file_scan_directory. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_files_if_empty(array $files) { + // Array used to record what files were deleted. + $kill_list = array(); + + foreach ($files as $uri => $file) { + // Ignore temp files. There's a separate process for cleaning those up. + if (strpos($uri, '/advagg_file_') !== FALSE) { + continue; + } + $size = filesize($uri); + if ($size === 0) { + $kill_list[] = advagg_delete_file_by_uri($uri); + continue; + } + } + // Let other modules know about the removed files. + // Call hook_advagg_removed_aggregates(). + module_invoke_all('advagg_removed_aggregates', $kill_list); + return $kill_list; +} + +/** + * Delete a file, and any compressed versions. + * + * @param string $uri + * URI of the file to delete. + * + * @return string + * The given URI. + */ +function advagg_delete_file_by_uri($uri) { + if (file_exists($uri)) { + file_unmanaged_delete($uri); + } + if (file_exists($uri . '.gz')) { + file_unmanaged_delete($uri . '.gz'); + } + if (file_exists($uri . '.br')) { + file_unmanaged_delete($uri . '.br'); + } + return $uri; +} + +/** + * Perform a cache_clear_all on all bins returned by advagg_flush_caches(TRUE). + * + * @param bool $push_new_changes + * FALSE: Do not scan for changes. + */ +function advagg_flush_all_cache_bins($push_new_changes = TRUE) { + $bins = advagg_flush_caches(TRUE, $push_new_changes); + foreach ($bins as $bin) { + cache_clear_all('*', $bin, TRUE); + } +} + +/** + * Remove all files from the advagg CSS/JS directories. + * + * @param bool $kill_htaccess + * Set to TRUE to remove the htaccess files as well. + * + * @return array + * Array of all files removed. + */ +function advagg_remove_all_aggregated_files($kill_htaccess = FALSE) { + $options = array( + 'callback' => 'file_unmanaged_delete', + 'nomask' => '/(\.\.?|CVS)$/', + ); + list($css_files, $js_files) = advagg_get_all_files($options); + // Let other modules know about the removed files. + // Call hook_advagg_removed_aggregates(). + module_invoke_all('advagg_removed_aggregates', $css_files); + module_invoke_all('advagg_removed_aggregates', $js_files); + + // Remove the htaccess files as well. + if ($kill_htaccess) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + if (file_exists($css_path[0] . '/.htaccess')) { + file_unmanaged_delete($css_path[0] . '/.htaccess'); + $css_files[] = $css_path[0] . '/.htaccess'; + } + if (file_exists($js_path[0] . '/.htaccess')) { + file_unmanaged_delete($js_path[0] . '/.htaccess'); + $js_files[] = $js_path[0] . '/.htaccess'; + } + } + return array($css_files, $js_files); +} + +/** + * Increment the advagg_global_counter variable by one. + * + * @todo Allow this value to be kept in sync across a multisite. + * + * @return int + * New value of advagg_global_counter. + */ +function advagg_increment_global_counter() { + $new_value = advagg_get_global_counter() + 1; + variable_set('advagg_global_counter', $new_value); + return $new_value; +} + +/** + * Scan for missing files and remove the associated entries in the database. + * + * @return array + * Array of what files were cleared out of the database. + */ +function advagg_remove_missing_files_from_db() { + $missing_files = array(); + $deleted = array(); + + // Get all files stored in the database. + $result = db_select('advagg_files', 'af') + ->fields('af') + ->execute(); + if (empty($result)) { + return $deleted; + } + + // Find missing files. + module_load_include('inc', 'advagg', 'advagg'); + foreach ($result as $row) { + $row = (array) $row; + $info = advagg_get_info_on_file($row['filename'], TRUE); + + // Make sure file exists. + if (empty($info['content_hash'])) { + $info += advagg_get_aggregates_using_file($info['filename_hash']); + $missing_files[$row['filename']] = $info; + continue; + } + } + if (empty($missing_files)) { + return $deleted; + } + + // Remove missing file database entries. + $types = array(); + foreach ($missing_files as $filename => $data) { + // Setup this run. + $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $advagg_files_del = 0; + $advagg_aggregates_del = 0; + $advagg_aggregates_versions_del = 0; + $clean_sweep = TRUE; + $filename_hash = ''; + + // Scan the data. + foreach ($data as $key => $values) { + if (!is_numeric($key)) { + $filename_hash = $values; + } + else { + // Remove the entry from the database if this aggregate has not been + // accessed in the last 2 weeks. + $can_delete = db_delete('advagg_aggregates_versions') + ->condition('aggregate_filenames_hash', $values['aggregate_filenames_hash']) + ->condition('atime', REQUEST_TIME - variable_get('advagg_remove_missing_files_from_db_time', ADVAGG_REMOVE_MISSING_FILES_FROM_DB_TIME), '<') + ->execute(); + + if ($can_delete > 0) { + $advagg_aggregates_versions_del += $can_delete; + $advagg_aggregates_del += db_delete('advagg_aggregates') + ->condition('aggregate_filenames_hash', $values['aggregate_filenames_hash']) + ->execute(); + } + else { + $clean_sweep = FALSE; + } + // Clear the cache. + cache_clear_all($values['cid'], 'cache_advagg_info', FALSE); + } + } + + // Remove the file entry if all aggregates referencing it have been removed. + if ($clean_sweep) { + $advagg_files_del += db_delete('advagg_files') + ->condition('filename_hash', $filename_hash) + ->execute(); + + // Add info to array. + if (!empty($advagg_files_del) + || !empty($advagg_aggregates_versions_del) + || !empty($advagg_aggregates_del) + ) { + $types[$ext] = TRUE; + $deleted[$filename] = array( + 'advagg_files' => $advagg_files_del, + 'advagg_aggregates_versions' => $advagg_aggregates_versions_del, + 'advagg_aggregates' => $advagg_aggregates_del, + ); + } + } + } + + // If something was deleted, clear the full aggregates cache. + if (!empty($deleted)) { + foreach ($types as $ext => $bool) { + cache_clear_all('advagg:' . $ext . ':', 'cache_advagg_aggregates', TRUE); + } + } + + // Return what was deleted. + return $deleted; +} + +/** + * Scan CSS/JS advagg dir and remove file if there is no associated db record. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_orphaned_aggregates() { + list($css_files, $js_files) = advagg_get_all_files(); + + // Make the advagg_get_hashes_from_filename() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + $css_files = advagg_delete_files_if_orphaned($css_files); + $js_files = advagg_delete_files_if_orphaned($js_files); + return array($css_files, $js_files); +} + +/** + * Given an array of files remove that file if there is no associated db record. + * + * @param array $files + * Array of files returned by file_scan_directory. + * + * @return array + * Array of files that got removed. + */ +function advagg_delete_files_if_orphaned(array $files) { + // Get the uri for the advagg_css/parts directory. + list($css_path) = advagg_get_root_files_dir(); + $parts_uri = $css_path[0] . '/parts/'; + + // Array used to record what files were deleted. + $kill_list = $keyed_file_list = array(); + + // Create a listing of all file names and associated hashes. + foreach ($files as $uri => $file) { + // Get info on file. + $data = advagg_get_hashes_from_filename($file->filename, TRUE); + if (is_array($data)) { + list(, $aggregate_filenames_hash) = $data; + // Check to see if the file is in the database. + $keyed_file_list[$aggregate_filenames_hash] = $uri; + } + else { + // Check to see if this is a parts css file. + $start = strpos($file->uri, $parts_uri); + if ($start !== FALSE) { + // Get the original filename. + $original_file = substr($file->uri, $start + strlen($parts_uri)); + $original_file = preg_replace('/(.\\d+\\.css)$/i', '.css', $original_file); + if (file_exists($original_file)) { + // Original file exists, do not delete. + continue; + } + } + + // Can not get data on file, remove it. + $kill_list[] = $uri; + continue; + } + } + + if (!empty($keyed_file_list)) { + $filenames_hash = array_keys($keyed_file_list); + $aggregates_in_database = array(); + // Process in chunks when a large array is passed. + do { + // Check if the aggregate_filenames_hash exists in the database. + $aggregates_in_database += db_select('advagg_aggregates_versions', 'av') + ->fields('av', array('aggregate_filenames_hash')) + ->condition('av.aggregate_filenames_hash', array_splice($filenames_hash, 0, 1000), 'IN') + ->distinct() + ->execute() + ->fetchAllAssoc('aggregate_filenames_hash'); + } while (count($filenames_hash)); + + // Get values not found in the database. + $to_delete = array_values(array_diff_key($keyed_file_list, $aggregates_in_database)); + // Add the file uri to the kill list. + $kill_list = array_merge($kill_list, $to_delete); + } + + if (!empty($kill_list)) { + foreach ($kill_list as $uri) { + advagg_delete_file_by_uri($uri); + } + } + + // Let other modules know about the removed files. + // Call hook_advagg_removed_aggregates(). + module_invoke_all('advagg_removed_aggregates', $kill_list); + return $kill_list; +} + +/** + * Delete aggregates that have not been accessed in the last 6 weeks. + * + * @return int + * Count of the number of rows removed from the databases. + */ +function advagg_remove_old_unused_aggregates() { + $advagg_aggregates_versions_del = 0; + $advagg_aggregates_del = 0; + + // Find orphaned aggregate versions entries. + // Create main query. + $query = db_select('advagg_aggregates_versions', 'aav') + ->fields('aav', array('aggregate_filenames_hash')) + ->groupBy('aav.aggregate_filenames_hash'); + // Create join and add in query comment. + $query->leftjoin('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash=aav.aggregate_filenames_hash'); + $query->isNull('aa.aggregate_filenames_hash'); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + // If we have an orphaned db entry, delete it. + if (!empty($results)) { + foreach ($results as $row) { + $advagg_aggregates_versions_del += db_delete('advagg_aggregates_versions') + ->condition('aggregate_filenames_hash', $row->aggregate_filenames_hash) + ->execute(); + } + } + + // Delete aggregate versions that have not been accessed in the last 45 days. + $advagg_aggregates_versions_del += db_delete('advagg_aggregates_versions') + ->condition('atime', REQUEST_TIME - variable_get('advagg_remove_old_unused_aggregates_time', ADVAGG_REMOVE_OLD_UNUSED_AGGREGATES_TIME), '<') + ->execute(); + + // See if any aggregates are orphaned now. + // Create main query. + $query = db_select('advagg_aggregates', 'aa') + ->fields('aa', array('aggregate_filenames_hash')) + ->groupBy('aa.aggregate_filenames_hash'); + // Create join and add in query comment. + $query->leftjoin('advagg_aggregates_versions', 'aav', 'aa.aggregate_filenames_hash=aav.aggregate_filenames_hash'); + $query->isNull('aav.aggregate_filenames_hash'); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + + // If we have an orphaned db entry, delete it. + if (!empty($results)) { + foreach ($results as $row) { + $advagg_aggregates_del += db_delete('advagg_aggregates') + ->condition('aggregate_filenames_hash', $row->aggregate_filenames_hash) + ->execute(); + } + } + + // Return the total count of entires removed from the database. + return $advagg_aggregates_versions_del + $advagg_aggregates_del; +} + +/** + * Delete orphaned/expired advagg locks from the semaphore database table. + * + * @return int + * Count of the number of rows removed from the databases. + */ +function advagg_cleanup_semaphore_table() { + // Let expiration times vary by 5 minutes. + $fuzz_factor = 300; + $results = db_delete('semaphore') + ->condition('name', db_like('advagg_') . '%', 'LIKE') + ->condition('expire', REQUEST_TIME - $fuzz_factor, '<') + ->execute(); + return $results; +} + +/** + * Delete leftover temp files. + * + * @return int + * Count of the number of files removed + */ +function advagg_remove_temp_files() { + // Make sure advagg_get_root_files_dir() is available. + drupal_load('module', 'advagg'); + // Make sure advagg_install_delete_empty_file_if_stale() is available. + module_load_include('install', 'advagg', 'advagg'); + + // Get the advagg paths. + $advagg_path = advagg_get_root_files_dir(); + $total_count = 0; + // Get the top level path. + $top_level = substr($advagg_path[0][0], 0, strpos($advagg_path[0][0], 'advagg_css')); + + // Remove empty temp files from public://. + $files = file_scan_directory($top_level, '/file.*|fil.*\.tmp/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_install_delete_empty_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_css. + $files = file_scan_directory($advagg_path[0][0], '/file.*|fil.*\.tmp/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_install_delete_empty_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_js. + $files = file_scan_directory($advagg_path[1][0], '/file.*|fil.*\.tmp/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_install_delete_empty_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://. + $files = file_scan_directory($top_level, '/file_advagg_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_css. + $files = file_scan_directory($advagg_path[0][0], '/file_advagg_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_js. + $files = file_scan_directory($advagg_path[1][0], '/file_advagg_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://. + $files = file_scan_directory($top_level, '/advagg_file_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_css. + $files = file_scan_directory($advagg_path[0][0], '/advagg_file_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Remove empty temp files from public://advagg_js. + $files = file_scan_directory($advagg_path[1][0], '/advagg_file_.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_delete_temp_file_if_stale', + )); + foreach ($files as $key => $file) { + if (file_exists($file->uri)) { + unset($files[$key]); + } + } + $total_count += count($files); + + // Output info. + return $total_count; +} + +/** + * Refresh all locale files. + * + * @return int + * Count of the number of files removed + */ +function advagg_refresh_all_locale_files() { + $locale_files = array(); + if (!module_exists('locale')) { + return $locale_files; + } + + $results = db_select('advagg_files', 'af') + ->fields('af') + ->condition('af.filetype', 'js') + ->condition('af.filesize', 0, '>') + ->execute(); + $javascript = array(); + foreach ($results as $row) { + $javascript[] = array( + 'type' => 'file', + 'data' => $row->filename, + ); + } + + if (!empty($javascript)) { + $javascript_before = $javascript; + $language_before = $GLOBALS['language']; + $language_list = language_list(); + foreach ($language_list as $lang) { + if ($lang->enabled) { + $GLOBALS['language'] = $lang; + $javascript = $javascript_before; + locale_js_alter($javascript); + $locale_file = array_diff_key($javascript, $javascript_before); + $locale_files += $locale_file; + } + } + $GLOBALS['language'] = $language_before; + } + return $locale_files; +} + +/** + * Callback to delete files if modified more than 60 seconds ago. + * + * @param string $uri + * Location of the file to check. + */ +function advagg_delete_temp_file_if_stale($uri) { + // Set stale file threshold to 60 seconds. + if (REQUEST_TIME - filemtime($uri) > 60) { + file_unmanaged_delete($uri); + } +} + +/** + * See if any of the subfiles has changed. + * + * @param string $filename + * Name of the file that is related to the subfiles. + * @param array $subfiles + * An array of files to check for changes. + * @param string $keyname + * Under what key to save the info on the files. + * @param bool $save_changes + * If TRUE then the changes will be updated in the cache. + * + * @return bool + * TRUE if one of the subfiles has changed. + */ +function advagg_detect_subfile_changes($filename, array $subfiles, $keyname, $save_changes = FALSE) { + // Get the info on this file from the cache. + module_load_include('inc', 'advagg', 'advagg'); + $info = advagg_get_info_on_file($filename); + $hash_id = 'advagg:subfiles:' . $keyname . ':' . $info['filename_hash']; + if (!isset($info[$keyname])) { + // Pull up the info from the database if missing from the cache. + $info[$keyname] = advagg_get_hash_settings($hash_id); + } + + $subfile_changed = array(); + // Check every subfile seeing if they have changed. + foreach ($subfiles as $subfile) { + $current_file_info = $defaults = array( + 'hash' => '', + 'size' => 0, + 'mtime' => 0, + ); + + // Get the currently saved info on this file. + $saved_file_info = isset($info[$keyname][$subfile]) ? $info[$keyname][$subfile] : array(); + $saved_file_info += $defaults; + + // Get the current info on the file. + if (file_exists($subfile)) { + $current_file_info = array( + 'hash' => drupal_hash_base64((string) @advagg_file_get_contents($subfile)), + 'size' => filesize($subfile), + 'mtime' => filemtime($subfile), + ); + } + + // Set the info in case a save happens. + $info[$keyname][$subfile] = $current_file_info; + + // Check for any differences. + $diff = array_diff_assoc($saved_file_info, $current_file_info); + if (!empty($diff)) { + $subfile_changed[$subfile] = $diff; + } + } + if (!empty($subfile_changed) && $save_changes) { + $cache_id = 'advagg:file:' . $info['filename_hash']; + + // Set static cache. + $filename_hashes = &drupal_static('advagg_get_info_on_file'); + $filename_hashes[$cache_id] = $info; + + // Set drupal cache. + cache_set($cache_id, $info, 'cache_advagg_info', CACHE_PERMANENT); + + // Save to database. + advagg_set_hash_settings($hash_id, $info[$keyname]); + } + return $subfile_changed; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.developer-documentation.php b/frontend/drupal/sites/all/modules/advagg/advagg.developer-documentation.php new file mode 100644 index 000000000..f49d8ac0f --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.developer-documentation.php @@ -0,0 +1,41 @@ + advagg_get_global_counter())); + } +} + +/** + * Implements hook_drush_command(). + */ +function advagg_drush_command() { + $items = array(); + $items['advagg-cron'] = array( + 'description' => dt('Run the advagg cron hook.'), + 'examples' => array( + 'Standard example' => 'drush advagg-cron', + ), + 'aliases' => array('advagg-c'), + ); + $items['advagg-clear-db-cache'] = array( + 'description' => dt('Remove all entries from the advagg cache bins.'), + 'examples' => array( + 'Standard example' => 'drush advagg-clear-db-cache', + ), + 'aliases' => array('advagg-cdc'), + ); + $items['advagg-clear-all-files'] = array( + 'description' => dt('Remove all generated files.'), + 'examples' => array( + 'Standard example' => 'drush advagg-clear-all-files', + ), + 'aliases' => array('advagg-caf'), + ); + $items['advagg-force-new-aggregates'] = array( + 'description' => dt('Force the creation of all new aggregates by incrementing a global counter.'), + 'examples' => array( + 'Standard example' => 'drush advagg-force-new-aggregates', + ), + 'aliases' => array('advagg-fna'), + ); + return $items; +} + +/** + * @} End of "addtogroup 3rd_party_hooks". + */ + +/** + * Callback function for drush advagg-force-new-aggregates. + * + * Callback is called by using drush_hook_command() where + * hook is the name of the module (advagg) and command is the name of + * the Drush command with all "-" characters converted to "_" characters. + */ +function drush_advagg_force_new_aggregates() { + // Clear out the cache. + drush_advagg_clear_db_cache(); + + // Increment counter. + module_load_include('inc', 'advagg', 'advagg.cache'); + $new_value = advagg_increment_global_counter(); + drush_log(dt('Global counter is now set to @new_value', array('@new_value' => $new_value)), 'ok'); +} + +/** + * Callback function for drush advagg-clear-all-files. + * + * Callback is called by using drush_hook_command() where + * hook is the name of the module (advagg) and command is the name of + * the Drush command with all "-" characters converted to "_" characters. + */ +function drush_advagg_clear_all_files() { + // Clear out the cache. + drush_advagg_clear_db_cache(); + + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + list($css_files, $js_files) = advagg_remove_all_aggregated_files(); + + // Report back the results. + drush_log(dt('All AdvAgg files have been deleted. @css_count CSS files and @js_count JS files have been removed.', array( + '@css_count' => count($css_files), + '@js_count' => count($js_files), + )), 'ok'); +} + +/** + * Callback function for drush advagg-clear-db-cache. + * + * Callback is called by using drush_hook_command() where + * hook is the name of the module (advagg) and command is the name of + * the Drush command with all "-" characters converted to "_" characters. + */ +function drush_advagg_clear_db_cache() { + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + advagg_flush_all_cache_bins(); + + // Report back the results. + drush_log(dt('All AdvAgg cache bins have been cleared.'), 'ok'); +} + +/** + * Callback function for drush advagg-cron. + * + * Callback is called by using drush_hook_command() where + * hook is the name of the module (advagg) and command is the name of + * the Drush command with all "-" characters converted to "_" characters. + */ +function drush_advagg_cron() { + // Run AdvAgg cron job. + $output = advagg_cron(TRUE); + + // Output results from running advagg_delete_stale_aggregates(). + list($css_files, $js_files) = $output[0]; + if (count($css_files) > 0 || count($js_files) > 0) { + drush_log(dt('All stale aggregates have been deleted. @css_count CSS files and @js_count JS files have been removed.', array( + '@css_count' => count($css_files), + '@js_count' => count($js_files), + )), 'ok'); + } + else { + drush_log(dt('No stale aggregates found. Nothing was deleted.'), 'ok'); + } + + // Output results from running advagg_delete_orphaned_aggregates(). + if (empty($output[1][0]) && empty($output[1][1])) { + drush_log(dt('All files have an associated db record; nothing was deleted.'), 'ok'); + } + else { + drush_log(dt('Some files had no associated db record and could be safely deleted from the file system. @raw', array('@raw' => print_r($output[1], TRUE))), 'ok'); + } + + // Output results from running advagg_remove_missing_files_from_db(). + if (empty($output[2])) { + drupal_set_message(dt('All source files where found, no database entries where pruned.'), 'ok'); + } + else { + // format_plural() not always available. + drupal_set_message(dt('Some source files are missing and as a result some unused aggregates were found. A total of @count database entries were removed.', array('@count' => count($output[2]))), 'ok'); + } + + // Output results from running advagg_remove_old_unused_aggregates(). + if (empty($output[3])) { + drupal_set_message(dt('No old and unused aggregates found. Nothing was deleted.'), 'ok'); + } + else { + // format_plural() not always available. + drupal_set_message(dt('Some old and unused aggregates were found. A total of @count database entries were removed.', array('@count' => $output[3])), 'ok'); + } + + // Output results from running advagg_cleanup_semaphore_table(). + if (empty($output[4])) { + drupal_set_message(dt('No old semaphore locks found.'), 'ok'); + } + else { + // format_plural() not always available. + drupal_set_message(dt('A total of @count old semaphore entries were removed.', array('@count' => count($output[4]))), 'ok'); + } + + // Output results from running advagg_remove_temp_files(). + if (empty($output[5])) { + drupal_set_message(dt('No leftover temporary files found. Nothing was deleted.'), 'ok'); + } + else { + // format_plural() not always available. + drupal_set_message(dt('Some oleftover temporary files were found. A total of @count temporary files were removed.', array('@count' => $output[5])), 'ok'); + } + + // Output results from running advagg_refresh_all_locale_files(). + if (empty($output[6])) { + drupal_set_message(dt('Locale did not translate anything in any JavaScript files.'), 'ok'); + } + else { + drupal_set_message(dt('Locale did translate some JavaScript files. Resulting locale js files: @files', array('@files' => print_r($output[6], TRUE))), 'ok'); + } +} + +/** + * Flush the correct caches so CSS/JS changes go live. + */ +function drush_advagg_smart_cache_flush() { + // Clear the libraries cache. + if (function_exists('libraries_flush_caches')) { + $cache_tables = libraries_flush_caches(); + foreach ($cache_tables as $table) { + cache_clear_all('*', $table, TRUE); + } + } + + // Run the command. + module_load_include('inc', 'advagg', 'advagg.cache'); + $flushed = advagg_push_new_changes(); + + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 0) { + // Display a simple message if not in Development mode. + drush_log('Advagg Cache Cleared', 'ok'); + } + else { + list($css_path) = advagg_get_root_files_dir(); + $parts_uri = $css_path[1] . '/parts'; + + // Report back the results. + foreach ($flushed as $filename => $data) { + if (strpos($filename, $parts_uri) === 0) { + // Do not report on css files manged in the parts directory. + unset($flushed[$filename]); + continue; + } + $ext = pathinfo($filename, PATHINFO_EXTENSION); + drush_log(dt('The file @filename has changed. @db_usage aggregates are using this file. @db_count db cache entries and all @type full cache entries have been flushed from the cache bins. Trigger: @changes', array( + '@filename' => $filename, + '@db_usage' => $data[0], + '@db_count' => $data[1], + '@changes' => print_r($data[2], TRUE), + '@type' => $ext, + )), 'ok'); + } + + if (empty($flushed)) { + drush_log(dt('No changes found. Nothing was cleared.'), 'ok'); + return; + } + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.inc b/frontend/drupal/sites/all/modules/advagg/advagg.inc new file mode 100644 index 000000000..7db438b36 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.inc @@ -0,0 +1,1805 @@ + $aggregate_filenames_hash, + 'aggregate_contents_hash' => $aggregate_contents_hash, + 'atime' => 0, + 'root' => $root, + ); + + // Save new aggregate into the database if it does not exist. + $return = db_merge('advagg_aggregates_versions') + ->key(array( + 'aggregate_filenames_hash' => $record['aggregate_filenames_hash'], + 'aggregate_contents_hash' => $record['aggregate_contents_hash'], + )) + ->insertFields($record) + ->execute(); + return $return; +} + +/** + * Insert/Update data in the advagg_aggregates table. + * + * @param array $files + * List of files in the aggregate including files meta data. + * @param string $aggregate_filenames_hash + * Hash of the groupings of files. + * + * @return bool + * Return TRUE if anything was written to the database. + */ +function advagg_insert_aggregate(array $files, $aggregate_filenames_hash) { + // Record if a database write was done. + $write_done = FALSE; + + // Check if the aggregate is in the database. + $files_in_db = array(); + $query = db_select('advagg_aggregates', 'aa') + ->fields('aa', array('filename_hash')) + ->condition('aggregate_filenames_hash', $aggregate_filenames_hash) + ->orderBy('aa.porder', 'ASC') + ->execute(); + foreach ($query as $row) { + $files_in_db[$row->filename_hash] = (array) $row; + } + + $count = 0; + foreach ($files as $file_meta_data) { + ++$count; + + // Skip if the file already exists in the aggregate. + if (!empty($files_in_db[$file_meta_data['filename_hash']])) { + continue; + } + + // Store settings for this file that depend on how it was added. + $settings = array(); + if (isset($file_meta_data['media_query'])) { + $settings['media'] = $file_meta_data['media_query']; + } + + // Write record into the database. + $record = array( + 'aggregate_filenames_hash' => $aggregate_filenames_hash, + 'filename_hash' => $file_meta_data['filename_hash'], + 'porder' => $count, + 'settings' => serialize($settings), + ); + $return = db_merge('advagg_aggregates') + ->key(array( + 'aggregate_filenames_hash' => $record['aggregate_filenames_hash'], + 'filename_hash' => $record['filename_hash'], + )) + ->insertFields($record) + ->execute(); + + if ($return) { + $write_done = TRUE; + } + } + return $write_done; +} + +/** + * Insert/Update data in the advagg_files table. + * + * @param array $files + * List of files in the aggregate including files meta data. + * @param string $type + * String; css or js. + * + * @return bool + * Return TRUE if anything was written to the database. + */ +function advagg_insert_update_files(array $files, $type) { + // Record if a database write was done. + $write_done = FALSE; + + $filename_hashes = array(); + foreach ($files as $file_meta_data) { + $filename_hashes[] = $file_meta_data['filename_hash']; + } + + $files_in_db = array(); + if (!empty($filename_hashes)) { + $query = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename_hash', $filename_hashes) + ->execute(); + foreach ($query as $row) { + $files_in_db[$row->filename] = (array) $row; + } + } + + // Make drupal_get_installed_schema_version() available. + include_once DRUPAL_ROOT . '/includes/install.inc'; + foreach ($files as $filename => $file_meta_data) { + // Create record. + $record = array( + 'filename' => $filename, + 'filename_hash' => $file_meta_data['filename_hash'], + 'content_hash' => $file_meta_data['content_hash'], + 'filetype' => $type, + 'filesize' => $file_meta_data['filesize'], + 'mtime' => $file_meta_data['mtime'], + 'linecount' => $file_meta_data['linecount'], + ); + try { + // Check the file in the database. + if (empty($files_in_db[$filename])) { + // Add in filesize_processed if the schema is 7210 or higher. + if (drupal_get_installed_schema_version('advagg') >= 7210) { + $record['filesize_processed'] = (int) advagg_generate_filesize_processed($filename, $type); + } + // Add in use_strict if the schema is 7212 or higher. + if (drupal_get_installed_schema_version('advagg') >= 7212) { + $record['use_strict'] = 0; + if ($type === 'js') { + $record['use_strict'] = (int) advagg_does_js_start_with_use_strict($filename); + } + } + + // Insert into database. + $record['changes'] = 1; + + $return = db_merge('advagg_files') + ->key(array( + 'filename_hash' => $record['filename_hash'], + )) + ->insertFields($record) + ->execute(); + if ($return) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Inserting into db
@record
.', array('@record' => print_r($record, TRUE)), WATCHDOG_DEBUG); + } + $write_done = TRUE; + } + } + else { + // Take changes counter out of the diff equation. + $changes = $files_in_db[$filename]['changes']; + unset($files_in_db[$filename]['changes']); + // If not in strict mode, only use mtime if newer than the existing one. + if (!variable_get('advagg_strict_mtime_check', ADVAGG_STRICT_MTIME_CHECK)) { + // Make sure mtime only moves forward. + if ($record['mtime'] <= $files_in_db[$filename]['mtime']) { + $record['mtime'] = $files_in_db[$filename]['mtime']; + } + } + + // If something is different, update. + $diff = array_diff_assoc($record, $files_in_db[$filename]); + if (!empty($diff)) { + $diff['changes'] = $changes + 1; + $diff['filename_hash'] = $record['filename_hash']; + + // Add in filesize_processed if the schema is 7210 or higher. + if (drupal_get_installed_schema_version('advagg') >= 7210) { + $diff['filesize_processed'] = (int) advagg_generate_filesize_processed($filename, $type); + } + if (drupal_get_installed_schema_version('advagg') >= 7212) { + $diff['use_strict'] = 0; + if ($type === 'js') { + $diff['use_strict'] = (int) advagg_does_js_start_with_use_strict($filename); + if (empty($diff['use_strict'])) { + $diff['use_strict'] = 0; + } + } + } + + $return = db_merge('advagg_files') + ->key(array( + 'filename_hash' => $diff['filename_hash'], + )) + ->fields($diff) + ->execute(); + if ($return) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Updating db
@diff
.', array('@diff' => print_r($diff, TRUE)), WATCHDOG_DEBUG); + } + $write_done = TRUE; + } + } + } + } + catch (PDOException $e) { + // If it fails we don't care, the file was added to the table by another + // process then. + // Still log it if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + watchdog('advagg', 'Development Mode - Caught PDO Exception: @info', array('@info' => $e)); + } + } + } + return $write_done; +} + +/** + * Given a filename calculate the processed filesize. + * + * @param string $filename + * String; filename containing path information as well. + * @param string $type + * String; css or js. + * + * @return int + * Processed filesize. + */ +function advagg_generate_filesize_processed($filename, $type) { + $files = &drupal_static(__FUNCTION__, array()); + if (!isset($files[$type][$filename])) { + // Make advagg_get_*_aggregate_contents() available. + module_load_include('inc', 'advagg', 'advagg.missing'); + $aggregate_settings = advagg_current_hooks_hash_array(); + + $file_aggregate = array($filename => array()); + if ($type === 'css') { + list($contents) = advagg_get_css_aggregate_contents($file_aggregate, $aggregate_settings); + } + elseif ($type === 'js') { + list($contents) = advagg_get_js_aggregate_contents($file_aggregate, $aggregate_settings); + } + if (!empty($contents)) { + $files[$type][$filename] = strlen(gzencode($contents, 9, FORCE_GZIP)); + } + else { + $files[$type][$filename] = 0; + } + } + return $files[$type][$filename]; +} + +/** + * Given a js string, see if "use strict"; is the first thing ran. + * + * @param string $filename + * String; filename containing path information as well. + * + * @return bool + * True if "use strict"; is the first thing ran. + */ +function advagg_does_js_start_with_use_strict($filename) { + $files = &drupal_static(__FUNCTION__, array()); + if (!isset($files[$filename])) { + // Make advagg_get_*_aggregate_contents() available. + module_load_include('inc', 'advagg', 'advagg.missing'); + $aggregate_settings = advagg_current_hooks_hash_array(); + + $file_aggregate = array($filename => array()); + list($contents) = advagg_get_js_aggregate_contents($file_aggregate, $aggregate_settings); + + // See if the js file starts with "use strict";. + // Trim the JS down to 24kb. + $length = variable_get('advagg_js_header_length', ADVAGG_JS_HEADER_LENGTH); + $header = advagg_get_js_header($contents, $length); + + // Look for the string. + $use_strict = stripos($header, '"use strict";'); + $strict_js = FALSE; + if ($use_strict === FALSE) { + $use_strict = stripos($header, "'use strict';"); + } + if ($use_strict !== FALSE) { + if ($use_strict == 0) { + $strict_js = TRUE; + } + else { + // Get all text before "use strict";. + $substr = substr($header, 0, $use_strict); + // Check if there are any comments. + $single_line_comment = strpos($substr, '//'); + $multi_line_comment = strpos($substr, '/*'); + $in_function = strpos($substr, '{'); + if ($single_line_comment !== FALSE || $multi_line_comment !== FALSE) { + // Remove js comments and try again. + advagg_remove_js_comments($header); + + // Look for the string. + $use_strict = stripos($header, '"use strict";'); + if ($use_strict === FALSE) { + $use_strict = stripos($header, "'use strict';"); + } + // Get all text before "use strict"; with comments removed. + $substr = substr($header, 0, $use_strict); + // Check if there is a function before use strict. + $in_function = strpos($substr, '{'); + } + if ($in_function === FALSE) { + $strict_js = TRUE; + } + } + } + + $files[$filename] = $strict_js; + } + return $files[$filename]; +} + +/** + * Read only the first 8192 bytes to get the file header. + * + * @param string $content + * JS string to cut. + * @param int $length + * The number of bytes to grab. See advagg_js_header_length variable. + * + * @return string + * The shortened JS string. + */ +function advagg_get_js_header($content, $length) { + $content = trim($content); + // Only grab the first X bytes. + if (function_exists('mb_strcut')) { + $header = mb_strcut($content, 0, $length); + } + else { + $header = substr($content, 0, $length); + } + + return $header; +} + +/** + * Remove comments from JavaScript. + * + * @param string $content + * JS string to minify. + */ +function advagg_remove_js_comments(&$content) { + // Remove comments. + $content = preg_replace('/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?@finfo', array( + '@finfo' => var_export($info, TRUE), + )); + } + } + + // Get filesystem data. + $files_info = advagg_get_info_on_files($files_info_filenames); + + foreach ($files_with_meta_data as $info) { + // Skip if not a string or key doesn't exist. + if (!is_string($info['data']) || !array_key_exists($info['data'], $files_info)) { + continue; + } + + $filename = $info['data']; + $info += $files_info[$filename]; + // Skip if file doesn't exist. + if (empty($info['content_hash'])) { + continue; + } + + // Add info to arrays. + $filename_hashes[] = $info['filename_hash']; + $content_hashes[] = $info['content_hash']; + $filenames[$filename] = $info; + } + + // Generate filename. + $aggregate_filenames_hash = drupal_hash_base64(implode('', $filename_hashes)); + $aggregate_contents_hash = drupal_hash_base64(implode('', $content_hashes)); + $aggregate_filename = advagg_build_filename($type, $aggregate_filenames_hash, $aggregate_contents_hash); + return array( + $aggregate_filename, + $filenames, + $aggregate_filenames_hash, + $aggregate_contents_hash, + ); +} + +/** + * Load cache bin file info in static cache. + * + * @param array $files + * Array; array of filenames. + * + * @return array + * $cached_data. key is $cache_id; value is an array which contains + * + * @code + * 'filesize' => filesize($filename), + * 'mtime' => @filemtime($filename), + * 'filename_hash' => $filename_hash, + * 'content_hash' => drupal_hash_base64($file_contents), + * 'linecount' => $linecount, + * 'data' => $filename, + * 'fileext' => $ext, + * @endcode + */ +function &advagg_load_files_info_into_static_cache(array $files) { + // Get the static cache of this data. + $cached_data = &drupal_static('advagg_get_info_on_file'); + + // Get the statically cached data for all the given files. + $cache_ids = array(); + foreach ($files as $file) { + $cache_id = 'advagg:file:' . advagg_drupal_hash_base64($file); + if (!empty($cached_data) + && !empty($cached_data[$cache_id]) + ) { + // Make sure the cache_id is included. + $cached_data[$cache_id]['cache_id'] = $cache_id; + } + else { + $cache_ids[$file] = $cache_id; + } + } + // Get info from the cache back-end next. + if (!empty($cache_ids)) { + $values = array_values($cache_ids); + $cache_hits = cache_get_multiple($values, 'cache_advagg_info'); + if (!empty($cache_hits)) { + foreach ($cache_hits as $hit) { + if (!empty($hit->data['data'])) { + // Make sure the cache_id is included. + $hit->data['cache_id'] = $hit->cid; + // Add to static cache. + $cached_data[$hit->cid] = $hit->data; + } + } + } + } + return $cached_data; +} + +/** + * Given a filename calculate the hash for it. Uses static cache. + * + * @param string $file + * Filename. + * + * @return string + * hash of filename. + */ +function advagg_drupal_hash_base64($file) { + // Get the static cache of this data. + $cached_data = &drupal_static('advagg_drupal_hash_base64', array()); + if (!array_key_exists($file, $cached_data)) { + $cached_data[$file] = drupal_hash_base64($file); + } + return $cached_data[$file]; +} + +/** + * Given a filename calculate various hashes and gather meta data. + * + * @param array $files + * Array; array of filenames containing path information as well. + * @param bool $bypass_cache + * Bool: TRUE to bypass the cache. + * + * @return array + * $return['filename'] which contains + * + * @code + * 'filesize' => filesize($filename), + * 'mtime' => @filemtime($filename), + * 'filename_hash' => $filename_hash, + * 'content_hash' => drupal_hash_base64($file_contents), + * 'linecount' => $linecount, + * 'data' => $filename, + * 'fileext' => $ext, + * @endcode + */ +function advagg_get_info_on_files(array $files, $bypass_cache = FALSE, $run_alter = TRUE) { + // Get the cached data. + $cached_data = &advagg_load_files_info_into_static_cache($files); + + // Get basic info on the files. + $return = array(); + foreach ($files as $file) { + $filename_hash = advagg_drupal_hash_base64($file); + $cache_id = 'advagg:file:' . $filename_hash; + // If we are not bypassing the cache add cached data. + if ($bypass_cache == FALSE + && is_array($cached_data) + && array_key_exists($cache_id, $cached_data) + ) { + $return[$file] = $cached_data[$cache_id]; + continue; + } + + // Clear PHP's internal file status cache. + advagg_clearstatcache($file); + + // Remove file in the cache if it does not exist. + if (!file_exists($file) || is_dir($file)) { + if (isset($cached_data[$cache_id])) { + cache_clear_all($cache_id, 'cache_advagg_info', FALSE); + } + // Return filename_hash and data. Empty values for the other keys. + $return[$file] = array( + 'filesize' => 0, + 'mtime' => 0, + 'filename_hash' => $filename_hash, + 'content_hash' => '', + 'linecount' => 0, + 'data' => $file, + 'cache_id' => $cache_id, + '#no_cache' => TRUE, + ); + continue; + } + + // Get the file contents. + $file_contents = (string) @advagg_file_get_contents($file); + + $ext = strtolower(pathinfo($file, PATHINFO_EXTENSION)); + if ($ext !== 'css' && $ext !== 'js') { + // Get the $ext from the database. + $row = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename', $file) + ->execute()->fetchAssoc(); + if (!empty($row['filetype'])) { + $ext = $row['filetype']; + } + if ($ext === 'less') { + $ext = 'css'; + } + } + + if ($ext === 'css') { + // Get the number of selectors. + $linecount = advagg_count_css_selectors($file_contents); + } + else { + // Get the number of lines. + $linecount = substr_count($file_contents, "\n"); + } + + // Build meta data array and set cache. + $return[$file] = array( + 'filesize' => (int) @filesize($file), + 'mtime' => @filemtime($file), + 'filename_hash' => $filename_hash, + 'content_hash' => drupal_hash_base64($file_contents), + 'linecount' => $linecount, + 'data' => $file, + 'fileext' => $ext, + 'cache_id' => $cache_id, + ); + if (isset($cached_data[$cache_id])) { + $return[$file] += $cached_data[$cache_id]; + } + } + + if ($run_alter) { + // Run hook so other modules can modify the data on these files. + // Call hook_advagg_get_info_on_files_alter(). + drupal_alter('advagg_get_info_on_files', $return, $cached_data, $bypass_cache); + + // Set the cache and populate return array. + foreach ($return as $info) { + // If no cache is empty add/update the cached entry. + // Update the cache if it is new or something changed. + if (empty($info['#no_cache']) + && !empty($info['cache_id']) + && (empty($cached_data[$info['cache_id']]) || $info !== $cached_data[$info['cache_id']]) + ) { + // CACHE_PERMANENT isn't good here. Use 2 weeks from now + 0-45 days. + // The random 0 to 45 day addition is to prevent a cache stampede. + cache_set($info['cache_id'], $info, 'cache_advagg_info', round(REQUEST_TIME + 1209600 + mt_rand(0, 3888000), -3)); + } + + // Update static cache. + $cached_data[$info['cache_id']] = $info; + } + } + + return $return; +} + +/** + * Given a filename calculate various hashes and gather meta data. + * + * @param string $filename + * String; filename containing path information. + * @param bool $bypass_cache + * (optional) Bool: TRUE to bypass the cache. + * @param bool $run_alter + * (optional) Bool: FALSE to not run drupal_alter. + * + * @return array + * Array containing key value pairs. + * + * @code + * 'filesize' => filesize($filename), + * 'mtime' => @filemtime($filename), + * 'filename_hash' => $filename_hash, + * 'content_hash' => drupal_hash_base64($file_contents), + * 'linecount' => $linecount, + * 'data' => $filename, + * 'fileext' => $ext, + * @endcode + */ +function advagg_get_info_on_file($filename, $bypass_cache = FALSE, $run_alter = TRUE) { + $files_info = advagg_get_info_on_files(array($filename), $bypass_cache, $run_alter); + return $files_info[$filename]; +} + +/** + * Build the filename. + * + * @param string $type + * String; css or js. + * @param string $aggregate_filenames_hash + * Hash of the groupings of files. + * @param string $aggregate_contents_hash + * Hash of the files contents. + * @param string $hooks_hash + * Hash value from advagg_get_current_hooks_hash(). + * + * @return string + * String: The filename. No path info. + */ +function advagg_build_filename($type, $aggregate_filenames_hash, $aggregate_contents_hash, $hooks_hash = '') { + if (empty($hooks_hash)) { + $hooks_hash = advagg_get_current_hooks_hash(); + } + return $type . ADVAGG_SPACE . + $aggregate_filenames_hash . ADVAGG_SPACE . + $aggregate_contents_hash . ADVAGG_SPACE . + $hooks_hash . '.' . $type; +} + +/** + * Wrapper around clearstatcache so it can use php 5.3's new features. + * + * @param string $filename + * String. + * + * @return null + * value from clearstatcache(). + */ +function advagg_clearstatcache($filename = NULL) { + static $php530; + if (!isset($php530)) { + $php530 = version_compare(PHP_VERSION, '5.3.0', '>='); + } + + if ($php530) { + return clearstatcache(TRUE, $filename); + } + else { + return clearstatcache(); + } +} + +/** + * Group the CSS/JS into the biggest buckets possible. + * + * @param array $files_to_aggregate + * An array of CSS/JS groups. + * @param string $type + * String; css or js. + * + * @return array + * New version of groups. + */ +function advagg_generate_groups(array $files_to_aggregate, $type) { + $groups = array(); + $count = 0; + $location = 0; + + $media = ''; + $defer = ''; + $async = ''; + $cache = ''; + $scope = ''; + $use_strict = 0; + $browsers = array(); + $selector_count = 0; + // Get CSS limit value. + $limit_value = variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE); + + if (variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER) + || variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT) + || variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD) + ) { + $filenames = array(); + foreach ($files_to_aggregate as $data) { + foreach ($data as $values) { + foreach ($values['items'] as $file_info) { + if (!empty($file_info['data']) && is_string($file_info['data'])) { + $filenames[] = $file_info['data']; + } + else { + watchdog('advagg', 'Bad data key. File info: @finfo Group info: @ginfo', array( + '@finfo' => var_export($file_info, TRUE), + '@ginfo' => var_export($values, TRUE), + )); + } + } + } + } + + // Get filesystem data. + $files_info = advagg_get_info_on_files($filenames, TRUE); + } + + $strict_files = array(); + if ($type == 'js') { + // Make drupal_get_installed_schema_version() available. + include_once DRUPAL_ROOT . '/includes/install.inc'; + if (drupal_get_installed_schema_version('advagg') >= 7213) { + $query = db_select('advagg_files', 'af') + ->fields('af', array('filename', 'use_strict')) + ->condition('use_strict', 1) + ->execute(); + foreach ($query as $row) { + $strict_files[$row->filename] = $row->use_strict; + } + } + } + + foreach ($files_to_aggregate as $data) { + foreach ($data as $values) { + + // Group into the biggest buckets possible. + $last_ext = ''; + foreach ($values['items'] as $file_info) { + $parts = array(); + // Check to see if media, browsers, defer, async, cache, or scope has + // changed from the previous run of this loop. + $changed = FALSE; + $ext = isset($file_info['fileext']) ? $file_info['fileext'] : pathinfo($file_info['data'], PATHINFO_EXTENSION); + $ext = strtolower($ext); + if ($ext !== 'css' && $ext !== 'js') { + if (empty($last_ext)) { + // Get the $ext from the database. + $row = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename', $file_info['data']) + ->execute()->fetchAssoc(); + $ext = $row['filetype']; + } + else { + $ext = $last_ext; + } + } + $last_ext = $ext; + if ($ext === 'css') { + if (isset($file_info['media'])) { + if (variable_get('advagg_combine_css_media', ADVAGG_COMBINE_CSS_MEDIA)) { + $file_info['media_query'] = $file_info['media']; + } + elseif ($media != $file_info['media']) { + // Media changed. + $changed = TRUE; + $media = $file_info['media']; + } + } + if (empty($file_info['media']) && !empty($media)) { + // Media changed to empty. + $changed = TRUE; + $media = ''; + } + } + if (isset($file_info['browsers'])) { + // Browsers changed. + $diff = array_merge(array_diff_assoc($file_info['browsers'], $browsers), array_diff_assoc($browsers, $file_info['browsers'])); + if (!empty($diff)) { + $changed = TRUE; + $browsers = $file_info['browsers']; + } + } + if (empty($file_info['browsers']) && !empty($browsers)) { + // Browsers changed to empty. + $changed = TRUE; + $browsers = array(); + } + + if (!empty($strict_files[$file_info['data']]) && $use_strict != $strict_files[$file_info['data']]) { + // use_strict value changed to 1. + $changed = TRUE; + $use_strict = 1; + } + if (!empty($use_strict) && empty($strict_files[$file_info['data']])) { + // use_strict value changed to 0. + $changed = TRUE; + $use_strict = 0; + } + if (isset($file_info['defer']) && $defer != $file_info['defer']) { + // Defer value changed. + $changed = TRUE; + $defer = $file_info['defer']; + } + if (!empty($defer) && empty($file_info['defer'])) { + // Defer value changed to empty. + $changed = TRUE; + $defer = ''; + } + if (isset($file_info['async']) && $async != $file_info['async']) { + // Async value changed. + $changed = TRUE; + $async = $file_info['async']; + } + if (!empty($async) && empty($file_info['async'])) { + // Async value changed to empty. + $changed = TRUE; + $async = ''; + } + if (isset($file_info['cache']) && $cache != $file_info['cache']) { + // Cache value changed. + $changed = TRUE; + $cache = $file_info['cache']; + } + if (!empty($cache) && empty($file_info['cache'])) { + // Cache value changed to empty. + $changed = TRUE; + $cache = ''; + } + if (isset($file_info['scope']) && $scope != $file_info['scope']) { + // Scope value changed. + $changed = TRUE; + $scope = $file_info['scope']; + } + if (!empty($scope) && empty($file_info['scope'])) { + // Scope value changed to empty. + $changed = TRUE; + $scope = ''; + } + + if (variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER) + && array_key_exists('data', $file_info) + && is_string($file_info['data']) + && array_key_exists($file_info['data'], $files_info) + ) { + $file_info += $files_info[$file_info['data']]; + // Prevent CSS rules exceeding 4095 due to limits with IE9 and below. + if ($ext === 'css') { + $selector_count += $file_info['linecount']; + if ($selector_count > $limit_value) { + $changed = TRUE; + $selector_count = $file_info['linecount']; + + // Break large file into multiple smaller files. + if ($file_info['linecount'] > $limit_value) { + $parts = advagg_split_css_file($file_info); + } + } + } + } + + // Merge in dns_prefetch. + if ((variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT)) + && isset($files_info[$file_info['data']]['dns_prefetch']) + ) { + if (!isset($file_info['dns_prefetch'])) { + $file_info['dns_prefetch'] = array(); + } + if (!empty($file_info['dns_prefetch']) && is_string($file_info['dns_prefetch'])) { + $temp = $file_info['dns_prefetch']; + unset($file_info['dns_prefetch']); + $file_info['dns_prefetch'] = array($temp); + } + $file_info['dns_prefetch'] = array_filter(array_unique(array_merge($file_info['dns_prefetch'], $files_info[$file_info['data']]['dns_prefetch']))); + } + + // Merge in preload. + if (variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD) + && isset($files_info[$file_info['data']]['preload']) + ) { + if (!isset($file_info['preload'])) { + $file_info['preload'] = array(); + } + if (!empty($file_info['preload']) && is_string($file_info['preload'])) { + $temp = $file_info['preload']; + unset($file_info['preload']); + $file_info['preload'] = array($temp); + } + $file_info['preload'] = array_filter(array_unique(array_merge($file_info['preload'], $files_info[$file_info['data']]['preload']))); + } + + // If one of the above options changed, it needs to be in a different + // aggregate. + if (!empty($parts)) { + foreach ($parts as $part) { + ++$count; + $groups[$location][$count][] = $part; + } + } + else { + if ($changed) { + ++$count; + } + $groups[$location][$count][] = $file_info; + } + } + } + // Grouping if inline is mixed between files. + ++$location; + } + + return $groups; +} + +/** + * Given a file info array it will split the file up. + * + * @param array $file_info + * File info array from advagg_get_info_on_file(). + * @param string $file_contents + * CSS file contents. + * + * @return array + * Array with advagg_get_info_on_file data and split data. + */ +function advagg_split_css_file(array $file_info, $file_contents = '') { + // Make advagg_parse_media_blocks() available. + module_load_include('inc', 'advagg', 'advagg.missing'); + + // Get the CSS file and break up by media queries. + if (empty($file_contents)) { + $file_contents = (string) @advagg_file_get_contents($file_info['data']); + } + $media_blocks = advagg_parse_media_blocks($file_contents); + + // Get the advagg_ie_css_selector_limiter_value. + $selector_limit = (int) max(variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE), 100); + + // Group media queries together. + $part_selector_count = 0; + $counter = 0; + $values = array(); + foreach ($media_blocks as $media_block) { + // Get the number of selectors. + $selector_count = advagg_count_css_selectors($media_block); + + // This chunk is bigger than $selector_limit. It needs to be split. + if ($selector_count > $selector_limit) { + $inner_selector_count = 0; + // Split css string. + list($media_query, $split_css_strings) = advagg_split_css_string($media_block, $selector_limit); + foreach ($split_css_strings as $split_css_strings) { + $counter_changed = FALSE; + if (empty($split_css_strings)) { + continue; + } + + // Make sure selector count doesn't go over selector limit. + $inner_selector_count = advagg_count_css_selectors($split_css_strings); + $part_selector_count += $inner_selector_count; + if ($part_selector_count > $selector_limit) { + if (!empty($values[$counter])) { + ++$counter; + } + $counter_changed = TRUE; + $part_selector_count = $inner_selector_count; + } + + // Add to output array. + if (isset($values[$counter])) { + if (!empty($media_query)) { + $values[$counter] .= "\n$media_query { $split_css_strings } "; + } + else { + $values[$counter] .= "$split_css_strings"; + } + } + else { + if (!empty($media_query)) { + $values[$counter] = "$media_query { $split_css_strings } "; + } + else { + $values[$counter] = $split_css_strings; + } + } + } + // Add to current selector counter and go to the next value. + if (!$counter_changed) { + $part_selector_count += $inner_selector_count; + } + continue; + } + + $part_selector_count += $selector_count; + if ($part_selector_count > $selector_limit) { + if (!empty($values[$counter])) { + ++$counter; + } + $values[$counter] = $media_block; + $part_selector_count = $selector_count; + } + else { + if (isset($values[$counter])) { + $values[$counter] .= "\n$media_block"; + } + else { + $values[$counter] = $media_block; + } + } + } + + // Save data. + $parts = array(); + $overall_counter = 0; + foreach ($values as $key => $value) { + $last_chunk = FALSE; + $file_info['split_last_part'] = FALSE; + if (count($values) - 1 == $key) { + $last_chunk = TRUE; + } + if ($last_chunk) { + $file_info['split_last_part'] = TRUE; + } + + // Get the number of selectors. + $selector_count = advagg_count_css_selectors($value); + $overall_counter += $selector_count; + + // Save file. + $subfile = advagg_create_subfile($value, $overall_counter, $file_info); + if (empty($subfile)) { + // Something broke; do not create a subfile. + watchdog('advagg', 'Spliting up a CSS file failed. File info: @info', array('@info' => var_export($file_info, TRUE))); + return array(); + } + $parts[] = $subfile; + } + return $parts; +} + +/** + * Count the number of selectors inside of a CSS string. + * + * @param string $css_string + * CSS string. + * + * @return int + * The number of CSS selectors. + */ +function advagg_count_css_selectors($css_string) { + return substr_count($css_string, ',') + substr_count($css_string, '{') - substr_count($css_string, '@media'); +} + +/** + * Given a css string it will split it if it's over the selector limit. + * + * @param string $css_string + * CSS string. + * @param int $selector_limit + * How many selectors can be grouped together. + * + * @return array + * Array that contains the $media_query and the $css_array. + */ +function advagg_split_css_string($css_string, $selector_limit) { + // See if this css string is wrapped in a @media statement. + $media_query = ''; + $media_query_pos = strpos($css_string, '@media'); + if ($media_query_pos !== FALSE) { + // Get the opening bracket. + $open_bracket_pos = strpos($css_string, "{", $media_query_pos); + // Skip if there is a syntax error. + if ($open_bracket_pos === FALSE) { + return array(); + } + $media_query = substr($css_string, $media_query_pos, $open_bracket_pos - $media_query_pos); + $css_string_inside = substr($css_string, $open_bracket_pos + 1); + } + else { + $css_string_inside = $css_string; + } + + // Split CSS into selector chunks. + $split = preg_split('/(\{.+?\}|,)/si', $css_string_inside, -1, PREG_SPLIT_DELIM_CAPTURE); + + $new_css_chunk = array(0 => ''); + $selector_chunk_counter = 0; + $counter = 0; + // Have the key value be the running selector count and put split array semi + // back together. + foreach ($split as $value) { + $new_css_chunk[$counter] .= $value; + if (strpos($value, '}') === FALSE) { + ++$selector_chunk_counter; + } + else { + if ($counter + 1 < $selector_chunk_counter) { + $selector_chunk_counter += ($counter - $selector_chunk_counter + 1) / 2; + } + $counter = $selector_chunk_counter; + if (!isset($new_css_chunk[$counter])) { + $new_css_chunk[$counter] = ''; + } + } + } + + // Generate output array in this function. + $css_array = array(); + $keys = array_keys($new_css_chunk); + $counter = 0; + $chunk_counter = 0; + foreach (array_keys($keys) as $key) { + // Get out of loop if at the end of the array. + if (!isset($keys[$key + 1])) { + break; + } + + // Get values, keys and counts. + $this_value = $new_css_chunk[$keys[$key]]; + $this_key = $keys[$key]; + $next_key = $keys[$key + 1]; + $this_selector_count = $next_key - $this_key; + + // Single rule is bigger than the selector limit. + if ($this_selector_count > $selector_limit) { + // Get css rules for these selectors. + $open_bracket_pos = strpos($this_value, "{"); + $css_rule = ' ' . substr($this_value, $open_bracket_pos); + + // Split on selectors. + $split = preg_split('/(\,)/si', $this_value, NULL, PREG_SPLIT_OFFSET_CAPTURE); + $index = 0; + $counter = 0; + while (isset($split[$index][1])) { + // Get starting and ending positions of the selectors given the selector + // limit. + $next_index = $index + $selector_limit - 1; + $start = $split[$index][1]; + if (isset($split[$next_index][1])) { + $end = $split[$next_index][1]; + } + else { + // Last one. + $temp = end($split); + $split_key = key($split); + $counter = $split_key % $selector_limit; + $end_open_bracket_pos = (int) strpos($temp[0], "{"); + $end = $temp[1] + $end_open_bracket_pos; + } + + // Extract substr. + $sub_this_value = substr($this_value, $start, $end - $start - 1) . $css_rule; + + // Save substr. + ++$chunk_counter; + $key_output = $selector_limit; + if (!empty($counter)) { + $key_output = $selector_limit - $counter; + } + $css_array["$chunk_counter $key_output"] = ''; + + if (!isset($css_array[$chunk_counter])) { + $css_array[$chunk_counter] = $sub_this_value; + } + else { + $css_array[$chunk_counter] .= $sub_this_value; + } + + // Move counter. + $index = $next_index; + } + continue; + } + + $counter += $this_selector_count; + if ($counter > $selector_limit) { + $key_output = $counter - $this_selector_count; + $css_array["$chunk_counter $key_output"] = ''; + $counter = $next_key - $this_key; + ++$chunk_counter; + } + if (!isset($css_array[$chunk_counter])) { + $css_array[$chunk_counter] = $this_value; + } + else { + $css_array[$chunk_counter] .= $this_value; + } + } + + // Group into sets smaller than $selector_limit. + return array($media_query, $css_array); +} + +/** + * Write CSS parts to disk; used when CSS selectors in one file is > 4096. + * + * @param string $css + * CSS data to write to disk. + * @param int $overall_split + * Running count of what selector we are from the original file. + * @param array $file_info + * File info array from advagg_get_info_on_file(). + * + * @return array + * Array with advagg_get_info_on_file data and split data; FALSE on failure. + */ +function advagg_create_subfile($css, $overall_split, array $file_info) { + static $parts_uri; + static $parts_path; + if (!isset($parts_uri)) { + list($css_path) = advagg_get_root_files_dir(); + $parts_uri = $css_path[0] . '/parts'; + $parts_path = $css_path[1] . '/parts'; + + // Create the public://advagg_css/parts dir. + file_prepare_directory($parts_uri, FILE_CREATE_DIRECTORY); + + // Make advagg_save_data() available. + module_load_include('inc', 'advagg', 'advagg.missing'); + } + + // Get the path from $file_info['data']. + $uri_path = advagg_get_relative_path($file_info['data']); + if (!file_exists($uri_path) || is_dir($uri_path)) { + return FALSE; + } + + // Write the current chunk of the CSS into a file. + $new_filename = str_ireplace('.css', '.' . $overall_split . '.css', $uri_path); + + // Fix for things that write dynamically to the public file system. + $scheme = file_uri_scheme($new_filename); + if ($scheme) { + $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); + if ($wrapper && method_exists($wrapper, 'getDirectoryPath')) { + // Use the wrappers directory path. + $new_filename = $wrapper->getDirectoryPath() . '/' . file_uri_target($new_filename); + } + else { + // If the scheme does not have a wrapper; prefix file with the scheme. + $new_filename = $scheme . '/' . file_uri_target($new_filename); + } + } + + $part_uri = $parts_uri . '/' . $new_filename; + $dirname = drupal_dirname($part_uri); + file_prepare_directory($dirname, FILE_CREATE_DIRECTORY); + $filename_path = (advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) ? $parts_uri : $parts_path; + + // Get info on the file that was just created. + $part = advagg_get_info_on_file($filename_path . '/' . $new_filename, TRUE) + $file_info; + $part['split'] = TRUE; + $part['split_location'] = $overall_split; + $part['split_original'] = $file_info['data']; + + // Overwrite/create file if hash doesn't match. + $hash = drupal_hash_base64($css); + if ($part['content_hash'] !== $hash) { + advagg_save_data($part_uri, $css, TRUE); + $part = advagg_get_info_on_file($filename_path . '/' . $new_filename, TRUE) + $file_info; + $part['split'] = TRUE; + $part['split_location'] = $overall_split; + $part['split_original'] = $file_info['data']; + } + + return $part; +} + +/** + * Replacement for drupal_build_css_cache() and drupal_build_js_cache(). + * + * @param array $files_to_aggregate + * An array of CSS/JS groups. + * @param string $type + * String; css or js. + * + * @return array + * array of aggregate files. + */ +function advagg_build_aggregate_plans(array $files_to_aggregate, $type) { + if ($type !== 'css' && $type !== 'js') { + return array(); + } + + // Place into biggest grouping possible. + $groups = advagg_generate_groups($files_to_aggregate, $type); + + // Get filenames. + $files = advagg_generate_filenames($groups, $type); + + // Insert/Update Database. + advagg_insert_update_db($files, $type, 1); + // Update atimes for root. + advagg_multi_update_atime($files); + + // Run hooks to modify the aggregate. + // Call hook_advagg_build_aggregate_plans_alter(). + $modified = FALSE; + drupal_alter('advagg_build_aggregate_plans', $files, $modified, $type); + + // If the hook above modified anything, re-insert into database. + if ($modified) { + // Insert/Update Database. + advagg_insert_update_db($files, $type, 0); + // Update atimes for non root. + advagg_multi_update_atime($files); + } + + // Get file paths. + list($css_path, $js_path) = advagg_get_root_files_dir(); + + // Build the plan. + $plans = array(); + + foreach ($files as $agg_filename => $values) { + if ($type === 'css') { + $mixed_media = FALSE; + $media = NULL; + foreach ($values['files'] as $value) { + if (!isset($value['media'])) { + continue; + } + if (is_null($media)) { + $media = $value['media']; + } + if ($media != $value['media']) { + $mixed_media = TRUE; + } + } + } + + $onload = array(); + $onerror = array(); + $attributes = array(); + $onloadcss = array(); + foreach ($values['files'] as &$items) { + // Get onload. + if (!empty($items['onload'])) { + $onload[] = $items['onload']; + } + // Get attributes onload. + if (!empty($items['attributes']['onload'])) { + $onload[] = $items['attributes']['onload']; + unset($items['attributes']['onload']); + } + // Get onerror. + if (!empty($items['onerror'])) { + $onload[] = $items['onerror']; + } + // Get attributes onerror. + if (!empty($items['attributes']['onerror'])) { + $onload[] = $items['attributes']['onerror']; + unset($items['attributes']['onerror']); + } + // Get attributes onloadCSS. + if (!empty($items['onloadCSS'])) { + $onloadcss[] = $items['onloadCSS']; + } + // Get attributes onloadCSS. + if (!empty($items['attributes']['onloadCSS'])) { + $onloadcss[] = $items['attributes']['onloadCSS']; + unset($items['attributes']['onloadCSS']); + } + // Get attributes. + if (!empty($items['attributes'])) { + $attributes += $items['attributes']; + } + } + $onload = implode(';', array_unique(array_filter($onload))); + $onerror = implode(';', array_unique(array_filter($onerror))); + $onloadcss = implode(';', array_unique(array_filter($onloadcss))); + + $first = reset($values['files']); + if (!empty($mixed_media)) { + $first['media'] = 'all'; + } + $url = ($type === 'css') ? $css_path[0] : $js_path[0]; + $path = ($type === 'css') ? $css_path[1] : $js_path[1]; + $plans[$agg_filename] = array( + 'data' => $url . '/' . $agg_filename, + 'media' => isset($first['media']) ? $first['media'] : '', + 'defer' => isset($first['defer']) ? $first['defer'] : '', + 'async' => isset($first['async']) ? $first['async'] : '', + 'onload' => $onload, + 'onerror' => $onerror, + 'browsers' => isset($first['browsers']) ? $first['browsers'] : array(), + 'cache' => isset($first['cache']) ? $first['cache'] : TRUE, + 'type' => $first['type'], + 'items' => $values, + 'filepath' => $path . '/' . $agg_filename, + 'filename' => $agg_filename, + 'attributes' => $attributes, + ); + if (!empty($onloadcss)) { + $plans[$agg_filename]['attributes']['onloadCSS'] = $onloadcss; + } + } + $plans = array_values($plans); + + // Create the aggregate files. + if (variable_get('advagg_pregenerate_aggregate_files', ADVAGG_PREGENERATE_AGGREGATE_FILES)) { + advagg_create_aggregate_files($plans, $type); + } + + // Run hooks to modify the plans. + // Call hook_advagg_build_aggregate_plans_post_alter(). + drupal_alter('advagg_build_aggregate_plans_post', $plans, $type); + + return $plans; +} + +/** + * Create the aggregate if it does not exist; using HTTPRL if possible. + * + * @param array $plans + * An array of aggregate file names. + * @param string $type + * String; css or js. + * + * @return array + * An array of what was done when generating the file. + */ +function advagg_create_aggregate_files(array $plans, $type) { + $filenames = array(); + $return = array(); + foreach ($plans as $plan) { + $filenames[] = $plan['filename']; + } + + // If the httprl module exists and we want to use it. + if (module_exists('httprl') + && variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL) + && (is_callable('httprl_is_background_callback_capable') + && httprl_is_background_callback_capable() + || !is_callable('httprl_is_background_callback_capable') + ) + ) { + if (variable_get('advagg_fast_filesystem', ADVAGG_FAST_FILESYSTEM)) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + foreach ($filenames as $key => $filename) { + if ($type === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + if (file_exists($uri)) { + unset($filenames[$key]); + } + } + } + if (!empty($filenames)) { + // Setup callback options array; call function in the background. + $callback_options = array( + array( + 'function' => 'advagg_build_aggregates', + ), + $filenames, $type, + ); + // Queue up the request. + httprl_queue_background_callback($callback_options); + // Execute request. + $return = httprl_send_request(); + } + } + else { + $return = advagg_build_aggregates($filenames, $type); + } + return $return; +} + +/** + * Loads the stylesheet and resolves all @import commands. + * + * Loads a stylesheet and replaces @import commands with the contents of the + * imported file. Use this instead of file_get_contents when processing + * stylesheets. + * + * The returned contents are compressed removing white space and comments only + * when CSS aggregation is enabled. This optimization will not apply for + * color.module enabled themes with CSS aggregation turned off. + * + * @param string $file + * Name of the stylesheet to be processed. + * @param bool $optimize + * Defines if CSS contents should be compressed or not. + * @param array $aggregate_settings + * Array of settings. + * + * @return string + * Contents of the stylesheet, including any resolved @import commands. + */ +function advagg_load_css_stylesheet($file, $optimize = TRUE, array $aggregate_settings = array(), $contents = '') { + $old_base_path = $GLOBALS['base_path']; + + // Change context to that of when this aggregate was created. + advagg_context_switch($aggregate_settings, 0); + + // Get the stylesheets contents. + $contents = advagg_load_stylesheet($file, $optimize, TRUE, $contents); + + // Resolve public:// if needed. + if (!advagg_is_external($file) && file_uri_scheme($file)) { + $file = advagg_get_relative_path($file); + } + + // Get the parent directory of this file, relative to the Drupal root. + $css_base_url = substr($file, 0, strrpos($file, '/')); + + // Handle split css files. + list($css_path) = advagg_get_root_files_dir(); + $parts_path = ((advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) ? $css_path[0] : $css_path[1]) . '/parts/'; + $url_parts = strpos($css_base_url, $parts_path); + // If this CSS file is actually a part of a previously split larger CSS file, + // don't use it to construct relative paths within the CSS file for + // 'url(...)' bits. + if ($url_parts !== FALSE) { + $css_base_url = substr($css_base_url, $url_parts + strlen($parts_path)); + } + + // Replace the old base path with the one that was passed in. + if (advagg_is_external($css_base_url) || variable_get('advagg_skip_file_create_url_inside_css', ADVAGG_SKIP_FILE_CREATE_URL_INSIDE_CSS)) { + $pos = strpos($css_base_url, $old_base_path); + if ($pos !== FALSE) { + $parsed_url = parse_url($css_base_url); + if (!empty($parsed_url['path'])) { + // Remove any double slash in path. + $parsed_url['path'] = str_replace('//', '/', $parsed_url['path']); + + // Get newly recalculated position. + $pos = strpos($parsed_url['path'], $old_base_path); + + // Replace. + if (strpos($parsed_url['path'], '/') !== 0 && $old_base_path === '/') { + // Special case if going to a subdir. + $parsed_url['path'] = $GLOBALS['base_path'] . $parsed_url['path']; + } + else { + $parsed_url['path'] = substr_replace($parsed_url['path'], $GLOBALS['base_path'], $pos, strlen($old_base_path)); + } + + $css_base_url = advagg_glue_url($parsed_url); + } + } + } + + _advagg_build_css_path(array(), $css_base_url . '/', $aggregate_settings); + // Anchor all paths in the CSS with its base URL, ignoring external, + // absolute paths, and urls that start with # or %23 (SVG). + $contents = preg_replace_callback('%url\(\s*+[\'"]?+(?![a-z]++:|/|\#|\%23+)([^\'"\)]++)[\'"]?+\s*+\)%i', '_advagg_build_css_path', $contents); + + // Change context back. + advagg_context_switch($aggregate_settings, 1); + + // Return the stylesheets contents. + return $contents; +} + +/** + * Changes context when generating CSS or JS files. + * + * @param array $aggregate_settings + * Array of settings. + * @param int $mode + * Use 0 to change context to what is inside of $aggregate_settings. + * Use 1 to change context back. + */ +function advagg_context_switch(array $aggregate_settings, $mode) { + $original = &drupal_static(__FUNCTION__); + + // Use current $aggregate_settings if none was passed in. + if (empty($aggregate_settings)) { + $aggregate_settings = advagg_current_hooks_hash_array(); + } + + // Call hook_advagg_context_alter(). + drupal_alter('advagg_context', $original, $aggregate_settings, $mode); +} + +/** + * Prefixes all paths within a CSS file for drupal_build_css_cache(). + * + * @param array $matches + * Array of matched items from preg_replace_callback(). + * @param string $base + * Base path. + * @param array $aggregate_settings + * Array of settings. + * + * @return string + * New version of the url() string from the css. + * + * @see _drupal_build_css_path() + * @see https://drupal.org/node/1961340#comment-7735815 + * @see https://drupal.org/node/1514182#comment-7875489 + */ +function _advagg_build_css_path(array $matches, $base = '', array $aggregate_settings = array()) { + $_base = &drupal_static(__FUNCTION__, ''); + $_aggregate_settings = &drupal_static(__FUNCTION__ . '_aggregate_settings', array()); + // Store base path for preg_replace_callback. + if (!empty($base)) { + $_base = $base; + } + if (!empty($aggregate_settings)) { + $_aggregate_settings = $aggregate_settings; + } + // Short circuit if no matches were passed in. + if (empty($matches)) { + return ''; + } + + // Prefix with base. + $url = $_base . $matches[1]; + + // If advagg_file_create_url() is not being used and the $url is local, redo + // the $url taking the base_path into account. + if (!advagg_is_external($url) && variable_get('advagg_skip_file_create_url_inside_css', ADVAGG_SKIP_FILE_CREATE_URL_INSIDE_CSS)) { + $new_base_path = $GLOBALS['base_path']; + if (isset($_aggregate_settings['variables']['base_path'])) { + $new_base_path = $_aggregate_settings['variables']['base_path']; + } + // Remove first /. + $new_base_path = ltrim($new_base_path, '/'); + $pos = FALSE; + // See if base_path is in the passed in $_base. + if (!empty($new_base_path)) { + $pos = strpos($_base, $new_base_path); + } + if ($pos !== FALSE) { + $url = substr($_base, $pos) . $matches[1]; + } + else { + $url = $new_base_path . $_base . $matches[1]; + } + } + // Remove '../' segments where possible. + $last = ''; + while ($url != $last) { + $last = $url; + $url = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $url); + } + + // Parse and build back the url without the query and fragment parts. + $parsed_url = parse_url($url); + $base_url = advagg_glue_url($parsed_url, TRUE); + + $query = isset($parsed_url['query']) ? $parsed_url['query'] : ''; + // In the case of certain URLs, we may have simply a '?' character without + // further parameters. parse_url() misses this and leaves 'query' blank, so + // need to this back in. + // See http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax + // for more information. + if ($query != '' || strpos($url, $base_url . '?') === 0) { + $query = '?' . $query; + } + $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; + + $run_file_create_url = FALSE; + if (!variable_get('advagg_skip_file_create_url_inside_css', ADVAGG_SKIP_FILE_CREATE_URL_INSIDE_CSS)) { + $run_file_create_url = TRUE; + } + if (empty($parsed_url['host'])) { + $base_url = ltrim($base_url, '/'); + } + $base_url = advagg_file_create_url($base_url, $_aggregate_settings, $run_file_create_url, 'css'); + + return 'url(' . $base_url . $query . $fragment . ')'; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.info b/frontend/drupal/sites/all/modules/advagg/advagg.info new file mode 100644 index 000000000..f2ece0743 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.info @@ -0,0 +1,13 @@ +name = Advanced CSS/JS Aggregation (AdvAgg) +description = Aggregates multiple CSS/JS files in a way that prevents 404 from happening when accessing a CSS or JS file. +package = Advanced CSS/JS Aggregation +core = 7.x +files[] = tests/advagg.test + +configure = admin/config/development/performance/advagg + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.install b/frontend/drupal/sites/all/modules/advagg/advagg.install new file mode 100644 index 000000000..cad60a2e8 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.install @@ -0,0 +1,2868 @@ + array( + 'aggregate_filenames_hash', + 'filename_hash', + ), + 'advagg_aggregates_versions' => array( + 'aggregate_filenames_hash', + 'aggregate_contents_hash', + ), + 'advagg_files' => array( + 'filename_hash', + 'content_hash', + ), + ); + + $schema = advagg_schema(); + foreach ($tables as $table => $fields) { + // Change utf8_bin to ascii_bin. + advagg_install_change_table_collation($table, $fields, 'ascii_bin', $schema[$table]['fields']); + } + // New install gets a locked admin section. + variable_set('advagg_admin_mode', 0); +} + +/** + * Implements hook_enable(). + */ +function advagg_enable() { + // Make sure the advagg_get_root_files_dir() function is available. + drupal_load('module', 'advagg'); + + // Make sure permissions for dirs are correct. Needed if installed via drush. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $stat_public = stat('public://'); + $stat_css = stat($css_path[0]); + $stat_js = stat($js_path[0]); + if (isset($stat_public['uid'])) { + if (isset($stat_css['uid']) && $stat_public['uid'] != $stat_css['uid']) { + @chown($css_path[0], $stat_public['uid']); + } + if (isset($stat_js['uid']) && $stat_public['uid'] != $stat_js['uid']) { + @chown($stat_js[0], $stat_public['uid']); + } + } + if (isset($stat_public['gid'])) { + if (isset($stat_css['gid']) && $stat_public['gid'] != $stat_css['gid']) { + @chgrp($css_path[0], $stat_public['gid']); + } + if (isset($stat_js['uid']) && $stat_public['gid'] != $stat_js['gid']) { + @chgrp($stat_js[0], $stat_public['gid']); + } + } + if (drupal_is_cli()) { + // Remove advagg and public dirs if empty and running from command line. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files = file_scan_directory($css_path[1], '/.*/'); + if (empty($files)) { + rmdir($css_path[1]); + } + $files = file_scan_directory($js_path[1], '/.*/'); + if (empty($files)) { + rmdir($js_path[1]); + } + $files = file_scan_directory('public://', '/.*/'); + if (empty($files)) { + rmdir('public://'); + } + } + + // Make sure the advagg_flush_all_cache_bins() function is available. + module_load_include('inc', 'advagg', 'advagg'); + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Flush caches. + advagg_flush_all_cache_bins(); + + // Flush menu cache on shutdown. + register_shutdown_function('menu_rebuild'); + + // Set the advagg_needs_update variable if this is a major version update. + if (!db_table_exists('advagg_aggregates_versions')) { + variable_set('advagg_needs_update', TRUE); + } + else { + variable_del('advagg_needs_update'); + } +} + +/** + * Implements hook_disable(). + */ +function advagg_disable() { + // Make sure the advagg_get_root_files_dir() function is available. + drupal_load('module', 'advagg'); + + // Make sure the advagg_flush_all_cache_bins() function is available. + module_load_include('inc', 'advagg', 'advagg'); + module_load_include('inc', 'advagg', 'advagg.cache'); + + // Flush caches. + advagg_flush_all_cache_bins(); + _drupal_flush_css_js(); + drupal_clear_css_cache(); + drupal_clear_js_cache(); + cache_clear_all('*', 'cache_page', TRUE); + + // Make sure the theme_registry: cid is cleared. + register_shutdown_function('cache_clear_all', 'theme_registry:', 'cache', TRUE); +} + +/** + * Implements hook_uninstall(). + */ +function advagg_uninstall() { + // Make sure the advagg_get_root_files_dir() function is available. + drupal_load('module', 'advagg'); + list($css_path, $js_path) = advagg_get_root_files_dir(); + + // Make sure the advagg_flush_all_cache_bins() function is available. + module_load_include('inc', 'advagg', 'advagg.cache'); + // Flush caches. + advagg_flush_all_cache_bins(); + + // Remove variables. + db_delete('variable') + ->condition('name', 'advagg%', 'LIKE') + ->execute(); + + // Remove all files and directories. + file_unmanaged_delete_recursive($css_path[0]); + file_unmanaged_delete_recursive($js_path[0]); + + // Make sure the theme_registry: cid is cleared. + register_shutdown_function('cache_clear_all', 'theme_registry:', 'cache', TRUE); +} + +/** + * Implements hook_schema(). + */ +function advagg_schema() { + // Create cache tables. + $schema['cache_advagg_aggregates'] = drupal_get_schema_unprocessed('system', 'cache'); + $schema['cache_advagg_aggregates']['fields']['cid']['binary'] = TRUE; + $schema['cache_advagg_aggregates']['description'] = 'Cache table for Advanced CSS/JS Aggregation. Used to keep a cache of the CSS and JS HTML tags.'; + + $schema['cache_advagg_info'] = drupal_get_schema_unprocessed('system', 'cache'); + $schema['cache_advagg_info']['fields']['cid']['binary'] = TRUE; + $schema['cache_advagg_info']['description'] = 'Cache table for Advanced CSS/JS Aggregation. Used to keep a cache of the db and file info.'; + + // Create database tables. + $schema['advagg_files'] = array( + 'description' => 'Files used in CSS/JS aggregation.', + 'fields' => array( + 'filename' => array( + 'description' => 'Path and filename of the file relative to Drupal webroot.', + 'type' => 'text', + 'size' => 'normal', + 'not null' => TRUE, + ), + 'filename_hash' => array( + 'description' => 'Hash of path and filename. Used to join tables.', + 'type' => 'char', + 'length' => 43, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'content_hash' => array( + 'description' => 'Hash of the file content. Used to see if the file has changed.', + 'type' => 'char', + 'length' => 43, + 'not null' => FALSE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'filetype' => array( + 'description' => 'Filetype.', + 'type' => 'varchar', + 'length' => 8, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + ), + 'filesize' => array( + 'description' => 'The file size in bytes.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'linecount' => array( + 'description' => 'The number of lines in the file.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'mtime' => array( + 'description' => 'The time the file was last modified.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'changes' => array( + 'description' => 'This is incremented every time a file changes.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'filesize_processed' => array( + 'description' => 'The file size in bytes after minification and compression.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'use_strict' => array( + 'description' => 'If 1 then the js file starts with "use strict";. If 0 then it does not.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'content_hash' => array('content_hash'), + 'filetype' => array('filetype'), + 'filesize' => array('filesize'), + 'use_strict' => array('use_strict'), + ), + 'primary key' => array('filename_hash'), + ); + + $schema['advagg_aggregates'] = array( + 'description' => 'What files are used in what aggregates.', + 'fields' => array( + 'aggregate_filenames_hash' => array( + 'description' => 'Hash of the aggregates list of files. Keep track of what files are in the aggregate.', + 'type' => 'char', + 'length' => 43, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'filename_hash' => array( + 'description' => 'Hash of path and filename.', + 'type' => 'char', + 'length' => 43, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'porder' => array( + 'description' => 'Processing order.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'settings' => array( + 'description' => 'Extra data about this file and how it is used in this aggregate.', + 'type' => 'blob', + 'not null' => TRUE, + 'size' => 'big', + 'translatable' => TRUE, + 'serialize' => TRUE, + ), + ), + 'indexes' => array( + 'porder' => array('porder'), + 'filename_hash' => array('filename_hash'), + 'aggregate_filenames_hash_porder' => array('aggregate_filenames_hash', 'porder'), + ), + 'primary key' => array('aggregate_filenames_hash', 'filename_hash'), + ); + + $schema['advagg_aggregates_versions'] = array( + 'description' => 'What files are used in what aggregates.', + 'fields' => array( + 'aggregate_filenames_hash' => array( + 'description' => 'Hash of the aggregates list of files. Keep track of what files are in the aggregate.', + 'type' => 'char', + 'length' => 43, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'aggregate_contents_hash' => array( + 'description' => 'Hash of all content_hashes in this aggregate. Simple Version control of the aggregate.', + 'type' => 'char', + 'length' => 43, + 'not null' => TRUE, + 'default' => '', + 'binary' => TRUE, + 'collation' => 'ascii_bin', + 'charset' => 'ascii', + 'mysql_character_set' => 'ascii', + ), + 'atime' => array( + 'description' => 'Last access time for this version of the aggregate. Updated every 12 hours.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'root' => array( + 'description' => 'If 1 then it is a root aggregate. 0 means not root aggregate.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'indexes' => array( + 'root' => array('root'), + 'atime' => array('atime'), + 'root_atime' => array( + 'root', + 'atime', + ), + ), + 'primary key' => array('aggregate_filenames_hash', 'aggregate_contents_hash'), + ); + + // Copy the variable table and change a couple of things. + $schema['advagg_aggregates_hashes'] = drupal_get_schema_unprocessed('system', 'variable'); + $schema['advagg_aggregates_hashes']['fields']['hash'] = $schema['advagg_aggregates_hashes']['fields']['name']; + $schema['advagg_aggregates_hashes']['fields']['hash']['length'] = 255; + $schema['advagg_aggregates_hashes']['fields']['hash']['description'] = 'The name of the hash.'; + $schema['advagg_aggregates_hashes']['fields']['hash']['binary'] = TRUE; + $schema['advagg_aggregates_hashes']['fields']['settings']['description'] = 'The settings associated with this hash.'; + $schema['advagg_aggregates_hashes']['fields']['settings'] = $schema['advagg_aggregates_hashes']['fields']['value']; + $schema['advagg_aggregates_hashes']['description'] = 'Key value pairs created by AdvAgg. Stores settings used at the time that the aggregate was created.'; + $schema['advagg_aggregates_hashes']['primary key'][0] = 'hash'; + unset($schema['advagg_aggregates_hashes']['fields']['name'], $schema['advagg_aggregates_hashes']['fields']['value']); + + return $schema; +} + +/** + * Upgrade AdvAgg previous versions (6.x-1.x and 7.x-1.x) to 7.x-2.x. + */ +function advagg_update_7200(&$sandbox) { + // Check and see if new tables exist. + $table_names = array_keys(advagg_schema()); + $all_tables_exist = TRUE; + foreach ($table_names as $table_name) { + if (!db_table_exists($table_name)) { + $all_tables_exist = FALSE; + } + } + // Bail if needed DB Tables exist. + if ($all_tables_exist) { + return t('Nothing needed to happen in Advanced CSS/JS Aggregation.'); + } + + // Remove all old advagg variables. + db_delete('variable') + ->condition('name', 'advagg%', 'LIKE') + ->execute(); + + // Remove old schema. + $tables_to_remove = array( + 'cache_advagg', + 'cache_advagg_files_data', + 'cache_advagg_bundle_reuse', + 'advagg_files', + 'advagg_bundles', + ); + foreach ($tables_to_remove as $table_to_remove) { + if (db_table_exists($table_to_remove)) { + db_drop_table($table_to_remove); + } + } + + // Install new schema. + drupal_install_schema('advagg'); + + return t('Upgraded Advanced CSS/JS Aggregation to 7.x-2.x.'); +} + +/** + * Remove Last-Modified Header from .htaccess to fix far future 304's. + */ +function advagg_update_7201(&$sandbox) { + return advagg_install_update_htaccess('Header set Last-Modified'); +} + +/** + * Remove the 480 week Far-Future code from .htaccess (violates RFC 2616 14.21). + */ +function advagg_update_7202(&$sandbox) { + return advagg_install_update_htaccess('290304000'); +} + +/** + * Add forcing of .js files to be application/javascript to follow RFC 4329 7.1. + */ +function advagg_update_7203(&$sandbox) { + return advagg_install_update_htaccess('', 'ForceType'); +} + +/** + * Remove empty temporary files left behind by AdvAgg. + */ +function advagg_update_7204(&$sandbox) { + // Make sure the advagg_get_root_files_dir() function is available. + drupal_load('module', 'advagg'); + + // Get the advagg paths. + $advagg_path = advagg_get_root_files_dir(); + + // Get the top level path. + $top_level = substr($advagg_path[0][0], 0, strpos($advagg_path[0][0], 'advagg_css')); + + // Start timer. + timer_start(__FUNCTION__); + + // Remove empty temp files from public://. + $files = file_scan_directory($top_level, '/file.*/', array( + 'recurse' => FALSE, + 'callback' => 'advagg_install_delete_empty_file_if_stale', + )); + + // Stop timer. + $time = timer_stop(__FUNCTION__); + $time = round($time['time'] / 1000, 4); + + // Output info. + if (count($files) > 0) { + return t('%count temporary files where removed in %time seconds', array( + '%count' => count($files), + '%time' => $time, + )); + } + else { + return t('Nothing needed to be done.'); + } +} + +/** + * Fix incorrect usage of ForceType in .htaccess from update 7203. + */ +function advagg_update_7205(&$sandbox) { + return t('First pattern results: !first Second pattern results: !second', array( + '!first' => advagg_install_update_htaccess('ForceType text/css .js'), + '!second' => advagg_install_update_htaccess('ForceType application/javascript .js'), + )); +} + +/** + * Update the schema making the varchar columns utf8_bin in MySQL. + */ +function advagg_update_7206(&$sandbox) { + $db_type = Database::getConnection()->databaseType(); + $tables_altered = array(); + if ($db_type === 'mysql') { + module_load_include('install', 'advagg', 'advagg'); + $schema = advagg_schema(); + $schema = array_keys($schema); + foreach ($schema as $table_name) { + $table_name = Database::getConnection()->prefixTables('{' . db_escape_table($table_name) . '}'); + $results = db_query("SHOW FULL FIELDS FROM $table_name")->fetchAllAssoc('Field'); + foreach ($results as $row) { + if (stripos($row->Type, 'varchar') !== FALSE && $row->Collation !== 'utf8_bin') { + db_query("ALTER TABLE $table_name MODIFY {$row->Field} {$row->Type} CHARACTER SET utf8 COLLATE utf8_bin"); + $tables_altered[$table_name][] = $row->Field; + } + } + } + } + + if (empty($tables_altered)) { + return t('Nothing needed to happen in AdvAgg.'); + } + + return t('The following columns inside of these database tables where converted to utf8_bin:
@data', array('@data' => print_r($tables_altered, TRUE))); +} + +/** + * Update schema making the varchar columns char. Change utf8_bin to ascii_bin. + */ +function advagg_update_7207(&$sandbox) { + $tables = array( + 'advagg_aggregates' => array( + 'aggregate_filenames_hash', + 'filename_hash', + ), + 'advagg_aggregates_versions' => array( + 'aggregate_filenames_hash', + 'aggregate_contents_hash', + ), + 'advagg_files' => array( + 'filename_hash', + 'content_hash', + ), + ); + + $schema = advagg_schema(); + foreach ($tables as $table => $fields) { + foreach ($fields as $field) { + // Change varchar to char. + db_change_field($table, $field, $field, $schema[$table]['fields'][$field]); + } + // Change utf8_bin to ascii_bin. + advagg_install_change_table_collation($table, $fields, 'ascii_bin', $schema[$table]['fields']); + } + return t('AdvAgg Tables converted from varchar to char and utf8_bin to ascii_bin.'); +} + +/** + * Add an index to the filename_hash column in the advagg_aggregates table. + */ +function advagg_update_7208(&$sandbox) { + if (!db_index_exists('advagg_aggregates', 'filename_hash')) { + db_add_index('advagg_aggregates', 'filename_hash', array('filename_hash')); + return t('Database index added to the filename_hash column of the advagg_aggregates table.'); + } + return t('Nothing needed to be done.'); +} + +/** + * Update schema making it match the definition. + */ +function advagg_update_7209(&$sandbox) { + $tables = array( + 'advagg_aggregates' => array( + 'aggregate_filenames_hash', + 'filename_hash', + ), + 'advagg_aggregates_versions' => array( + 'aggregate_filenames_hash', + 'aggregate_contents_hash', + ), + 'advagg_files' => array( + 'filename_hash', + 'content_hash', + ), + ); + + $schema = advagg_schema(); + foreach ($tables as $table => $fields) { + foreach ($fields as $field) { + // Change varchar to char. + db_change_field($table, $field, $field, $schema[$table]['fields'][$field]); + } + // Change utf8_bin to ascii_bin. + advagg_install_change_table_collation($table, $fields, 'ascii_bin', $schema[$table]['fields']); + } + return t('Database schema was adjusted to match what is listed in advagg_schema.'); +} + +/** + * Add filesize_processed field to advagg_files table. + */ +function advagg_update_7210() { + if (!db_field_exists('advagg_files', 'filesize_processed')) { + $spec = array( + 'description' => 'The file size in bytes after minification and compression.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ); + db_add_field('advagg_files', 'filesize_processed', $spec); + } + return t('The filesize_processed field has been added to the advagg_files table.'); +} + +/** + * Populate the filesize_processed field in the advagg_files table. + */ +function advagg_update_7211(&$sandbox) { + drupal_load('module', 'advagg'); + module_load_include('inc', 'advagg', 'advagg'); + $types = array('css', 'js'); + + // If first run of this update function then set progress variables. + if (!isset($sandbox['progress'])) { + $count = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filesize_processed', 0) + ->countQuery() + ->execute() + ->fetchField(); + $sandbox['progress'] = 0; + $sandbox['max'] = $count; + } + + // How many items should be processed per pass. + $limit = 20; + + foreach ($types as $type) { + $query = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filesize_processed', 0) + ->condition('filetype', $type) + ->range($sandbox['progress'], $limit) + ->execute(); + foreach ($query as $row) { + $row->filesize_processed = (int) advagg_generate_filesize_processed($row->filename, $type); + if (!empty($row->filesize_processed)) { + $write = (array) $row; + db_merge('advagg_files') + ->key(array( + 'filename_hash' => $write['filename_hash'], + )) + ->fields($write) + ->execute(); + } + } + } + + // Update our progress information. + $sandbox['progress'] += $limit; + // Set the value for finished. + $sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']); + + if ($sandbox['#finished']) { + return t('The filesize_processed field has been populated inside the advagg_files table.'); + } +} + +/** + * Add use_strict field to advagg_files table. + */ +function advagg_update_7212() { + if (!db_field_exists('advagg_files', 'use_strict')) { + $spec = array( + 'description' => 'If 1 then the js file starts with "use strict";. If 0 then it does not.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ); + db_add_field('advagg_files', 'use_strict', $spec); + } + if (!db_index_exists('advagg_files', 'use_strict')) { + db_add_index('advagg_files', 'use_strict', array('use_strict')); + } + return t('The use_strict field has been added to the advagg_files table.'); +} + +/** + * Populate the use_strict field in the advagg_files table. + */ +function advagg_update_7213(&$sandbox) { + drupal_load('module', 'advagg'); + module_load_include('inc', 'advagg', 'advagg'); + + // If first run of this update function then set progress variables. + if (!isset($sandbox['progress'])) { + $count = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filetype', 'js') + ->countQuery() + ->execute() + ->fetchField(); + $sandbox['progress'] = 0; + $sandbox['max'] = $count; + } + + // How many items should be processed per pass. + $limit = 10; + + $query = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filetype', 'js') + ->range($sandbox['progress'], $limit) + ->execute(); + foreach ($query as $row) { + $row->use_strict = (int) advagg_does_js_start_with_use_strict($row->filename); + if (!empty($row->use_strict)) { + $write = (array) $row; + db_merge('advagg_files') + ->key(array( + 'filename_hash' => $write['filename_hash'], + )) + ->fields($write) + ->execute(); + } + } + + // Update our progress information. + $sandbox['progress'] += $limit; + // Set the value for finished. + $sandbox['#finished'] = ($sandbox['progress'] >= $sandbox['max']) ? TRUE : ($sandbox['progress'] / $sandbox['max']); + + if ($sandbox['#finished']) { + return t('The use_strict field has been populated inside the advagg_files table.'); + } +} + +/** + * Update .htaccess to support brotli compression (br). + */ +function advagg_update_7214(&$sandbox) { + return advagg_install_update_htaccess('', 'brotli'); +} + +/** + * Update .htaccess to support brotli compression (br). + */ +function advagg_update_7215(&$sandbox) { + return advagg_install_update_htaccess('', '%{HTTP:Accept-encoding} gzip'); +} + +/** + * Update .htaccess to support brotli compression (br). + */ +function advagg_update_7216(&$sandbox) { + if ($GLOBALS['base_path'] !== '/') { + return advagg_install_update_htaccess('', 'ErrorDocument 404'); + } +} + +/** + * Update migrate the advagg_browser_dns_prefetch variable. + */ +function advagg_update_7217(&$sandbox) { + $advagg_browser_dns_prefetch = variable_get('advagg_browser_dns_prefetch', NULL); + $advagg_resource_hints_dns_prefetch = variable_get('advagg_resource_hints_dns_prefetch', NULL); + $advagg_resource_hints_location = variable_get('advagg_resource_hints_location', NULL); + variable_del('advagg_browser_dns_prefetch'); + if (empty($advagg_browser_dns_prefetch) + || !is_null($advagg_resource_hints_dns_prefetch) + || !is_null($advagg_resource_hints_location) + ) { + return t('Nothing needed to be done.'); + } + drupal_load('module', 'advagg'); + $config_path = advagg_admin_config_root_path(); + + if ($advagg_browser_dns_prefetch == 1) { + variable_set('advagg_resource_hints_location', 1); + variable_set('advagg_resource_hints_dns_prefetch', TRUE); + } + elseif ($advagg_browser_dns_prefetch == 2) { + variable_set('advagg_resource_hints_location', 3); + variable_set('advagg_resource_hints_dns_prefetch', TRUE); + } + else { + return t('Nothing happened.'); + } + return t('Old DNS Prefetch variable transferred to the new variable. Other options are under Resource Hints on the configuration page', array('@url' => url($config_path . '/advagg', array('fragment' => 'edit-resource-hints')))); +} + +/** + * Update the advagg .htaccess file fixing edge cases with the new rules. + */ +function advagg_update_7218(&$sandbox) { + return advagg_install_update_htaccess('', 'Options +FollowSymLinks'); +} + +/** + * Update the .htaccess file in the advagg directories adding immutable header. + */ +function advagg_update_7219(&$sandbox) { + return advagg_install_update_htaccess('', 'immutable'); +} + +/** + * Update the advagg_files table; use_strict column might have been incorrect. + */ +function advagg_update_7220() { + // Get all files that have use_strict marked. + $filenames = array(); + $query = db_select('advagg_files', 'af') + ->fields('af', array('filename', 'use_strict')) + ->condition('use_strict', 1) + ->execute(); + foreach ($query as $row) { + $filenames[] = $row->filename; + } + if (empty($filenames)) { + return t('Nothing needed to happen. Good to go!'); + } + + drupal_load('module', 'advagg'); + module_load_include('inc', 'advagg', 'advagg'); + + // Force change. + $info = advagg_get_info_on_files($filenames); + foreach ($info as &$value) { + $value['mtime']++; + } + advagg_insert_update_files($info, 'js'); + + // Fix changed record. + advagg_get_info_on_files($filenames); + advagg_insert_update_files($info, 'js'); + + // Detect changes. + $filenames_new = array(); + $query = db_select('advagg_files', 'af') + ->fields('af', array('filename', 'use_strict')) + ->condition('use_strict', 1) + ->execute(); + foreach ($query as $row) { + $filenames_new[] = $row->filename; + } + + // Output results. + if (count($filenames_new) == count($filenames_new)) { + return t('Nothing needed to happen. Good to go!'); + } + else { + return t('The advagg_files table has been updated; use_strict column has been updated.'); + } +} + +/** + * Add index to aggregate_filenames_hash and porder in advagg_aggregates table. + */ +function advagg_update_7221(&$sandbox) { + if (!db_index_exists('advagg_aggregates', 'aggregate_filenames_hash_porder')) { + db_add_index('advagg_aggregates', 'aggregate_filenames_hash_porder', array('aggregate_filenames_hash', 'porder')); + return t('Database index added to the aggregate_filenames_hash and porder column of the advagg_aggregates table.'); + } + return t('Nothing needed to be done.'); +} + +/** + * Run various checks that are fast. + * + * @param string $phase + * Can be install, update, or runtime. + * + * @return array + * An associative array. + */ +function advagg_install_fast_checks($phase = 'runtime') { + $requirements = array(); + // Ensure translations don't break at install time. + $t = get_t(); + + // Always check these, independent of the current phase. + $function_list = array( + 'rename', + ); + // Check each function to make sure it exists. + foreach ($function_list as $function_name) { + if (!function_exists($function_name)) { + $requirements['advagg_function_' . $function_name] = array( + 'title' => $t('Adv CSS/JS Agg - Function Disabled'), + 'value' => $phase === 'install' ? FALSE : $function_name, + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('%name() is disabled on this server. Please contact your hosting provider or server administrator and see if they can re-enable this function for you.', array( + '!url' => 'http://php.net/' . str_replace('_', '-', $function_name), + '%name' => $function_name, + )), + ); + } + } + // Check to see if any incompatible modules are installed. + if (module_exists('agrcache')) { + $requirements['advagg_module_agrcache'] = array( + 'title' => $t('Adv CSS/JS Agg - Aggregate cache module'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $phase === 'install' ? FALSE : $t('The Aggregate cache module is incompatible with AdvAgg.'), + 'description' => $t('You need to uninstall the agrcache module or uninstall AdvAgg.'), + ); + } + if (module_exists('bundle_aggregation')) { + $requirements['advagg_module_bundle_aggregation'] = array( + 'title' => $t('Adv CSS/JS Agg - Bundle aggregation module'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $phase === 'install' ? FALSE : $t('The Bundle aggregation module is incompatible with AdvAgg.'), + 'description' => $t('You need to uninstall the bundle_aggregation module or uninstall AdvAgg.'), + ); + } + if (module_exists('core_library')) { + $requirements['advagg_module_core_library'] = array( + 'title' => $t('Adv CSS/JS Agg - Core Library module'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $phase === 'install' ? FALSE : $t('The Core Library module is incompatible with AdvAgg.'), + 'description' => $t('You need to uninstall the core_library module or uninstall AdvAgg.'), + ); + } + + // If not at runtime, return here. + if ($phase !== 'runtime') { + return $requirements; + } + // Make sure the advagg default values for variable_get are available. + drupal_load('module', 'advagg'); + + // Do the following checks only at runtime. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $config_path = advagg_admin_config_root_path(); + + // Make sure directories are writable. + if (!file_prepare_directory($css_path[0], FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) { + $requirements['advagg_css_path_0_prepare_dir'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('CSS directory is not created or writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $css_path[0])), + ); + } + if (!is_writable($css_path[0])) { + $requirements['advagg_css_path_0_write'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('CSS directory is not writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $css_path[0])), + ); + } + if (!is_readable($css_path[0])) { + $requirements['advagg_css_path_0_read'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('CSS directory is not readable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $css_path[0])), + ); + } + $css_wrapper = file_stream_wrapper_get_instance_by_uri($css_path[0]); + if ($css_wrapper instanceof DrupalLocalStreamWrapper) { + if (!is_writable($css_path[1])) { + $requirements['advagg_css_path_1_write'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('CSS directory is not writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $css_path[1])), + ); + } + if (!is_readable($css_path[1])) { + $requirements['advagg_css_path_1_read'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('CSS directory is not readable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $css_path[1])), + ); + } + } + if (!file_prepare_directory($js_path[0], FILE_CREATE_DIRECTORY + FILE_MODIFY_PERMISSIONS)) { + $requirements['advagg_js_path_0_prepare_dir'] = array( + 'title' => $t('Adv CSS/JS Agg - JS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('JS directory is not created or writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $js_path[0])), + ); + } + if (!is_writable($js_path[0])) { + $requirements['advagg_js_path_0_write'] = array( + 'title' => $t('Adv CSS/JS Agg - JS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('JS directory is not writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $js_path[0])), + ); + } + if (!is_readable($js_path[0])) { + $requirements['advagg_js_path_0_read'] = array( + 'title' => $t('Adv CSS/JS Agg - JS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('JS directory is not readable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $js_path[0])), + ); + } + $js_wrapper = file_stream_wrapper_get_instance_by_uri($js_path[0]); + if ($js_wrapper instanceof DrupalLocalStreamWrapper) { + if (!is_writable($js_path[1])) { + $requirements['advagg_js_path_1_write'] = array( + 'title' => $t('Adv CSS/JS Agg - JS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('JS directory is not writable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $js_path[1])), + ); + } + if (!is_readable($js_path[1])) { + $requirements['advagg_js_path_1_read'] = array( + 'title' => $t('Adv CSS/JS Agg - JS Path'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('JS directory is not readable.'), + 'description' => $t('%path is not setup correctly.', array('%path' => $js_path[1])), + ); + } + } + + if (!variable_get('advagg_skip_enabled_preprocess_check', ADVAGG_SKIP_ENABLED_PREPROCESS_CHECK)) { + // Make sure variables are set correctly. + if (!variable_get('advagg_enabled', ADVAGG_ENABLED)) { + $requirements['advagg_not_on'] = array( + 'title' => $t('Adv CSS/JS Agg - Enabled'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Advanced CSS/JS aggregation is disabled.'), + 'description' => $t('Go to the Advanced CSS/JS aggregation settings page and enable it.', array('@settings' => url($config_path . '/advagg'))), + ); + } + if (!variable_get('preprocess_css', FALSE) || !variable_get('preprocess_js', FALSE)) { + $requirements['advagg_core_off'] = array( + 'title' => $t('Adv CSS/JS Agg - Core Variables'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Core CSS and/or JS aggregation is disabled.'), + 'description' => $t('"Optimize CSS files" and "Optimize JavaScript files" on the performance page should be enabled.', array('@performance' => url('admin/config/development/performance', array('fragment' => 'edit-bandwidth-optimization')))), + ); + } + } + + // Check that the menu router handler is working. + // Paths will vary based on s3fs no_rewrite_cssjs setting. + if (advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) { + // If using s3fs and no_rewrite_cssjs is not set, external paths are needed. + // Use $css_path[0] and $js_path[0] since they contain the scheme. + $menu_path_key = 0; + $menu_css_path = trim(parse_url(file_create_url($css_path[0] . '/test.css'), PHP_URL_PATH)); + if (strpos($menu_css_path, $GLOBALS['base_path']) === 0) { + $menu_css_path = substr($menu_css_path, strlen($GLOBALS['base_path'])); + } + $menu_js_path = trim(parse_url(file_create_url($js_path[0] . '/test.js'), PHP_URL_PATH)); + if (strpos($menu_js_path, $GLOBALS['base_path']) === 0) { + $menu_js_path = substr($menu_js_path, strlen($GLOBALS['base_path'])); + } + } + else { + // Determine paths if not using s3fs, or no_rewrite_cssjs is set. + // Use $css_path[1] and $js_path[1] since they are without schemes. + $menu_path_key = 1; + $menu_css_path = $css_path[1] . '/test.css'; + $menu_js_path = $js_path[1] . '/test.js'; + } + + // Use the paths set above to check menu router handler. + $advagg_async_generation_menu_issue = FALSE; + if (!file_uri_scheme($css_path[$menu_path_key])) { + $item_css = menu_get_item($menu_css_path); + if (empty($item_css['page_callback']) + || strpos($item_css['page_callback'], 'advagg') === FALSE + ) { + $advagg_async_generation_menu_issue = TRUE; + } + } + if (!file_uri_scheme($js_path[$menu_path_key])) { + $item_js = menu_get_item($menu_js_path); + if (empty($item_js['page_callback']) + || strpos($item_js['page_callback'], 'advagg') === FALSE + ) { + $advagg_async_generation_menu_issue = TRUE; + } + } + if ($advagg_async_generation_menu_issue) { + $requirements['advagg_async_generation_menu_issue'] = array( + 'title' => $t('Adv CSS/JS Agg - Async Mode'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Flush your caches.'), + 'description' => $t('You need to flush your menu cache. This can be done at the top of the performance page; under "Clear cache" press the "Clear all caches" button.', array( + '@performance' => url('admin/config/development/performance'), + )), + ); + } + + // Make hook_element_info_alter worked. + $styles_info = element_info('styles'); + $scripts_info = element_info('scripts'); + if (empty($styles_info['#pre_render']) + || !is_array($styles_info['#pre_render']) + || !in_array('advagg_modify_css_pre_render', $styles_info['#pre_render']) + || empty($scripts_info['#pre_render']) + || !is_array($scripts_info['#pre_render']) + || !in_array('advagg_modify_js_pre_render', $scripts_info['#pre_render']) + ) { + if (!empty($scripts_info['#group_callback']) && $scripts_info['#group_callback'] === 'omega_group_js') { + $requirements['advagg_hook_element_info_alter_omega'] = array( + 'title' => $t('Adv CSS/JS Agg - omega theme patch'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Omega theme needs a patch.'), + 'description' => $t('The patch can be found in this issue', array( + '@patch' => 'https://www.drupal.org/files/issues/omega-2492461-1-smarter-element-info-alter.patch', + '@issue' => 'https://www.drupal.org/node/2492461', + )), + ); + } + else { + $requirements['advagg_hook_element_info_alter'] = array( + 'title' => $t('Adv CSS/JS Agg - element_info'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Flush your caches.'), + 'description' => $t('You need to flush your cache_bootstrap cache bin as advagg_hook_element_info_alter() is not working correctly. This can be done near the top of the performance page under Clear cache.
Styles:

@styles

Scripts:

@scripts

', array( + '@performance' => url('admin/config/development/performance'), + '@styles' => print_r($styles_info, TRUE), + '@scripts' => print_r($scripts_info, TRUE), + )), + ); + } + } + + // Make sure some modules have the correct patches installed. + if (module_exists('css_emimage')) { + $file_path = drupal_get_path('module', 'css_emimage'); + if (!file_exists($file_path . '/css_emimage.advagg.inc')) { + $requirements['advagg_module_css_emimage_patch'] = array( + 'title' => $t('Adv CSS/JS Agg - CSS Embedded Images module'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('The CSS Embedded Images module needs to be updated.'), + 'description' => $t('CSS Embedded Images needs to be upgraded to version 1.3 or higher, the currently installed version is incompatible with AdvAgg.', array('@link' => 'http://drupal.org/project/css_emimage')), + ); + } + } + if (module_exists('labjs')) { + if (!function_exists('labjs_advagg_modify_js_pre_render_alter')) { + $requirements['advagg_module_labjs_patch'] = array( + 'title' => $t('Adv CSS/JS Agg - LAB.js module'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('The LAB.js module needs a patch to be compatible with AdvAgg.'), + 'description' => $t('You need to install the latest patch in this issue.', array('@link' => 'http://drupal.org/node/1977122')), + ); + } + } + + // Adjust some modules settings. + $search404_ignore_query = variable_get('search404_ignore_query', 'gif jpg jpeg bmp png'); + if (module_exists('search404') && + (strpos($search404_ignore_query, 'css') === FALSE + || strpos($search404_ignore_query, 'js') === FALSE + ) + ) { + $added_ext = array(); + if (strpos($search404_ignore_query, 'css') === FALSE) { + $added_ext[] = 'css'; + } + if (strpos($search404_ignore_query, 'js') === FALSE) { + $added_ext[] = 'js'; + } + $requirements['advagg_search404_module'] = array( + 'title' => $t('Adv CSS/JS Agg - Search 404 Settings'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP requests to advagg for css/js files may not be generating correctly.'), + 'description' => $t('The Search 404 module is enabled. You need to change the search404_ignore_query setting, also known as "Extensions to abort search" so advagg will work. Go to the Search 404 settings page under the "Advanced settings" fieldgroup and look for the "Extensions to abort search" setting. Add @code to the string that looks like this:

@old

so it will then look like this:

@new

', array( + '@config' => url('admin/config/search/search404'), + '@code' => ' ' . implode(' ', $added_ext), + '@old' => $search404_ignore_query, + '@new' => trim($search404_ignore_query) . ' ' . implode(' ', $added_ext), + )), + ); + } + + if (module_exists('securepages') && variable_get('securepages_enable', 0) && function_exists('securepages_match')) { + $test_css = securepages_match($css_path[1] . '/test.css'); + $test_js = securepages_match($js_path[1] . '/test.js'); + if ($test_css === 0 || $test_js === 0) { + $added_paths = array(); + $securepages_ignore = variable_get('securepages_ignore', ''); + if (strpos($securepages_ignore, $css_path[1]) === FALSE) { + $added_paths[] = $css_path[1] . '/*'; + } + if (strpos($securepages_ignore, $js_path[1]) === FALSE) { + $added_paths[] = $js_path[1] . '/*'; + } + if (!empty($added_paths)) { + $requirements['advagg_securepages_module'] = array( + 'title' => $t('Adv CSS/JS Agg - Secure Pages Settings'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Requests to advagg for css/js files may be getting redirected to http on a https page.'), + ); + if (!empty($securepages_ignore)) { + $requirements['advagg_securepages_module']['description'] = $t('The Secure Pages module is enabled. You need to change the securepages_ignore setting, also known as "Ignore pages" so advagg will work. Go to the Secure Pages settings page and under the "Ignore pages" setting add

!code

to the string that looks like this:

@old

so it will then look like this:

@old
!code

', array( + '@config' => url('admin/config/system/securepages', array('fragment' => 'edit-securepages-ignore')), + '!code' => implode("\n
", $added_paths), + '@old' => trim($securepages_ignore), + )); + } + else { + $requirements['advagg_securepages_module']['description'] = $t('The Secure Pages module is enabled. You need to change the securepages_ignore setting, also known as "Ignore pages" so advagg will work. Go to the Secure Pages settings page and under the "Ignore pages" setting add

!code

to that section.', array( + '@config' => url('admin/config/system/securepages', array('fragment' => 'edit-securepages-ignore')), + '!code' => implode("\n
", $added_paths), + )); + } + } + } + } + + // Check that https is correct. + if (empty($GLOBALS['is_https']) && + ((isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) === 'on') + || (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') + || (isset($_SERVER['HTTP_HTTPS']) && $_SERVER['HTTP_HTTPS'] === 'on') + ) + ) { + $requirements['advagg_is_https_check'] = array( + 'title' => $t('Adv CSS/JS Agg - HTTPS'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The core global $is_https is not TRUE.'), + 'description' => $t('You need to add in this logic near the top your settings.php file:
@code
', array( + '@code' => 'if ((isset($_SERVER[\'HTTPS\']) && strtolower($_SERVER[\'HTTPS\']) == \'on\') + || (isset($_SERVER[\'HTTP_X_FORWARDED_PROTO\']) && $_SERVER[\'HTTP_X_FORWARDED_PROTO\'] == \'https\') + || (isset($_SERVER[\'HTTP_HTTPS\']) && $_SERVER[\'HTTP_HTTPS\'] == \'on\') +) { + $_SERVER[\'HTTPS\'] = \'on\'; +}', + )), + ); + } + + // Make sure $base_url is correct. + // Site is https but $base_url starts with http://. + if (!empty($GLOBALS['is_https']) + && strpos($GLOBALS['base_url'], 'http://') === 0 + ) { + $requirements['advagg_is_https_check'] = array( + 'title' => $t('Adv CSS/JS Agg - $base_url'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The core global $base_url\'s scheme is incorrect.'), + 'description' => $t('You need to add in this logic near the bottom of your settings.php file:

@code

', array( + '@code' => 'if (isset($_SERVER["HTTPS"]) && strtolower($_SERVER["HTTPS"]) == "on" && isset($base_url)) { + $base_url = str_replace("http://", "https://", $base_url); +}', + )), + ); + } + + return $requirements; +} + +/** + * Implements hook_requirements(). + */ +function advagg_requirements($phase) { + $t = get_t(); + $requirements = advagg_install_fast_checks($phase); + // If not at runtime, return here. + if ($phase !== 'runtime') { + return $requirements; + } + + // Make sure outbound http requests will work. + $request = drupal_http_request('https://www.google.com/robots.txt', array('timeout' => 8)); + if (empty($request->data) || $request->code != 200) { + $requirements['advagg_drupal_http_request_failure'] = array( + 'title' => $t('Adv CSS/JS Agg - drupal_http_request test'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('An external request for https://www.google.com/robots.txt could not be fulfilled.'), + 'description' => $t('If drupal_http_request does not work, the tests that AdvAgg performs may not be accurate.'), + ); + } + + // Make sure http requests to advagg will work. + advagg_install_check_via_http($requirements); + + // Check that file writes happen without any errors. + if (empty($requirements)) { + module_load_include("missing.inc", "advagg"); + $current_hash = advagg_get_current_hooks_hash(); + $aggregate_settings = advagg_get_hash_settings($current_hash); + $types = array('css', 'js'); + foreach ($types as $type) { + $filename = $type . ADVAGG_SPACE . 'test_write' . REQUEST_TIME . '.' . $type; + $files = array('misc/farbtastic/farbtastic.' . $type => array()); + list($files_to_save, $errors) = advagg_save_aggregate($filename, $files, $type, $aggregate_settings); + foreach ($files_to_save as $uri => $data) { + @unlink($uri); + } + if (!empty($errors)) { + $requirements['advagg_file_write_error_' . $type] = array( + 'title' => $t('Adv CSS/JS Agg - File Write'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('File write had some issues with %type files.', array('%type' => $type)), + 'description' => $t('Most likely there is an issue with file and/or directory premissions. Error: @error', array( + '@error' => print_r($errors, TRUE), + )), + ); + } + } + } + + // If all requirements have been met, state advagg should be working. + if (empty($requirements)) { + $description = ''; + + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $description .= ' ' . $t('Currently running in development mode.'); + } + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 5) { + $aggressive_cache_conflicts = advagg_aggressive_cache_conflicts(); + if (empty($aggressive_cache_conflicts)) { + $description .= ' ' . $t('It appears that there are no incompatible modules, so you should be able to safely use the Aggressive cache. To adjust this setting, go to the AdvAgg: configuration page and under "AdvAgg Cache Settings" select Aggressive and then save.', array( + '@config' => url('admin/config/development/performance/advagg', array( + 'fragment' => 'edit-advagg-cache-level', + )), + )); + } + } + + $requirements['advagg_ok'] = array( + 'title' => $t('Adv CSS/JS Agg'), + 'severity' => REQUIREMENT_OK, + 'value' => $t('OK'), + 'description' => $t('Advanced CSS/JS Aggregator should be working correctly.') . ' ' . $description, + ); + } + + return $requirements; +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * Make sure http requests to css/js files work correctly. + * + * @param array $requirements + * Array of requirements used in hook_requirements(). + */ +function advagg_install_check_via_http(array &$requirements) { + // If other checks have not passed, do not test this. + if (!empty($requirements)) { + return; + } + + // Ensure translations don't break at install time. + $t = get_t(); + + // Setup some variables. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $types = array('css', 'js'); + $config_path = advagg_admin_config_root_path(); + + // Get s3fs no_rewrite_cssjs setting. + $s3fs_no_rewrite_cssjs = advagg_get_s3fs_config('no_rewrite_cssjs'); + + // Make sure we get an advagg fast 404. + $mod_url = FALSE; + if (!variable_get('maintenance_mode', FALSE) && !variable_get('advagg_skip_404_check', FALSE)) { + foreach ($types as $type) { + if ($type === 'css') { + $url_path = $css_path[0]; + $file_path = $css_path[1]; + } + elseif ($type === 'js') { + $url_path = $js_path[0]; + $file_path = $js_path[1]; + } + + // Set arguments for drupal_http_request(). + // Make a 404 request to the advagg menu callback. + $url = file_create_url($url_path . '/' . $type . ADVAGG_SPACE . REQUEST_TIME . '.' . $type); + $options = array('timeout' => 8); + + if (empty($url)) { + $filename_path = (!is_null($s3fs_no_rewrite_cssjs) && empty($s3fs_no_rewrite_cssjs)) ? $url_path : $file_path; + $filename = advagg_install_get_first_advagg_file($filename_path, $type); + $url = file_create_url($url_path . '/' . $filename); + $end = strpos($url, $filename); + if ($end !== FALSE) { + $url = substr($url, 0, $end) . $type . ADVAGG_SPACE . REQUEST_TIME . '.' . $type; + } + else { + $requirements['advagg_self_request'] = array( + 'title' => $t('Adv CSS/JS Agg - Self Request Failure'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('The uri: %url can not be converted to a url.', array('%url' => $url_path . '/' . $type . ADVAGG_SPACE . REQUEST_TIME . '.' . $type)), + 'description' => $t('If you are using a non default stream wrapper this might be the issue.'), + ); + continue; + } + } + + // Send request. + advagg_install_url_mod($url, $options, $mod_url); + $request = drupal_http_request($url, $options); + + // Try an alt URL if the request code is not positive. + if ($request->code < 0) { + $mod_url = TRUE; + advagg_install_url_mod($url, $options, $mod_url); + $new_request = drupal_http_request($url, $options); + + if ($new_request->code < 0) { + $description = ''; + if (!module_exists('httprl')) { + $description = t('Enabling the HTTP Parallel Request and Threading Library module might be able to fix this as AdvAgg will use HTTPRL to build the URL if it is enabled.', array('!httprl' => 'https://drupal.org/project/httprl')); + } + $requirements['advagg_self_request'] = array( + 'title' => $t('Adv CSS/JS Agg - Self Request Failure'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP loopback requests to this server are returning a non positive response code of %code', array('%code' => $new_request->code)), + 'description' => $t('If you have manually verified that AdvAgg is working correctly you can set the advagg_skip_404_check variable to TRUE in your settings.php. Editing the servers hosts file so the host name points to the localhost might also fix this (127.0.0.1 !hostname). To manually check go to @url, view the source (press ctrl+u on your keyboard) and check for this string @string. If that string is in the source, you can safely add this to your settings.php file @code', array( + '!hostname' => $_SERVER['HTTP_HOST'], + '@url' => $url, + '@string' => '', + '@code' => '$conf[\'advagg_skip_404_check\'] = TRUE;', + )) . ' ' . $description, + ); + // Skip the rest of the advagg checks as they will all fail. + $types = array(); + break; + } + else { + $request = $new_request; + } + } + + // Try request without https. + if ($request->code == 0 && stripos($request->error, 'Error opening socket ssl://') !== FALSE) { + $url = advagg_force_http_path($url); + $request = drupal_http_request($url, $options); + } + + // Try request to 127.0.0.1. + if ($request->code == 0 && stripos($request->error, 'getaddrinfo failed') !== FALSE) { + $parts = @parse_url($url); + if ($parts['host'] !== '127.0.0.1') { + $options['headers']['Host'] = $parts['host']; + $parts['host'] = '127.0.0.1'; + $url = advagg_glue_url($parts); + $request = drupal_http_request($url, $options); + } + } + + // Check response. Report an error if + // - Not a 404 OR + // - No data returned OR + // - Headers do not contain "x-advagg" AND + // - Body does not contain "advagg_missing_fast404". + if ($request->code != 404 + || empty($request->data) + || (empty($request->headers['x-advagg']) + && strpos($request->data, '') === FALSE + ) + ) { + // Fast 404 check. + $url_path_404 = parse_url($url, PHP_URL_PATH); + $exclude_paths = variable_get('404_fast_paths_exclude', FALSE); + $fast_404_html = variable_get('404_fast_html', '404 Not Found

Not Found

The requested URL "@path" was not found on this server.

'); + // Replace @path in the variable with the page path. + $fast_404_html = trim(strtr($fast_404_html, array('@path' => check_plain($url_path_404)))); + if (!empty($request->data) + && $fast_404_html == trim($request->data) + && !empty($exclude_paths) + && strpos($exclude_paths, 'advagg_') === FALSE + ) { + $pos_a = strpos($exclude_paths, '(?:styles)'); + $pos_b = strpos($exclude_paths, '(?:styles|'); + if ($exclude_paths === '/\/(?:styles)\//') { + $description = $t('Change it from %value to /\/(?:styles|advagg_(cs|j)s)\//', array( + '%value' => $exclude_paths, + )); + } + elseif ($pos_a !== FALSE) { + $description = $t('Change it from %value to %code ', array( + '%value' => $exclude_paths, + '%code' => str_replace('(?:styles)', '(?:styles|advagg_(cs|j)s)', $exclude_paths), + )); + } + elseif ($pos_b !== FALSE) { + $description = $t('Change it from %value to %code ', array( + '%value' => $exclude_paths, + '%code' => str_replace('(?:styles|', '(?:styles|advagg_(cs|j)s|', $exclude_paths), + )); + } + else { + $description = $t('Add in advagg_(cs|j)s into the regex. Current value: %value', array( + '%value' => $exclude_paths, + )); + } + $requirements['advagg_404_fast_' . $type . '_generation'] = array( + 'title' => $t('Adv CSS/JS Agg - Fast 404: HTTP Request'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'description' => $t('If you have fast 404 enabled in your settings.php file, you need to change the 404_fast_paths_exclude setting so advagg will work.') . ' ' . $description, + ); + } + elseif (module_exists('fast_404') + && defined('FAST_404_EXT_CHECKED') + && !in_array('/advagg_', variable_get('fast_404_string_whitelisting', array())) + && strpos(variable_get('fast_404_exts', '/^(?!robots).*\.(txt|png|gif|jpe?g|css|js|ico|swf|flv|cgi|bat|pl|dll|exe|asp)$/i'), $type) !== FALSE + ) { + $requirements['advagg_fast_404_module_' . $type . '_generation'] = array( + 'title' => $t('Adv CSS/JS Agg - Fast 404: HTTP Request'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'description' => $t('The fast 404 module is enabled. You need to change the fast_404_string_whitelisting setting so advagg will work. In your settings.php file add in the code below:
@code
', + array('@code' => '$conf[\'fast_404_string_whitelisting\'][] = \'/advagg_\';')), + ); + } + elseif (module_exists('stage_file_proxy') + && variable_get('stage_file_proxy_origin', NULL) + && strpos(advagg_file_get_contents(drupal_get_path('module', 'stage_file_proxy') . '/stage_file_proxy.module'), 'advagg') === FALSE + ) { + // Stage File Proxy patch is missing. + $requirements['advagg_stage_file_proxy_' . $type . '_generation'] = array( + 'title' => $t('Adv CSS/JS Agg - Fast 404: HTTP Request'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'description' => $t('If you have the Stage File Proxy module enabled, make sure this patch has been applied.', array( + '@patch' => 'https://drupal.org/node/1977170#comment-7331810', + '@module' => 'https://drupal.org/project/stage_file_proxy', + )), + ); + } + elseif (!variable_get('clean_url', 0)) { + $requirements['advagg_clean_url'] = array( + 'title' => $t('Adv CSS/JS Agg - Clean URLs'), + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('Go to the clean URL settings page and enable Clean URLs.', array( + '@settings' => url('admin/config/search/clean-urls'), + )), + ); + } + elseif ($request->code == 401) { + $requirements['advagg_set_user_pass'] = array( + 'title' => $t('Adv CSS/JS Agg - Set Basic Auth'), + 'value' => $t('HTTP requests to advagg for @type files are not getting through.', array('@type' => $type)), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('Authorization is required when accessing your site. In order to test that the @type files are working you will need to add the following code in your settings.php file:

@code1
@code2

filling in the correct username and password needed to access this site.', array( + '@code1' => '$conf[\'advagg_auth_basic_user\'] = \'\'; ', + '@code2' => '$conf[\'advagg_auth_basic_pass\'] = \'\';', + '@type' => $type, + )), + ); + } + elseif ($request->code == 403) { + $requirements['advagg_' . $type . '_server_permissions'] = array( + 'title' => $t('Adv CSS/JS Agg - Webserver can not access files'), + 'value' => $t('HTTP requests to advagg for @type files are not getting through.', array('@type' => $type)), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('Your webserver can not access @type advagg files. This is usually a server permissions issue. Raw request info:
@request
', array( + '@type' => $type, + '@request' => var_export($request, TRUE), + )), + ); + } + elseif (stripos($request->data, 'nginx')) { + $config_location = ''; + if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + $config_location = ' ' . $t('You might be able to find the nginx configuration file by running
@command_1
or
@command_2
', array( + '@command_1' => 'ps -o args -C nginx', + '@command_2' => 'nginx -t', + )); + } + $requirements['advagg_' . $type . '_nginx_config'] = array( + 'title' => $t('Adv CSS/JS Agg - Nginx not sending 404 to Drupal.'), + 'value' => $t('HTTP requests to advagg for @type files are not getting through.', array('@type' => $type)), + 'severity' => REQUIREMENT_ERROR, + 'description' => $t('Your nginx webserver is not sending 404s to drupal. Please make sure that your nginx configuration has something like this in it:

@code

Note that @drupal (last line of code above) might be @rewrite or @rewrites depending on your servers configuration. If there are image style rules in your Nginx configuration add this right below that. !config_location Raw request info:
@request
', array( + '@request' => var_export($request, TRUE), + '@code' => ' +### +### advagg_css and advagg_js support +### +location ~* files/advagg_(?:css|js)/ { + gzip_static on; + access_log off; + expires max; + add_header ETag ""; + add_header Cache-Control "max-age=31449600, no-transform, public"; + try_files $uri $uri/ @drupal; +}', + '!config_location' => $config_location, + )), + ); + } + elseif (!advagg_install_htaccess_errordocument($type)) { + $parsed_base_url = parse_url($GLOBALS['base_url']); + if (isset($parsed_base_url['scheme'])) { + unset($parsed_base_url['scheme']); + } + if ($type === 'css') { + $location = $css_path[1] . '/.htaccess'; + } + if ($type === 'js') { + $location = $js_path[1] . '/.htaccess'; + } + $requirements['advagg_' . $type . '_errordoc_404'] = array( + 'title' => $t('Adv CSS/JS Agg - HTTP Request'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through. The .htaccess needs to be rebuilt.'), + 'description' => $t('The .htaccess file generated by AdvAgg has the incorrect errordoc location. This can happen if Drush is used incorrectly or if the site has been moved to a different directory structure. If you are currently using drush this is how to access it correctly:

@drush

Odds are you will need to fix the errordoc location. Go to the AdvAgg: Operations page and under Regenerate .htaccess files press the Recreate htaccess files button. If you wish to manually edit the file go to the @htaccess_loc file and make sure the following line is in there near the top and any other ErrorDocument 404 statements have been removed.

@code

', array( + '@drush' => 'drush --root=' . DRUPAL_ROOT . '/ --uri=' . advagg_glue_url($parsed_base_url) . ' ', + '@url' => url($config_path . '/advagg/operations', array('fragment' => 'edit-htaccess')), + '@htaccess_loc' => $location, + '@code' => "ErrorDocument 404 {$GLOBALS['base_path']}index.php", + )), + ); + } + elseif (!is_null($s3fs_no_rewrite_cssjs) + && !empty($s3fs_no_rewrite_cssjs) + && !empty($request->headers['server']) + && $request->headers['server'] === 'AmazonS3' + ) { + $severity = REQUIREMENT_WARNING; + if (module_exists('httprl') && variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL)) { + $severity = REQUIREMENT_ERROR; + } + // S3 doesn't do origin pull. + $requirements['advagg_' . $type . '_generation'] = array( + 'title' => $t('Adv CSS/JS Agg - HTTP Request'), + 'severity' => $severity, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'description' => $t('AdvAgg will issue a request for a file that does not exist inside of the AdvAgg directory. If AdvAgg sends a 404, everything is ok; if something else sends a 404 then that means that AdvAgg will not be able to generate an aggregate if it is missing as something else is handling the 404 before AdvAgg has a chance to do it. If you are reading this, it means that something else is handling the 404 before AdvAgg can. In this case the s3fs Advanced Configuration Option "Don\'t render proxied CSS/JS file paths" should be disabled. Raw request info:
@request
', array( + '@request' => var_export($request, TRUE), + '@url' => url('admin/config/media/s3fs', array('fragment' => 'edit-s3fs-no-rewrite-cssjs')), + )), + ); + } + else { + // Menu callback failed. + $requirements['advagg_' . $type . '_generation'] = array( + 'title' => $t('Adv CSS/JS Agg - HTTP Request'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('HTTP requests to advagg for ' . $type . ' files are not getting through.'), + 'description' => $t('AdvAgg will issue a request for a file that does not exist inside of the AdvAgg directory. If AdvAgg sends a 404, everything is ok; if something else sends a 404 then that means that AdvAgg will not be able to generate an aggregate if it is missing as something else is handling the 404 before AdvAgg has a chance to do it. If you are reading this, it means that something else is handling the 404 before AdvAgg can. In some cases this can sometimes be a false report; go here: @url and check if the source (press ctrl+u on your keyboard) has an html comment that says "advagg_missing_fast404"; if it does, this is a false report, add this $conf[\'advagg_skip_404_check\'] = TRUE; to your settings.php file. Raw request info:
@request
', array( + '@request' => var_export($request, TRUE), + '@url' => $url, + )), + ); + } + } + } + } + elseif (variable_get('maintenance_mode', FALSE)) { + $requirements['advagg_maintenance_mode'] = array( + 'title' => $t('Adv CSS/JS Agg - HTTP Request'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t("HTTP requests to advagg's 404 handler can not be tested currently."), + 'description' => $t('This can not be tested while the site is in maintenance mode', array('@maintenance' => url('admin/config/development/maintenance'))), + ); + } + + // Check gzip encoding. + foreach ($types as $type) { + if ($type === 'css') { + $url_path = $css_path[0]; + $file_path = $css_path[1]; + } + elseif ($type === 'js') { + $url_path = $js_path[0]; + $file_path = $js_path[1]; + } + // Get filename. + $filename_path = (!is_null($s3fs_no_rewrite_cssjs) && empty($s3fs_no_rewrite_cssjs)) ? $url_path : $file_path; + $filename = advagg_install_get_first_advagg_file($filename_path, $type); + + // Skip if filename is empty. + if (empty($filename)) { + continue; + } + + $urls = array(); + $url = file_create_url($url_path . '/' . $filename); + if (empty($url)) { + continue; + } + $urls[] = $url; + if (module_exists('cdn')) { + // Get CDN defaults. + $blacklist = variable_get(CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE, CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_DEFAULT); + $auth_blacklist = variable_get(CDN_EXCEPTION_AUTH_USERS_BLACKLIST_VARIABLE, CDN_EXCEPTION_AUTH_USERS_BLACKLIST_DEFAULT); + // Set CDN blacklists to be empty. + $GLOBALS['conf'][CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE] = ''; + $GLOBALS['conf'][CDN_EXCEPTION_AUTH_USERS_BLACKLIST_VARIABLE] = ''; + // Create URL. + $urls[] = file_create_url($url_path . '/' . $filename); + // Set CDN blacklist back to the original value. + $GLOBALS['conf'][CDN_EXCEPTION_DRUPAL_PATH_BLACKLIST_VARIABLE] = $blacklist; + $GLOBALS['conf'][CDN_EXCEPTION_AUTH_USERS_BLACKLIST_VARIABLE] = $auth_blacklist; + } + $urls = array_unique($urls); + + // Set arguments for drupal_http_request(). + $options = array( + 'headers' => array( + 'Accept-Encoding' => 'gzip, deflate', + ), + 'version' => '1.0', + '#advagg_path' => "{$file_path}/{$filename}", + 'timeout' => 8, + ); + // Test http 1.0. + $old_requirements = $requirements; + advagg_install_chk_urls($requirements, $urls, $options, $mod_url, $type, $url_path, $file_path, $filename); + + // Test http 1.1 + // If Drupal version is >= 7.22 and httprl_override_core exists. + if (defined('VERSION') && floatval(VERSION) >= 7.22 && is_callable('httprl_override_core')) { + $old = variable_get('drupal_http_request_function', FALSE); + $GLOBALS['conf']['drupal_http_request_function'] = 'httprl_override_core'; + + // Only test 1.1; 1.0 is rarely used these days. + $requirements = $old_requirements; + + $options['version'] = '1.1'; + advagg_install_chk_urls($requirements, $urls, $options, $mod_url, $type, $url_path, $file_path, $filename); + + $GLOBALS['conf']['drupal_http_request_function'] = $old; + } + + if (function_exists('brotli_compress') + && defined('BROTLI_TEXT') + && variable_get('advagg_brotli', ADVAGG_BROTLI) + ) { + // Set arguments for drupal_http_request(). + $options = array( + 'headers' => array( + 'Accept-Encoding' => 'br', + ), + 'version' => '1.0', + 'timeout' => 8, + ); + // Test http 1.0. + $old_requirements = $requirements; + advagg_install_chk_urls($requirements, $urls, $options, $mod_url, $type, $url_path, $file_path, $filename); + + // Test http 1.1 + // If Drupal version is >= 7.22 and httprl_override_core exists. + if (defined('VERSION') && floatval(VERSION) >= 7.22 && is_callable('httprl_override_core')) { + $old = variable_get('drupal_http_request_function', FALSE); + $GLOBALS['conf']['drupal_http_request_function'] = 'httprl_override_core'; + + // Only test 1.1; 1.0 is rarely used these days. + $requirements = $old_requirements; + + $options['version'] = '1.1'; + advagg_install_chk_urls($requirements, $urls, $options, $mod_url, $type, $url_path, $file_path, $filename); + + $GLOBALS['conf']['drupal_http_request_function'] = $old; + } + } + } +} + +/** + * Make sure http requests to css/js files work correctly. + * + * @param array $requirements + * Array of requirements used in hook_requirements(). + * @param array $urls + * Array of urls. + * @param array $options + * Options array to pass to drupal_http_request(). + * @param bool $mod_url + * Set to TRUE if an alt URL was used. + * @param string $type + * String: css or js. + * @param string $url_path + * The url path to the file. + * @param string $file_path + * File path to the file. + * @param string $filename + * Name of the file. + */ +function advagg_install_chk_urls(array &$requirements, array $urls, array $options, $mod_url, $type, $url_path, $file_path, $filename) { + // Ensure translations don't break at install time. + $t = get_t(); + + list($css_path, $js_path) = advagg_get_root_files_dir(); + $config_path = advagg_admin_config_root_path(); + + $options += array( + 'timeout' => 8, + ); + + $is_apache = FALSE; + if (stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== FALSE || function_exists('apache_get_modules')) { + $is_apache = TRUE; + $mod_headers = advagg_install_apache_mod_loaded('mod_headers'); + $mod_rewrite = advagg_install_apache_mod_loaded('mod_rewrite'); + $mod_expires = advagg_install_apache_mod_loaded('mod_expires'); + } + foreach ($urls as $url) { + $key = strtolower(pathinfo($url, PATHINFO_EXTENSION)); + // Make sure the URL contains a schema. + if (strpos($url, 'http') !== 0) { + if ($GLOBALS['is_https']) { + $url = 'https:' . $url; + } + else { + $url = 'http:' . $url; + } + } + + // Before sending the request when using s3fs, check if the file exists. + if (module_exists('s3fs') && !file_exists($url_path . '/' . $filename)) { + if (module_exists('httprl') && variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL)) { + $httprl_message = 'This may be due to an issue with the HTTPRL module or its configuration. '; + } + else { + $httprl_message = ''; + } + $requirements['advagg_' . $type . '_missing' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - file does not exist'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Unable to find %type files.', array('%type' => $type)), + 'description' => $t('The AdvAgg database records and S3 files are not in sync. The file referenced in the database to perform a test cannot be found in the S3 file system. @httprl_messageFile URL:
@file
', array( + '@httprl_message' => $httprl_message, + '@file' => $url, + )), + ); + continue; + } + + // Send request. + advagg_install_url_mod($url, $options, $mod_url); + $request = drupal_http_request($url, $options); + $encoding_type = 'gzip'; + if (!empty($request->options['headers']['Accept-Encoding']) && strpos($request->options['headers']['Accept-Encoding'], 'br') !== FALSE) { + $encoding_type = 'br'; + } + + if (!variable_get('advagg_skip_gzip_check', ADVAGG_SKIP_GZIP_CHECK)) { + // Check response. Report an error if + // - Not a 200. + // - Headers do not contain "content-encoding". + // - content-encoding is not gzip, deflate or br. + if ($request->code != 200 + || empty($request->headers['content-encoding']) + || ($request->headers['content-encoding'] !== 'gzip' + && $request->headers['content-encoding'] !== 'deflate' + && $request->headers['content-encoding'] !== 'br' + ) + ) { + // Gzip failed. + if (!variable_get('advagg_gzip', ADVAGG_GZIP) + && $encoding_type === 'gzip' + ) { + // Recommend that gzip be turned on. + $requirements['advagg_' . $type . '_gzip' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - gzip'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Gzip is failing for %type files.', array('%type' => $type)), + 'description' => $t('Try enabling on the "Create .gz files" setting on the Advanced CSS/JS Aggregation Configuration page', array( + '@advagg' => url($config_path . '/advagg'), + '%type' => $type, + )), + ); + } + elseif (function_exists('brotli_compress') + && defined('BROTLI_TEXT') + && !variable_get('advagg_brotli', ADVAGG_BROTLI) + && $encoding_type === 'br' + ) { + // Recommend that br be turned on. + $requirements['advagg_' . $type . '_br' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - brotli'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Brotli is failing for %type files.', array('%type' => $type)), + 'description' => $t('Try enabling on the "Create .br files" setting on the Advanced CSS/JS Aggregation Configuration page', array( + '@advagg' => url($config_path . '/advagg'), + '%type' => $type, + )), + ); + } + else { + // If not apache skip this. + $apache_module_missing = FALSE; + if ($is_apache) { + if ($mod_headers === FALSE || $mod_rewrite === FALSE) { + $apache_module_missing = TRUE; + if ($mod_headers === FALSE) { + $requirements['advagg_mod_headers' . $key . '_' . $encoding_type] = array( + 'title' => $t('Adv CSS/JS Agg - Apache'), + 'description' => $t('The Apache module "mod_headers" is not available. Enable mod_headers for Apache if at all possible. This is causing @encoding to fail.', array( + '!link' => 'http://httpd.apache.org/docs/current/mod/mod_headers.html', + '@encoding' => $encoding_type, + )), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Apache module "mod_headers" is not installed.'), + ); + } + if ($mod_rewrite === FALSE) { + $requirements['advagg_mod_rewrite' . $key . '_' . $encoding_type] = array( + 'title' => $t('Adv CSS/JS Agg - Apache'), + 'description' => $t('The Apache module "mod_rewrite" is not available. You must enable mod_rewrite for Apache. This is causing @encoding to fail.', array( + '!link' => 'http://httpd.apache.org/docs/current/mod/mod_rewrite.html', + '@encoding' => $encoding_type, + )), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Apache module "mod_rewrite" is not installed.'), + ); + } + } + } + if (!$apache_module_missing) { + // Check via external service. + $ext_url = 'http://checkgzipcompression.com/?url=' . urlencode($url); + if ($encoding_type === 'br') { + $ext_url = 'https://tools.keycdn.com/brotli-query.php?url=' . urlencode($url) . '&public=0'; + } + $external_compression_request = drupal_http_request($ext_url, array( + 'timeout' => 7, + 'headers' => array('Connection' => 'close'), + )); + if (!empty($external_compression_request->data)) { + if ($encoding_type === 'br') { + if (stripos($external_compression_request->data, '') !== FALSE) { + preg_match("/(.*)<\/strong>/siU", $external_compression_request->data, $title_matches); + if (stripos($title_matches[1], 'Negative') === FALSE) { + $external_test_results = 1; + } + else { + $external_test_results = -1; + } + } + else { + $external_test_results = 0; + } + } + elseif (stripos($external_compression_request->data, '') !== FALSE) { + preg_match("/<title>(.*)<\/title>/siU", $external_compression_request->data, $title_matches); + if (stripos($title_matches[1], 'gzip') === FALSE) { + $external_test_results = 0; + } + elseif (stripos($title_matches[1], 'not gzip') === FALSE) { + $external_test_results = 1; + } + else { + $external_test_results = -1; + } + } + } + if (!isset($external_test_results) || $external_test_results !== 1) { + if ($request->code != 200) { + $rewritebase = advagg_htaccess_rewritebase(); + if (!empty($rewritebase)) { + if ($type === 'css') { + $rewritebase_advagg = advagg_htaccess_rewritebase($css_path[1]); + } + if ($type === 'js') { + $rewritebase_advagg = advagg_htaccess_rewritebase($js_path[1]); + } + } + $advagg_htaccess_rewritebase = variable_get('advagg_htaccess_rewritebase', ADVAGG_HTACCESS_REWRITEBASE); + if ($request->code == 307 && !empty($rewritebase) && empty($rewritebase_advagg) && empty($advagg_htaccess_rewritebase)) { + $requirements['advagg_' . $type . $encoding_type . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - @encoding', array('@encoding' => $encoding_type)), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('@encoding is failing for %type files.', array( + '%type' => $type, + '@encoding' => $encoding_type, + )), + 'description' => $t('The web server is not returning a 200, instead a @code is being returned. The RewriteBase option should be set on the <a href="@url">configuration page</a> under "Obscure Options" look for "AdvAgg RewriteBase Directive in .htaccess files". Raw request info: <pre>@request</pre>', array( + '@code' => $request->code, + '@encoding' => $encoding_type, + '@request' => print_r($request, TRUE), + '@url' => url($config_path . '/advagg', array('fragment' => 'edit-advagg-htaccess-rewritebase')), + )), + ); + } + else { + if (module_exists('s3fs') && ($request->code == 307 || $request->code == -2)) { + $requirements['advagg_' . $type . $encoding_type . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - Redirect Loop'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('S3fs issue. Proxy is not setup correctly for %type.', array( + '%type' => $type, + )), + 'description' => $t('The web server is not returning a 200, instead a @code is being returned. Apache proxy settings for httpd.conf: <p><code>!httpd</code></p>. Raw request info: <pre>@request</pre>', array( + '!httpd' => nl2br(str_replace(' ', '  ', htmlentities(advagg_install_s3fs_proxy_settings($type)))), + '@code' => $request->code, + '@encoding' => $encoding_type, + '@request' => print_r($request, TRUE), + )), + ); + } + else { + $requirements['advagg_' . $type . $encoding_type . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - @encoding', array('@encoding' => $encoding_type)), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('@encoding is failing for %type files.', array( + '%type' => $type, + '@encoding' => $encoding_type, + )), + 'description' => $t('The web server is not returning a 200, instead a @code is being returned. @encoding can not be tested. Raw request info: <pre>@request</pre>', array( + '@code' => $request->code, + '@encoding' => $encoding_type, + '@request' => print_r($request, TRUE), + )), + ); + } + } + } + elseif (empty($request->data)) { + $url = 'http://checkgzipcompression.com/?url=' . urlencode($url); + if ($encoding_type === 'br') { + $url = 'https://tools.keycdn.com/brotli-query.php?url=' . urlencode($url) . '&public=0'; + } + $requirements['advagg_' . $type . $encoding_type . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - @encoding', array('@encoding' => $encoding_type)), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('@encoding is failing for %type files.', array( + '%type' => $type, + '@encoding' => $encoding_type, + )), + 'description' => $t('No data was returned from your server; this @encoding test can not be done locally. Error: @error - @message You can manually test if @encoding is working by <a href="@urlGZ">going here</a> and seeing if @encoding is enabled. You can turn this warning off by adding this to your settings.php file: <code>@skipcode</code>', array( + '@error' => $request->code, + '@message' => isset($request->error) ? $request->error : '', + '@url' => $url, + '@skipcode' => '$conf[\'advagg_skip_gzip_check\'] = TRUE;', + '@encoding' => $encoding_type, + )), + ); + } + else { + // Recommend servers configuration be adjusted. + $request->data = '...'; + $requirements['advagg_' . $type . $encoding_type . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - @encoding', array('@encoding' => $encoding_type)), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('@encoding is failing for %type files.', array( + '%type' => $type, + '@encoding' => $encoding_type, + )), + 'description' => $t('The web servers configuration will need to be adjusted. In most cases make sure that the webroots .htaccess file still contains this section "Rules to correctly serve gzip compressed CSS and JS files". Also check in the <a href="@readme">readme</a>, under Troubleshooting. Certain default web server configurations (<a href="!nginx">nginx</a>) do not gzip HTTP/1.0 requests. If you are using cloudfront you will have to <a href="!cloudfront">add metadata</a> to the .gz files. There are some other options if using <a href="!so">cloudfront</a>. Raw request info: <pre>@request</pre>', array( + '!nginx' => 'http://www.cdnplanet.com/blog/gzip-nginx-cloudfront/', + '@request' => print_r($request, TRUE), + '!cloudfront' => 'http://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ServingCompressedFiles.html#CompressedS3', + '!so' => 'http://stackoverflow.com/questions/5442011/serving-gzipped-css-and-javascript-from-amazon-cloudfront-via-s3', + '@readme' => url(drupal_get_path('module', 'advagg') . '/README.txt'), + )), + ); + } + } + } + } + } + elseif ($request->code == 200 + && !empty($request->headers['content-encoding']) + && !empty($request->data) + && ($request->headers['content-encoding'] === 'gzip' + || $request->headers['content-encoding'] === 'deflate' + || $request->headers['content-encoding'] === 'br' + )) { + // Do the first level of decoding if not already done. + if (!isset($request->chunk_size)) { + if ($request->headers['content-encoding'] === 'gzip') { + $request->data = @gzinflate(substr($request->data, 10)); + } + elseif ($request->headers['content-encoding'] === 'deflate') { + $request->data = @gzinflate($request->data); + } + elseif ($request->headers['content-encoding'] === 'br' && is_callable('brotli_uncompress')) { + $request->data = @brotli_uncompress($request->data); + } + } + // Check for double gzip compression. + $contents = @file_get_contents($options['#advagg_path']); + if ($contents !== $request->data && (@gzinflate(substr($request->data, 10)) !== FALSE || @gzinflate($request->data) !== FALSE)) { + $config_path = advagg_admin_config_root_path(); + $description = ''; + if (variable_get('advagg_gzip', ADVAGG_GZIP)) { + $description .= $t('Go to the Advanced CSS/JS aggregation <a href="@settings">settings page</a>, under Obsucre Options uncheck the Create .gz files setting.', array( + '@settings' => url($config_path . '/advagg', array('fragment' => 'edit-obscure')), + )); + } + else { + $description .= $t('Your webserver configuration needs to be changed so that %type files are not being double compressed.', array( + '%type' => $type, + )); + if (isset($request->headers['content-type'])) { + $description .= ' ' . $t('The content type is: %type.', array( + '%type' => $request->headers['content-type'], + )); + } + } + $requirements['advagg_' . $type . '_gzip' . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - gzip'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Double gzip encoding detected for %type files.', array('%type' => $type)), + 'description' => $description, + ); + } + if ($contents !== $request->data && (is_callable('brotli_uncompress') && @brotli_uncompress($request->data) !== FALSE)) { + $config_path = advagg_admin_config_root_path(); + $description = ''; + if (variable_get('advagg_brotli', ADVAGG_BROTLI)) { + $description .= $t('Go to the Advanced CSS/JS aggregation <a href="@settings">settings page</a>, under Obsucre Options uncheck the Create .br files setting.', array( + '@settings' => url($config_path . '/advagg', array('fragment' => 'edit-obscure')), + )); + } + else { + $description .= $t('Your webserver configuration needs to be changed so that %type files are not being double compressed.', array( + '%type' => $type, + )); + if (isset($request->headers['content-type'])) { + $description .= ' ' . $t('The content type is: %type.', array( + '%type' => $request->headers['content-type'], + )); + } + } + $requirements['advagg_' . $type . '_br' . $key . $options['version']] = array( + 'title' => $t('Adv CSS/JS Agg - br'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('Double br encoding detected for %type files.', array('%type' => $type)), + 'description' => $description, + ); + } + } + } + $content_type = $type; + if ($content_type === 'js') { + $content_type = 'javascript'; + } + + if ($request->code == 200) { + $matches = array(); + if (!empty($request->headers['x-advagg']) + && preg_match('/Generated file at (\\d+)/is', $request->headers['x-advagg'], $matches) + && $matches[1] + 30 > REQUEST_TIME + ) { + if (!file_exists($file_path . '/' . $filename)) { + $requirements['advagg_' . $type . '_file_write_' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Can not write to the filesystem'), + 'severity' => REQUIREMENT_ERROR, + 'value' => $t('The request for a file was served by Drupal instead of the web server (like Apache).'), + 'description' => $t('Something is preventing writes to your filesystem from working.'), + ); + } + else { + $requirements['advagg_' . $type . '_loopback_issue' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Incorrect readings'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The request for a file was served by Drupal instead of the web server (like Apache).'), + 'description' => $t('It means that AdvAgg can not test for Far-Future headers internally and you will need to use an external tool in order to do so. Warnings given or not given about AdvAgg Expires, Cache-Control, and If-Modified-Since might be incorrect. In the <a href="@readme">readme</a>, under Troubleshooting try the Far-Future recommendations.', array('@readme' => url(drupal_get_path('module', 'advagg') . '/README.txt'))), + ); + } + } + + if ($type === 'css' + && (empty($request->headers['content-type']) + || strpos($request->headers['content-type'], 'text/' . $content_type) === FALSE + ) + ) { + // Recommend servers configuration be adjusted. + $requirements['advagg_' . $type . '_type' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Content-Type'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The wrong Content-Type is being sent by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. Was looking for <code>@typematch</code>, actually got <code>@received</code>.', array( + '@typematch' => 'text/' . $content_type, + '@received' => isset($request->headers['content-type']) ? $request->headers['content-type'] : 'NULL', + )), + ); + } + + if ($type === 'js' + && (empty($request->headers['content-type']) + || (strpos($request->headers['content-type'], 'application/' . $content_type) === FALSE + && strpos($request->headers['content-type'], 'application/x-' . $content_type) === FALSE + ) + ) + ) { + // Recommend servers configuration be adjusted. + $requirements['advagg_' . $type . '_type' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Content-Type'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The wrong Content-Type is being sent by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. Was looking for <code>@typematch</code>, actually got <code>@received</code>. You might need to apply the drupal core patch located here <a href="@url">@url</a>.', array( + '@url' => 'https://drupal.org/node/2193333#comment-8469991', + '@typematch' => 'application/' . $content_type, + '@received' => isset($request->headers['content-type']) ? $request->headers['content-type'] : 'NULL', + )), + ); + } + + // Test far future headers. + $apache_module_missing = FALSE; + if (!variable_get('advagg_skip_far_future_check', ADVAGG_SKIP_FAR_FUTURE_CHECK) + && empty($_SERVER['PANTHEON_ENVIRONMENT']) + && empty($_SERVER['PANTHEON_SITE_NAME']) + ) { + // Make sure the expires header is at least set to 1 month + // (2628000 seconds) in the future. + if (!empty($request->headers['expires']) + && strtotime($request->headers['expires']) < time() + 2628000 + ) { + // Recommend servers configuration be adjusted. + if ($is_apache && $mod_headers === FALSE) { + $apache_module_missing = TRUE; + } + else { + $requirements['advagg_' . $type . '_expires' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Expires'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The expires header being sent by your web server is not at least 1 month in the future.'), + 'description' => $t('The web servers configuration should be adjusted only for AdvAgg files. Was looking for a second counter over 2,628,000 (1 month), actually got <code>@received</code> (@expires). You can turn this warning off if you can not adjust this value (Pantheon) by adding this to your settings.php file: <code>@skipcode</code>', array( + '@received' => isset($request->headers['expires']) ? number_format(strtotime($request->headers['expires']) - REQUEST_TIME) : 'NULL', + '@expires' => isset($request->headers['expires']) ? $request->headers['expires'] : 'NULL', + '@skipcode' => '$conf[\'advagg_skip_far_future_check\'] = TRUE;', + )), + ); + } + } + // Make sure the cache-control header max age value is at least set to + // 1 month (2628000 seconds) in the future. + $matches = array(); + if (empty($request->headers['cache-control']) + || !preg_match('/\s*max-age\s*=\s*(\\d+)\s*/is', $request->headers['cache-control'], $matches) + || $matches[1] < 2628000 + ) { + // Recommend servers configuration be adjusted. + if ($is_apache && $mod_headers === FALSE) { + $apache_module_missing = TRUE; + } + else { + $requirements['advagg_' . $type . '_cache_control' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Cache-Control'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t("The cache-control's max-age header being sent by your web server is not at least 1 month in the future."), + 'description' => $t('The web servers configuration should be adjusted only for AdvAgg files. Was looking that the max-age second counter is over 2,628,000 (1 month), actually got <code>@received</code>. You can turn this warning off if you can not adjust this value (Pantheon) by adding this to your settings.php file: <code>@skipcode</code>', array( + '@received' => isset($request->headers['cache-control']) ? $request->headers['cache-control'] : 'NULL', + '@skipcode' => '$conf[\'advagg_skip_far_future_check\'] = TRUE;', + )), + ); + } + } + } + + // Handle missing apache modules. + if ($apache_module_missing && $is_apache && $mod_headers === FALSE) { + $requirements['advagg_mod_headers_far_future_headers_' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Apache'), + 'description' => $t('The Apache module "mod_headers" is not available. Enable <a href="!link">mod_headers</a> for Apache if at all possible. This is causing far-future headers to not be sent correctly.', array('!link' => 'http://httpd.apache.org/docs/current/mod/mod_headers.html')), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Apache module "mod_headers" is not installed.'), + ); + if ($mod_expires === FALSE) { + $requirements['advagg_mod_headers_far_future_expires_' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Apache'), + 'description' => $t('The Apache module "mod_expires" is not available. Enable <a href="!link">mod_expires</a> for Apache if at all possible. This is causing far-future headers to not be sent correctly.', array('!link' => 'http://httpd.apache.org/docs/current/mod/mod_expires.html')), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Apache module "mod_headers" is not installed.'), + ); + } + } + + // Test 304. + if (!variable_get('advagg_skip_304_check', ADVAGG_SKIP_304_CHECK) + && empty($_SERVER['PANTHEON_ENVIRONMENT']) + && empty($_SERVER['PANTHEON_SITE_NAME']) + ) { + $etag_works = FALSE; + if (isset($request->headers['etag'])) { + // Send an Etag header and see if the web server returns a 304. + $url = file_create_url($url_path . '/' . $filename); + $if_modified_options = $options; + $if_modified_options['headers'] = array( + 'Accept-Encoding' => 'gzip, deflate, br', + 'If-None-Match' => $request->headers['etag'], + ); + if (!empty($request->options['headers']['Accept-Encoding'])) { + $if_modified_options['headers']['Accept-Encoding'] = $request->options['headers']['Accept-Encoding']; + } + + // Send request. + advagg_install_url_mod($url, $if_modified_options, $mod_url); + $request_304 = drupal_http_request($url, $if_modified_options); + if ($request_304->code != 304) { + // Recommend servers configuration be adjusted. + $requirements['advagg_' . $type . '_304' . $key . '_etag'] = array( + 'title' => $t('Adv CSS/JS Agg - Etag'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The If-None-Match (Etag) header is being ignored by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. The server should have responded with a 304, instead a @code was returned.', array( + '@code' => $request_304->code, + )), + ); + } + else { + $etag_works = TRUE; + } + } + if (isset($request->headers['last-modified'])) { + // Send a If-Modified-Since header and see if the web server returns a + // 304. + $url = file_create_url($url_path . '/' . $filename); + $if_modified_options = $options; + $if_modified_options['headers'] = array( + 'Accept-Encoding' => 'gzip, deflate, br', + 'If-Modified-Since' => $request->headers['last-modified'], + ); + if (!empty($request->options['headers']['Accept-Encoding'])) { + $if_modified_options['headers']['Accept-Encoding'] = $request->options['headers']['Accept-Encoding']; + } + + // Send request. + advagg_install_url_mod($url, $if_modified_options, $mod_url); + $request_304 = drupal_http_request($url, $if_modified_options); + if ($request_304->code != 304) { + // Recommend servers configuration be adjusted. + $requirements['advagg_' . $type . '_304' . $key . '_last_modified'] = array( + 'title' => $t('Adv CSS/JS Agg - If-Modified-Since'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The If-Modified-Since (Last-Modified) header is being ignored by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. The server should have responded with a 304, instead a @code was returned.', array( + '@code' => $request_304->code, + )), + ); + } + elseif (isset($requirements['advagg_' . $type . '_304' . $key . '_etag'])) { + // Last-Modified works, Etag is broken. 304s are working. Don't warn + // user. + unset($requirements['advagg_' . $type . '_304' . $key . '_etag']); + } + } + if ($etag_works && isset($requirements['advagg_' . $type . '_304' . $key . '_last_modified'])) { + // Etag works, Last-Modified is broken. 304s are working. Don't warn + // user. + unset($requirements['advagg_' . $type . '_304' . $key . '_last_modified']); + } + + // Both the Last-Modified and Etag header are missing. + if (empty($request->headers['last-modified']) && empty($request->headers['etag'])) { + // Get path to advagg .htaccess file. + $files = array( + $file_path . '/.htaccess', + DRUPAL_ROOT . '/.htaccess', + ); + + // Check for bad .htaccess files. + $bad_config_found = FALSE; + foreach ($files as $count => $file) { + if (!file_exists($file)) { + continue; + } + $contents = advagg_file_get_contents($file); + if (strpos($contents, 'Header unset Last-Modified') !== FALSE) { + $bad_config_found = TRUE; + $requirements['advagg_' . $type . '_last-modified_' . $key . $count] = array( + 'title' => $t('Adv CSS/JS Agg - Last-Modified'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The Last-Modified header is not being sent out by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. The server should have sent out a Last-Modified header. Remove "Header unset Last-Modified" inside this file to fix the issue: @file', array('@file' => $file)), + ); + } + } + + // Recommend servers configuration be adjusted. + if (!$bad_config_found) { + $requirements['advagg_' . $type . '_last-modified_' . $key] = array( + 'title' => $t('Adv CSS/JS Agg - Last-Modified'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The Last-Modified header is not being sent out by your web server.'), + 'description' => $t('The web servers configuration will need to be adjusted. The server should have sent out a Last-Modified header and/or an Etag header. If you can not get the Last-Modified header to work, you can try using ETags. Inside <code>@htaccess</code> right before <code>@line1</code> at the bottom of the file add in <code>@line2</code>. You will also need to remove the <code>@line3</code> line further up.', array( + '@htaccess' => $file_path . '/.htaccess', + '@line1' => '</FilesMatch>', + '@line2' => 'FileETag MTime Size', + '@line3' => 'Header unset ETag', + )), + ); + } + } + } + } + } +} + +/** + * Given a advagg path this will return the first aggregate it can find. + * + * @param string $directory + * Path to the advagg css/js dir. + * @param string $type + * String: css or js. + * + * @return string + * Returns aggregate filename or an empty string on failure. + */ +function advagg_install_get_first_advagg_file($directory, $type) { + module_load_include('inc', 'advagg', 'advagg.missing'); + $filename = ''; + + // Get contents of the advagg directory. + $scanned_directory = @scandir($directory); + // Bailout here if the advagg directory is empty. + if (empty($scanned_directory)) { + // Get a file that will generate from the database. + return advagg_generate_advagg_filename_from_db($type); + } + // Filter list. + $blacklist = array('..', '.', '.htaccess', 'parts'); + $scanned_directory = array_diff($scanned_directory, $blacklist); + + // Make the last file empty. + $scanned_directory[] = ''; + + foreach ($scanned_directory as $key => $filename) { + // Skip if filename is not long enough. + $len = strlen($filename); + if ($len < 91 + strlen(ADVAGG_SPACE) * 3) { + continue; + } + + // See if this uri contains .gz near the end of it. + $pos = strripos($filename, '.gz', 91 + strlen(ADVAGG_SPACE) * 3); + if (!empty($pos)) { + // If this is a .gz file skip. + if ($pos == $len - 3) { + continue; + } + } + + $gzip_filename = $scanned_directory[$key + 1]; + $br_filename = $scanned_directory[$key + 1]; + if (function_exists('brotli_compress') + && defined('BROTLI_TEXT') + && variable_get('advagg_brotli', ADVAGG_BROTLI) + ) { + $gzip_filename = $scanned_directory[$key + 2]; + // Skip if the next file does not have a .br extension. + // This can occur if: + // - File is not .br compressed, or, + // - Using s3fs module and only .br compression is set. In + // this case, the advagg_advadgg_save_aggregate_alter() + // function will not add a file extension. + if (strcmp($filename . '.br', $br_filename) !== 0 + && (!module_exists('s3fs') + || (module_exists('s3fs') && variable_get('advagg_gzip', ADVAGG_GZIP)))) { + continue; + } + } + else { + // Skip if the next file is a .br file. + if (strcmp($filename . '.br', $br_filename) === 0) { + continue; + } + } + + if (variable_get('advagg_gzip', ADVAGG_GZIP)) { + // Skip if the next file does not have a .gz extension. + // This can occur if: + // - File is not .gz compressed, or, + // - Using s3fs module and either: + // - Only .gz compression option is set or, + // - Both .gz and .br compression options are set. In + // this case, the advagg_advagg_save_aggregate_alter() + // function creates a .gz file by default. + if (strcmp($filename . '.gz', $gzip_filename) !== 0 && !module_exists('s3fs')) { + continue; + } + } + else { + // Skip if the next file is a .gz file. + if (strcmp($filename . '.gz', $gzip_filename) === 0) { + continue; + } + } + + $data = advagg_get_hashes_from_filename(basename($filename)); + if (is_array($data)) { + list($type, $aggregate_filenames_hash, $aggregate_contents_hash) = $data; + + // Get a list of files. + $files = advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash); + + if (!empty($files)) { + // All checked passed above, break out of loop. + break; + } + } + } + + if (empty($filename)) { + return advagg_generate_advagg_filename_from_db($type); + } + return $filename; +} + +/** + * Modify $url and $options before making the HTTP request. + * + * @param string $url + * Full URL. + * @param array $options + * Options array for drupal_http_request(). + * @param bool $mod_url + * Set to TRUE to try and modify the $url variable. + */ +function advagg_install_url_mod(&$url, array &$options, $mod_url = FALSE) { + // Set the $options array. + // Set all timeouts to 8 seconds. + $options += array( + 'timeout' => 8, + 'dns_timeout' => 8, + 'connect_timeout' => 8, + 'ttfb_timeout' => 8, + ); + // Set connection to closed to prevent keep-alive from causing a timeout. + $options['headers']['Connection'] = 'close'; + // Set referrer to current page. + $options['headers']['Referer'] = $GLOBALS['base_root'] . request_uri(); + + $parts = @parse_url($url); + if (!is_array($parts)) { + return; + } + + // Check if this is a protocol relative url, if so add a artificial scheme so + // that advagg_glue_url() will produce a proper absolute url. That will work + // with drupal_http_request(). + if (!isset($parts['scheme']) && substr($url, 0, 2) == '//') { + global $base_url; + $parts['scheme'] = @parse_url($base_url, PHP_URL_SCHEME); + } + // Pass along user/pass in the URL. + $advagg_auth_basic_user = variable_get('advagg_auth_basic_user', ADVAGG_AUTH_BASIC_USER); + if (module_exists('shield')) { + $parts['user'] = variable_get('shield_user', ''); + $parts['pass'] = variable_get('shield_pass', ''); + } + elseif (isset($_SERVER['AUTH_TYPE']) + && $_SERVER['AUTH_TYPE'] == 'Basic' + ) { + $parts['user'] = $_SERVER['PHP_AUTH_USER']; + $parts['pass'] = $_SERVER['PHP_AUTH_PW']; + } + elseif (isset($_SERVER['HTTP_AUTHORIZATION']) + && strpos($_SERVER['HTTP_AUTHORIZATION'], 'Basic ') === 0 + ) { + $user_pass = explode(':', base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 5))); + $parts['user'] = $user_pass[0]; + $parts['pass'] = $user_pass[1]; + } + elseif (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION']) + && strpos($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 'Basic ') === 0 + ) { + $user_pass = explode(':', base64_decode(substr($_SERVER['REDIRECT_HTTP_AUTHORIZATION'], 5))); + $parts['user'] = $user_pass[0]; + $parts['pass'] = $user_pass[1]; + } + elseif (!empty($advagg_auth_basic_user)) { + $parts['user'] = $advagg_auth_basic_user; + $parts['pass'] = variable_get('advagg_auth_basic_pass', ADVAGG_AUTH_BASIC_PASS); + } + + if ($mod_url) { + if (function_exists('httprl_build_url_self')) { + // Remove the base_path from path. + $path = substr($parts['path'], strlen($GLOBALS['base_path'])); + $new_url = httprl_build_url_self($path); + } + else { + if (!empty($_SERVER['HTTP_HOST'])) { + if ($parts['host'] != $_SERVER['HTTP_HOST']) { + $parts['host'] = $_SERVER['HTTP_HOST']; + } + } + elseif (!empty($_SERVER['SERVER_NAME'])) { + if ($parts['host'] != $_SERVER['SERVER_NAME']) { + $parts['host'] = $_SERVER['SERVER_NAME']; + } + } + elseif (!empty($_SERVER['SERVER_ADDR'])) { + if ($parts['host'] != $_SERVER['SERVER_ADDR']) { + $parts['host'] = $_SERVER['SERVER_ADDR']; + } + } + else { + $parts['host'] = '127.0.0.1'; + } + } + } + + if (empty($new_url)) { + $new_url = advagg_glue_url($parts); + } + $url = $new_url; +} + +/** + * Checks to see if an apache module is enabled. + * + * @param string $mod + * Name of an apache module. + * + * @return bool + * TRUE if it exists; FALSE if it does not; NULL if it can not be determined. + */ +function advagg_install_apache_mod_loaded($mod) { + $sapi_type = php_sapi_name(); + if (substr($sapi_type, 0, 3) == 'cgi' || $sapi_type == 'fpm-fcgi') { + // NULL returned, apache_get_modules and phpinfo can not be called. + return NULL; + } + if (is_callable('apache_get_modules')) { + $mods = apache_get_modules(); + if (in_array($mod, $mods)) { + // Return TRUE, module exists. + return TRUE; + } + } + elseif (is_callable('phpinfo') && FALSE === strpos(ini_get('disable_functions'), 'phpinfo')) { + // Use static so we don't run phpinfo multiple times. + $phpinfo = &drupal_static(__FUNCTION__); + if (empty($phpinfo)) { + // Use phpinfo to get the info if apache_get_modules doesn't exist. + ob_start(); + phpinfo(8); + $phpinfo = ob_get_clean(); + } + if (FALSE !== strpos($phpinfo, $mod)) { + // Return TRUE, module exists. + return TRUE; + } + } + else { + // NULL returned, apache_get_modules and phpinfo can not be called. + return NULL; + } + return FALSE; +} + +/** + * Convert the table to the specified collation. + * + * @param string $table_name + * Perform the operation on this table. + * @param array $fields + * An array of field names. + * @param string $collation + * The db collation to change to table columns to. + * @param array $schema_fields + * An array of field definitions. + * + * @return array + * Returns an array of tables and column names. + */ +function advagg_install_change_table_collation($table_name, array $fields, $collation, array $schema_fields) { + $db_type = Database::getConnection()->databaseType(); + // Skip if not MySQL. + if ($db_type !== 'mysql') { + return FALSE; + } + $table_name = Database::getConnection()->prefixTables('{' . db_escape_table($table_name) . '}'); + $results = db_query("SHOW FULL FIELDS FROM $table_name")->fetchAllAssoc('Field'); + $db_schema = Database::getConnection()->schema(); + foreach ($results as $row) { + if (!in_array($row->Field, $fields)) { + continue; + } + + $charset = strtolower(substr($collation, 0, strpos($collation, '_'))); + $query = "ALTER TABLE $table_name CHANGE `{$row->Field}` `{$row->Field}` {$row->Type}"; + $query .= " CHARACTER SET $charset COLLATE $collation"; + + if (isset($schema_fields[$row->Field]['not null'])) { + if ($schema_fields[$row->Field]['not null']) { + $query .= ' NOT NULL'; + } + else { + $query .= ' NULL'; + } + } + + // $schema_fields[$row->Field]['default'] can be NULL, so we explicitly + // check for the key here. + if (isset($schema_fields[$row->Field]) + && is_array($schema_fields[$row->Field]) + && array_key_exists('default', $schema_fields[$row->Field]) + ) { + $default = $schema_fields[$row->Field]['default']; + if (is_string($default)) { + $default = "'" . $default . "'"; + } + elseif (!isset($default)) { + $default = 'NULL'; + } + $query .= ' DEFAULT ' . $default; + } + + if (empty($schema_fields[$row->Field]['not null']) && !isset($schema_fields[$row->Field]['default'])) { + $query .= ' DEFAULT NULL'; + } + + // Add column comment. + if (!empty($schema_fields[$row->Field]['description'])) { + $query .= ' COMMENT ' . $db_schema->prepareComment($schema_fields[$row->Field]['description'], 255); + } + + db_query($query); + } +} + +/** + * Callback to delete files if size == 0 and modified more than 60 seconds ago. + * + * @param string $uri + * Location of the file to check. + */ +function advagg_install_delete_empty_file_if_stale($uri) { + // Set stale file threshold to 60 seconds. + if (filesize($uri) < 3 && REQUEST_TIME - filemtime($uri) > 60) { + file_unmanaged_delete($uri); + } +} + +/** + * Callback to delete files if size == 0 and modified more than 60 seconds ago. + * + * @param string $has_string + * If the .htaccess file contains this string it will be removed and + * recreated. + * @param string $does_not_have_string + * If the .htaccess file does not contains this string it will be removed & + * recreated. + * + * @return string + * Translated string indicating what was done. + */ +function advagg_install_update_htaccess($has_string = '', $does_not_have_string = '') { + // Make sure the advagg_get_root_files_dir() function is available. + drupal_load('module', 'advagg'); + + // Get paths to .htaccess file. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $files['css'] = $css_path[0] . '/.htaccess'; + $files['js'] = $js_path[0] . '/.htaccess'; + + // Check for old .htaccess files. + $something_done = FALSE; + foreach ($files as $type => $uri) { + if (!file_exists($uri)) { + unset($files[$type]); + continue; + } + $contents = advagg_file_get_contents($uri); + // Remove old .htaccess file if it has this string. + if (!empty($has_string) && strpos($contents, $has_string) !== FALSE) { + drupal_unlink($uri); + $something_done = TRUE; + } + // Remove old .htaccess file if it does not have this string. + if (!empty($does_not_have_string) && strpos($contents, $does_not_have_string) === FALSE) { + drupal_unlink($uri); + $something_done = TRUE; + } + } + + // Create the new .htaccess file. + $new_htaccess = FALSE; + if (!empty($files) && $something_done && variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE)) { + // Make the advagg_htaccess_check_generate() function available. + module_load_include('inc', 'advagg', 'advagg.missing'); + foreach ($files as $type => $uri) { + advagg_htaccess_check_generate(array($uri => $type), $type, TRUE); + $new_htaccess = TRUE; + } + } + + // Output info. + if ($something_done) { + if ($new_htaccess) { + return t('Removed the old .htaccess file and put in a new one.'); + } + else { + return t('Removed the old .htaccess file.'); + } + } + else { + return t('Nothing needed to be done.'); + } +} + +/** + * See if the .htaccess file uses the RewriteBase directive. + * + * @param string $type + * Either css or js. + * + * @return bool + * FALSE if the ErrorDocument 404 statement is incorrect. + */ +function advagg_install_htaccess_errordocument($type) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + if ($type === 'css') { + $location = $css_path[1] . '/.htaccess'; + } + if ($type === 'js') { + $location = $js_path[1] . '/.htaccess'; + } + $good = TRUE; + + // Get the location of the 404 error doc. + if (is_readable($location)) { + $htaccess = advagg_file_get_contents($location); + $matches = array(); + $found = preg_match_all('/\\n\s*ErrorDocument\s*404\s*(.*)/i', $htaccess, $matches); + if ($found && !empty($matches[0])) { + $matches[1] = array_map('trim', $matches[1]); + $location = array_pop($matches[1]); + } + } + else { + return $good; + } + // If it's pointing to the wrong place or doesn't exist return FALSE. + if (!empty($location) && $location !== "{$GLOBALS['base_path']}index.php") { + $good = FALSE; + } + if (empty($location) && $GLOBALS['base_path'] !== '/') { + $good = FALSE; + } + + return $good; +} + +/** + * Create proxy settings. + * + * @return string + * Apache httpd.conf settings for the s3fs module. + */ +function advagg_install_s3fs_proxy_settings() { + list($css_path, $js_path) = advagg_get_root_files_dir(); + + $position = strpos($css_path[0], '://'); + $dir = ''; + if ($position !== FALSE) { + $dir = substr($css_path[0], $position + 3); + } + $css_target = str_replace($dir, '', file_create_url($css_path[0])); + $position = strpos($js_path[0], '://'); + $dir = ''; + if ($position !== FALSE) { + $dir = substr($js_path[0], $position + 3); + } + $js_target = str_replace($dir, '', file_create_url($js_path[0])); + $scheme = parse_url($js_target, PHP_URL_SCHEME); + + $config = ''; + $extra = ''; + if ($scheme === 'http') { + $port = '80'; + } + elseif ($scheme === 'https') { + $port = '443'; + $extra = " SSLProxyEngine on\n"; + } + $config .= "<VirtualHost *:$port>\n"; + $config .= " ProxyRequests Off\n"; + $config .= $extra; + $config .= " <Proxy *>\n"; + $config .= " Order deny,allow\n"; + $config .= " Allow from all\n"; + $config .= " </Proxy>\n"; + $config .= " ProxyTimeout 4\n"; + $config .= " ProxyPass {$GLOBALS['base_path']}s3fs-css/ $css_target\n"; + $config .= " ProxyPassReverse {$GLOBALS['base_path']}s3fs-css/ $css_target\n"; + $config .= " ProxyPass {$GLOBALS['base_path']}s3fs-js/ $js_target\n"; + $config .= " ProxyPassReverse {$GLOBALS['base_path']}s3fs-js/ $js_target\n"; + $config .= " ProxyErrorOverride On\n"; + $config .= " ErrorDocument 404 {$GLOBALS['base_path']}index.php\n"; + $config .= "</VirtualHost>\n"; + + return $config; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.make.example b/frontend/drupal/sites/all/modules/advagg/advagg.make.example new file mode 100644 index 000000000..1971310e1 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.make.example @@ -0,0 +1,50 @@ +; Rename this file to advagg.make to have it included within a drush make +; command. + +api = 2 +core = 7.x + +; Contrib module: libraries +; --------------------------------------- +projects[libraries][version] = "2" + +; JavaScript libraries +; --------------------------------------- + +; Library: CSSLint. +libraries[csslint][download][type] = git +libraries[csslint][download][url] = https://github.com/CSSLint/csslint + +; Library: fontfaceobserver. +libraries[fontfaceobserver][download][type] = git +libraries[fontfaceobserver][download][url] = https://github.com/bramstein/fontfaceobserver.git + +; Library: jshint. +libraries[jshint][download][type] = git +libraries[jshint][download][url] = https://github.com/jshint/jshint + +; Library: JShrink. +libraries[JShrink][download][type] = git +libraries[JShrink][download][url] = https://github.com/tedious/JShrink + +; Library: JSMinPlus. +libraries[jsminplus][download][type] = git +libraries[jsminplus][download][url] = https://github.com/JSMinPlus/JSMinPlus +libraries[jsminplus][directory_name] = "jsminplus" + +; Library: jspacker. +libraries[jspacker][download][type] = git +libraries[jspacker][download][url] = https://github.com/tholu/php-packer +libraries[jspacker][directory_name] = "jspacker" + +; Library: jsqueeze. +libraries[jsqueeze][download][type] = git +libraries[jsqueeze][download][url] = https://github.com/tchwork/jsqueeze + +; Library: loadCSS. +libraries[loadCSS][download][type] = git +libraries[loadCSS][download][url] = https://github.com/filamentgroup/loadCSS + +; Library: YUI-CSS-compressor-PHP-port. +libraries[YUI-CSS-compressor-PHP-port][download][type] = git +libraries[YUI-CSS-compressor-PHP-port][download][url] = https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.missing.inc b/frontend/drupal/sites/all/modules/advagg/advagg.missing.inc new file mode 100644 index 000000000..2bd1dddbd --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.missing.inc @@ -0,0 +1,1657 @@ +<?php + +/** + * @file + * Advanced CSS/JS aggregation module. + * + * Functions used to generate a file given the filename. + */ + +/** + * Menu Callback; generates a missing CSS/JS file. + */ +function advagg_missing_aggregate($input = '') { + // Do not stop processing this request. + ignore_user_abort(TRUE); + + // Generate missing file. + $msg = advagg_missing_generate($input); + + if (module_exists('jquery_update')) { + $arg = arg(); + $filename = array_pop($arg); + $filename = explode('?', $filename); + $filename = array_shift($filename); + if (strpos($filename, 'min.map') !== FALSE && strpos($filename, 'jquery') !== FALSE) { + // Get filename from request. + $wrong_pattern = t('Wrong pattern.'); + if ($msg === $wrong_pattern) { + $version = variable_get('jquery_update_jquery_version', '1.10'); + $trueversion = '1.9.1'; + switch ($version) { + case '1.9': + $trueversion = '1.9.1'; + break; + + case '1.10': + $trueversion = '1.10.2'; + break; + + case '1.11': + $trueversion = '1.11.2'; + break; + + case '2.1': + $trueversion = '2.1.4'; + break; + } + $url = "https://cdn.jsdelivr.net/gh/jquery/jquery@$trueversion/jquery.min.map"; + drupal_goto($url, array('external' => TRUE), 301); + } + } + } + + // If here send out fast 404. + advagg_missing_fast404($msg); +} + +/** + * Generates a missing CSS/JS file and send it to client. + * + * @return string + * text if bundle couldn't be generated. + */ +function advagg_missing_generate($input = '') { + // Make sure we are not in a redirect loop. + $redirect_counter = isset($_GET['redirect_counter']) ? intval($_GET['redirect_counter']) : 0; + if ($redirect_counter > 5) { + watchdog('advagg', 'This request could not generate correctly. Loop detected. Request data: %info', array('%info' => $_GET['q'])); + return t('In a loop.'); + } + + // Get filename from request. + $arg = arg(); + $filename = array_pop($arg); + $filename = explode('?', $filename); + $filename = array_shift($filename); + + // Quit ASAP if filename does not match the AdvAgg pattern. + $data = advagg_get_hashes_from_filename($filename); + if (is_string($data) || !is_array($data)) { + // Try again with the function input. + $filename = $input; + $data1 = advagg_get_hashes_from_filename($filename); + if (is_string($data) || !is_array($data)) { + return "$data $data1"; + } + else { + $data = $data1; + } + } + + // Check to see if the file exists. + list($css_path, $js_path) = advagg_get_root_files_dir(); + if ($data[0] === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($data[0] === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + + if (file_exists($uri) && filesize($uri) >= 0) { + // File does exist and filesize is bigger than zero, 307 to it. + $uri = advagg_generate_location_uri($filename, $data[0], $data[3]); + ++$redirect_counter; + $uri .= '?redirect_counter=' . $redirect_counter; + header('Location: ' . $uri, TRUE, 307); + exit(); + } + + // Get lock so only one process will do the work. + $lock_name = 'advagg_' . $filename; + $uri = $GLOBALS['base_path'] . $_GET['q']; + $created = FALSE; + $files_to_save = array(); + if (variable_get('advagg_no_locks', ADVAGG_NO_LOCKS)) { + $return = advagg_missing_create_file($filename, FALSE, $data); + if (!is_array($return)) { + return $return; + } + else { + list($files_to_save, $type) = $return; + $created = TRUE; + } + } + elseif (lock_acquire($lock_name, 10) || $redirect_counter > 4) { + if ($redirect_counter > 4) { + $return = advagg_missing_create_file($filename, TRUE, $data); + } + else { + $return = advagg_missing_create_file($filename, FALSE, $data); + } + lock_release($lock_name); + if (!is_array($return)) { + return $return; + } + else { + list($files_to_save, $type) = $return; + $created = TRUE; + } + } + else { + // Wait for another request that is already doing this work. + // We choose to block here since otherwise the router item may not + // be available in menu_execute_active_handler() resulting in a 404. + lock_wait($lock_name, 10); + if (file_exists($uri) && filesize($uri) > 0) { + $type = $data[0]; + $created = TRUE; + } + } + + // Redirect and try again on failure. + if (empty($created)) { + $uri = advagg_generate_location_uri($filename, $data[0], $data[3]); + ++$redirect_counter; + $uri .= '?redirect_counter=' . $redirect_counter; + header('Location: ' . $uri, TRUE, 307); + exit(); + } + + if ($redirect_counter > 4) { + watchdog('advagg', 'One of the alter hooks failed when generating this file: %uri. Thus this file was created without any alter hooks.', array('%uri' => $uri), WATCHDOG_ERROR); + } + + // Output file's contents if creating the file was successful. + // This function will call exit. + advagg_missing_send_saved_file($files_to_save, $uri, $created, $filename, $type, $redirect_counter, $data[3]); +} + +/** + * Given the filename, type, and settings, create absolute URL for 307 redirect. + * + * Due to url inbound alter we can not trust that the redirect will work if + * using $GLOBALS['base_path'] . $_GET['q']. Generate the uri as if it was + * going to be embedded in the html. + * + * @param string $filename + * Just the filename no path information. + * @param string $type + * String: css or js. + * @param array $aggregate_settings + * Array of settings. + * + * @return string + * String pointing to the URI of the file. + */ +function advagg_generate_location_uri($filename, $type, array $aggregate_settings = array()) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + if ($type === 'css') { + $uri_307 = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri_307 = $js_path[0] . '/' . $filename; + } + + if (empty($aggregate_settings)) { + $aggregate_settings = advagg_current_hooks_hash_array(); + } + + // 307s need to be absolute. RFC 2616 14.30. + $aggregate_settings['variables']['advagg_convert_absolute_to_relative_path'] = FALSE; + $aggregate_settings['variables']['advagg_convert_absolute_to_protocol_relative_path'] = FALSE; + + // Make advagg_context_switch available. + module_load_include('inc', 'advagg', 'advagg'); + advagg_context_switch($aggregate_settings, 0); + $uri_307 = advagg_file_create_url($uri_307, $aggregate_settings); + advagg_context_switch($aggregate_settings, 1); + + return $uri_307; +} + +/** + * Send the css/js file to the client. + * + * @param array $files_to_save + * Array of filenames and data. + * @param string $uri + * Requested filename. + * @param bool $created + * If file was created in a different thread. + * @param string $filename + * Just the filename no path information. + * @param string $type + * String: css or js. + * @param array $aggregate_settings + * Array of settings. Used to generate the 307 redirect location. + */ +function advagg_missing_send_saved_file(array $files_to_save, $uri, $created, $filename, $type, $redirect_counter, array $aggregate_settings = array()) { + // Send a 304 if this is a repeat request. + if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= REQUEST_TIME) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } + + $return_compressed_br = FALSE; + $return_compressed_gz = FALSE; + // Negotiate whether to use gzip compression. + if (!empty($_SERVER['HTTP_ACCEPT_ENCODING'])) { + if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'br') !== FALSE) { + $return_compressed_br = TRUE; + } + if (strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE) { + $return_compressed_gz = TRUE; + } + } + header('Vary: Accept-Encoding', FALSE); + + if (!empty($created)) { + if ($return_compressed_br && file_exists($uri . '.br') && filesize($uri . '.br') > 0) { + $uri .= '.br'; + } + elseif ($return_compressed_gz && file_exists($uri . '.gz') && filesize($uri . '.gz') > 0) { + $uri .= '.gz'; + } + if (!isset($files_to_save[$uri]) && file_exists($uri) && filesize($uri)) { + // Do not use advagg_file_get_contents here. + $files_to_save[$uri] = (string) @file_get_contents($uri); + } + } + + // Make sure zlib.output_compression does not compress the output. + ini_set('zlib.output_compression', '0'); + header('Vary: Accept-Encoding', FALSE); + // Clear the output buffer. + if (ob_get_level()) { + ob_end_clean(); + } + // Set generic far future headers. + if (variable_get('advagg_farfuture_php', ADVAGG_FARFUTURE_PHP)) { + advagg_missing_set_farfuture_headers(); + } + // Return compressed content if we can. + if ($return_compressed_br || $return_compressed_gz) { + foreach ($files_to_save as $uri => $data) { + // See if this uri contains .br near the end of it. + $pos = strripos($uri, '.br', 91 + strlen(ADVAGG_SPACE) * 3); + if (!empty($pos)) { + $len = strlen($uri); + if ($pos == $len - 3) { + // .br file exists, send it out. + header('Content-Encoding: br'); + break; + } + } + + // See if this uri contains .gz near the end of it. + $pos = strripos($uri, '.gz', 91 + strlen(ADVAGG_SPACE) * 3); + if (!empty($pos)) { + $len = strlen($uri); + if ($pos == $len - 3) { + // .gz file exists, send it out. + header('Content-Encoding: gzip'); + break; + } + } + } + } + else { + $data = trim(reset($files_to_save)); + } + + // Output file and exit. + if (!empty($data)) { + $strlen = strlen($data); + // Send a 304 if this is a repeat request. + if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { + $etags = explode(' ', $_SERVER['HTTP_IF_NONE_MATCH']); + if ($etags[0] < REQUEST_TIME + 31 * 24 * 60 * 60 + && isset($etags[1]) + && $etags[1] == $strlen + ) { + header("HTTP/1.1 304 Not Modified"); + exit(); + } + } + + // Send out a 200 OK status. + $default = ADVAGG_HTTP_200_CODE; + if (module_exists('httprl') + && variable_get('advagg_use_httprl', ADVAGG_USE_HTTPRL) + && (is_callable('httprl_is_background_callback_capable') + && httprl_is_background_callback_capable() + || !is_callable('httprl_is_background_callback_capable') + ) + ) { + // Use 203 instead of 200 if HTTPRL is being used. + $default = 203; + } + $number = variable_get('advagg_http_200_code', $default); + header("{$_SERVER['SERVER_PROTOCOL']} $number OK"); + + // Insure the Last-Modified header is set so 304's work correctly. + if (file_exists($uri) && $filemtime = @filemtime($uri)) { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', $filemtime)); + // Etags generation in php is broken due to millisecond precision for the + // files mtime; apache has it, php does not. + } + else { + header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T', REQUEST_TIME)); + } + // Set the Expires date 1 month into the future. + if (variable_get('advagg_farfuture_php', ADVAGG_FARFUTURE_PHP)) { + header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', REQUEST_TIME + 31 * 24 * 60 * 60)); + } + // Also send an etag out. + header('Etag: ' . REQUEST_TIME . ' ' . $strlen); + + if ($type === 'css') { + header("Content-Type: text/css"); + } + elseif ($type === 'js') { + header("Content-Type: application/javascript; charset=UTF-8"); + } + header('X-AdvAgg: Generated file at ' . REQUEST_TIME); + + print $data; + exit(); + } + else { + // Redirect and try again on failure. + $uri = advagg_generate_location_uri($filename, $type, $aggregate_settings); + ++$redirect_counter; + $uri .= '?redirect_counter=' . $redirect_counter; + header('Location: ' . $uri, TRUE, 307); + exit(); + } +} + +/** + * Set various headers so the browser will cache the file for a long time. + */ +function advagg_missing_set_farfuture_headers() { + // Hat tip to the CDN module for the far future headers. + // + // Browsers that implement the W3C Access Control specification might refuse + // to use certain resources such as fonts if those resources violate the + // same-origin policy. Send a header to explicitly allow cross-domain use of + // those resources. This is called Cross-Origin Resource Sharing, or CORS. + header("Access-Control-Allow-Origin: *"); + // Remove all previously set Cache-Control headers, because we're going to + // override it. Since multiple Cache-Control headers might have been set, + // simply setting a new, overriding header isn't enough: that would only + // override the *last* Cache-Control header. Yay for PHP! + if (function_exists('header_remove')) { + header_remove('Cache-Control'); + header_remove('ETag'); + header_remove('Set-Cookie'); + } + else { + header('Cache-Control:'); + header('Cache-Control:'); + header('ETag:'); + header('ETag:'); + header('Set-Cookie:'); + header('Set-Cookie:'); + } + // Set a far future Cache-Control header (52 weeks), which prevents + // intermediate caches from transforming the data and allows any + // intermediate cache to cache it, since it's marked as a public resource. + if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) { + header('Cache-Control: max-age=31449600, no-transform, public, immutable'); + } + else { + header('Cache-Control: max-age=31449600, no-transform, public'); + } +} + +/** + * Given a filename create that file. + * + * @param string $filename + * Just the filename no path information. + * @param bool $no_alters + * (optional) Set to TRUE to do the bare amount of processing on the file. + * @param mixed $data + * (optional) Output from advagg_get_hashes_from_filename(). + * + * @return mixed + * On failure a string saying why it failed. + * On success the $files_to_save array. + */ +function advagg_missing_create_file($filename, $no_alters = FALSE, $data = array()) { + // Option to still delever the file if fatal error. + register_shutdown_function("advagg_missing_fatal_handler", $filename); + + if (empty($data)) { + $data = advagg_get_hashes_from_filename($filename); + } + if (is_array($data)) { + list($type, $aggregate_filenames_hash, $aggregate_contents_hash, $aggregate_settings) = $data; + } + else { + return $data; + } + + if (empty($aggregate_settings)) { + $aggregate_settings = advagg_current_hooks_hash_array(); + } + + // Set no alters if this is the last chance of generating the aggregate. + if ($no_alters) { + $aggregate_settings['settings']['no_alters'] = TRUE; + } + + // Get a list of files. + $files = advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash); + if (empty($files)) { + return t('Hashes do not match database.'); + } + + // Save aggregate file. + list($files_to_save, $errors) = advagg_save_aggregate($filename, $files, $type, $aggregate_settings); + // Update atime. + advagg_multi_update_atime(array( + array( + 'aggregate_filenames_hash' => $aggregate_filenames_hash, + 'aggregate_contents_hash' => $aggregate_contents_hash, + ), + )); + // Make sure .htaccess file exists in the advagg dir. + if (variable_get('advagg_htaccess_check_generate', ADVAGG_HTACCESS_CHECK_GENERATE)) { + advagg_htaccess_check_generate($files_to_save, $type); + } + + // Return data. + return array( + $files_to_save, + $type, + $aggregate_filenames_hash, + $aggregate_contents_hash, + $aggregate_settings, + $files, + $errors, + ); +} + +/** + * Generate .htaccess rules and place them in advagg dir. + * + * @param array $files_to_save + * Array of files that where saved. + * @param string $type + * String: css or js. + * @param bool $force + * (Optional) force recreate the .htaccess file. + * + * @return array + * Empty array if not errors happened, list of errors if the write had any + * issues. + */ +function advagg_htaccess_check_generate(array $files_to_save, $type, $force = FALSE) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + + $content_type = $type; + if ($content_type === 'js') { + $content_type = 'javascript'; + $advagg_dir = basename($js_path[1]); + } + elseif ($content_type === 'css') { + $advagg_dir = basename($css_path[1]); + } + $type_upper = strtoupper($type); + $data = "\n"; + + // Some hosting companies do not allow "FollowSymLinks" but will support + // "SymLinksIfOwnerMatch". + if (variable_get('advagg_htaccess_symlinksifownermatch', ADVAGG_HTACCESS_SYMLINKSIFOWNERMATCH)) { + $data .= "Options +SymLinksIfOwnerMatch\n"; + } + else { + $data .= "Options +FollowSymLinks\n"; + } + if ($GLOBALS['base_path'] !== '/') { + $data .= "ErrorDocument 404 {$GLOBALS['base_path']}index.php\n"; + } + // See if RewriteBase is needed. + $advagg_htaccess_rewritebase = trim(variable_get('advagg_htaccess_rewritebase', ADVAGG_HTACCESS_REWRITEBASE)); + if (!empty($advagg_htaccess_rewritebase) && !empty($advagg_dir)) { + $rewrite_base_rule = str_replace('//', '/', $advagg_htaccess_rewritebase . '/' . $advagg_dir); + $data .= "RewriteBase $rewrite_base_rule\n"; + } + + $data .= "\n"; + $data .= "<IfModule mod_rewrite.c>\n"; + $data .= " RewriteEngine on\n"; + $data .= " <IfModule mod_headers.c>\n"; + $data .= " # Serve brotli compressed ${type_upper} files if they exist and the client accepts br.\n"; + $data .= " RewriteCond %{HTTP:Accept-encoding} br\n"; + $data .= " RewriteCond %{REQUEST_FILENAME}\.br -s\n"; + $data .= " RewriteRule ^(.*)\.${type} " . '$1' . "\.${type}\.br [QSA]\n"; + if ($type === 'css') { + $data .= " RewriteRule \.${type}\.br$ - [T=text/${content_type},E=no-gzip:1]\n"; + } + else { + $data .= " RewriteRule \.${type}\.br$ - [T=application/${content_type},E=no-gzip:1]\n"; + } + $data .= "\n"; + $data .= " <FilesMatch \"\.${type}\.br$\">\n"; + $data .= " # Serve correct encoding type.\n"; + $data .= " Header set Content-Encoding br\n"; + $data .= " # Force proxies to cache gzipped & non-gzipped css/js files separately.\n"; + $data .= " Header append Vary Accept-Encoding\n"; + $data .= " </FilesMatch>\n"; + $data .= "\n"; + $data .= " # Serve gzip compressed ${type_upper} files if they exist and the client accepts gzip.\n"; + $data .= " RewriteCond %{HTTP:Accept-encoding} gzip\n"; + $data .= " RewriteCond %{REQUEST_FILENAME}\.gz -s\n"; + $data .= " RewriteRule ^(.*)\.${type} " . '$1' . "\.${type}\.gz [QSA]\n"; + if ($type === 'css') { + $data .= " RewriteRule \.${type}\.gz$ - [T=text/${content_type},E=no-gzip:1]\n"; + } + else { + $data .= " RewriteRule \.${type}\.gz$ - [T=application/${content_type},E=no-gzip:1]\n"; + } + $data .= "\n"; + $data .= " <FilesMatch \"\.${type}\.gz$\">\n"; + $data .= " # Serve correct encoding type.\n"; + $data .= " Header set Content-Encoding gzip\n"; + $data .= " # Force proxies to cache gzipped & non-gzipped css/js files separately.\n"; + $data .= " Header append Vary Accept-Encoding\n"; + $data .= " </FilesMatch>\n"; + $data .= " </IfModule>\n"; + $data .= "</IfModule>\n"; + $data .= "\n"; + $data .= "<FilesMatch \"^${type}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}" . ADVAGG_SPACE . "[A-Za-z0-9-_]{43}.${type}(\.gz|\.br)?\">\n"; + $data .= " # No mod_headers. Apache module headers is not enabled.\n"; + $data .= " <IfModule !mod_headers.c>\n"; + $data .= " # No mod_expires. Apache module expires is not enabled.\n"; + $data .= " <IfModule !mod_expires.c>\n"; + $data .= " # Use ETags.\n"; + $data .= " FileETag MTime Size\n"; + $data .= " </IfModule>\n"; + $data .= " </IfModule>\n"; + $data .= "\n"; + $data .= " # Use Expires Directive if apache module expires is enabled.\n"; + $data .= " <IfModule mod_expires.c>\n"; + $data .= " # Do not use ETags.\n"; + $data .= " FileETag None\n"; + $data .= " # Enable expirations.\n"; + $data .= " ExpiresActive On\n"; + $data .= " # Cache all aggregated ${type} files for 52 weeks after access (A).\n"; + $data .= " ExpiresDefault A31449600\n"; + $data .= " </IfModule>\n"; + $data .= "\n"; + $data .= " # Use Headers Directive if apache module headers is enabled.\n"; + $data .= " <IfModule mod_headers.c>\n"; + $data .= " # Do not use etags for cache validation.\n"; + $data .= " Header unset ETag\n"; + $data .= " # Serve correct content type.\n"; + if ($type === 'css') { + $data .= " Header set Content-Type text/${content_type}\n"; + } + else { + $data .= " Header set Content-Type application/${content_type}\n"; + } + $data .= " <IfModule !mod_expires.c>\n"; + $data .= " # Set a far future Cache-Control header to 52 weeks.\n"; + if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) { + $data .= " Header set Cache-Control \"max-age=31449600, no-transform, public, immutable\"\n"; + } + else { + $data .= " Header set Cache-Control \"max-age=31449600, no-transform, public\"\n"; + } + $data .= " </IfModule>\n"; + $data .= " <IfModule mod_expires.c>\n"; + if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) { + $data .= " Header append Cache-Control \"no-transform, public, immutable\"\n"; + } + else { + $data .= " Header append Cache-Control \"no-transform, public\"\n"; + } + $data .= " </IfModule>\n"; + $data .= " </IfModule>\n"; + if ($type === 'css') { + $data .= " ForceType text/${content_type}\n"; + } + else { + $data .= " ForceType application/${content_type}\n"; + } + $data .= "</FilesMatch>\n"; + + $errors = array(); + foreach (array_keys($files_to_save) as $uri) { + $dir = dirname($uri); + $htaccess_file = $dir . '/.htaccess'; + if (!$force && file_exists($htaccess_file)) { + continue; + } + + $errors = advagg_save_data($htaccess_file, $data, $force); + } + return $errors; +} + +/** + * Given a filename return the type and 2 hashes. + * + * @param string $filename + * Just the filename no path information. + * @param bool $skip_hash_settings + * Allows for the skipping of db lookup for required file hooks. + * + * @return mixed + * On failure a string saying why it failed. + * On success array($ext, $aggregate_hash, $files_hash). + */ +function advagg_get_hashes_from_filename($filename, $skip_hash_settings = FALSE) { + // Verify requested filename has the correct pattern. + if (!advagg_match_file_pattern($filename)) { + return t('Wrong pattern.'); + } + + // Get the extension. + $ext = substr($filename, strpos($filename, '.', 131 + strlen(ADVAGG_SPACE) * 3) + 1); + + // Set extraction points. + if ($ext === 'css') { + $aggregate_filenames_start = 3 + strlen(ADVAGG_SPACE); + $aggregate_contents_start = 46 + strlen(ADVAGG_SPACE) * 2; + $hooks_hashes_start = 89 + strlen(ADVAGG_SPACE) * 3; + } + elseif ($ext === 'js') { + $aggregate_filenames_start = 2 + strlen(ADVAGG_SPACE); + $aggregate_contents_start = 45 + strlen(ADVAGG_SPACE) * 2; + $hooks_hashes_start = 88 + strlen(ADVAGG_SPACE) * 3; + } + else { + return t('Wrong file type.'); + } + + // Extract info from wanted filename. + $aggregate_filenames_hash = substr($filename, $aggregate_filenames_start, 43); + $aggregate_contents_hash = substr($filename, $aggregate_contents_start, 43); + $hooks_hashes_value = substr($filename, $hooks_hashes_start, 43); + + $aggregate_settings = array(); + if (!$skip_hash_settings) { + // Verify that the hooks hashes is valid. + $aggregate_settings = advagg_get_hash_settings($hooks_hashes_value); + if (empty($aggregate_settings)) { + if (!variable_get('advagg_weak_file_verification', ADVAGG_WEAK_FILE_VERIFICATION)) { + return t('Bad hooks hashes value.'); + } + elseif (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'File @filename has an empty aggregate_settings variable; the 3rd hash is incorrect.', array('@filename' => $filename), WATCHDOG_DEBUG); + } + } + } + + return array( + $ext, + $aggregate_filenames_hash, + $aggregate_contents_hash, + $aggregate_settings, + ); +} + +/** + * Get the files that belong inside of this aggregate. + * + * @param string $type + * String: css or js. + * @param string $aggregate_filenames_hash + * Hash of the groupings of files. + * @param string $aggregate_contents_hash + * Hash of the files contents. + * + * @return array + * List of files in the order they should be included. + */ +function advagg_get_files_from_hashes($type, $aggregate_filenames_hash, $aggregate_contents_hash) { + // Create main query for the advagg_aggregates_versions table. + $query = db_select('advagg_aggregates_versions', 'aav') + ->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash) + ->condition('aav.aggregate_contents_hash', $aggregate_contents_hash); + // Create join query for the advagg_aggregates table. + $subquery_aggregates = $query->join('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash = + aav.aggregate_filenames_hash AND aa.aggregate_filenames_hash = :aggregate_filenames_hash', + array(':aggregate_filenames_hash' => $aggregate_filenames_hash)); + // Create join query for the advagg_files table. + $query->join('advagg_files', 'af', 'af.filename_hash = aa.filename_hash AND + af.filetype = :type AND af.filesize > 0', array(':type' => $type)); + // Select fields and ordering of the query; add in query comment as well. + $query = $query->fields('af', array('filename')) + ->fields($subquery_aggregates, array('settings')) + ->orderBy('porder', 'ASC'); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + + // Add in files that are included in this aggregate. + $files = array(); + foreach ($results as $value) { + $files[$value->filename] = unserialize($value->settings); + } + + // Try again with weak file verification. + if (empty($files) && variable_get('advagg_weak_file_verification', ADVAGG_WEAK_FILE_VERIFICATION)) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Filehash @filename of type @type has an aggregate_contents_hash variable; the 2rd hash is incorrect.', array( + '@filename' => $aggregate_filenames_hash, + '@type' => $type, + ), WATCHDOG_DEBUG); + } + + // Create main query for the advagg_aggregates_versions table. + $query = db_select('advagg_aggregates_versions', 'aav') + ->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash); + // Create join query for the advagg_aggregates table. + $subquery_aggregates = $query->join('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash = + aav.aggregate_filenames_hash AND aa.aggregate_filenames_hash = :aggregate_filenames_hash', + array(':aggregate_filenames_hash' => $aggregate_filenames_hash)); + // Create join query for the advagg_files table. + $query->join('advagg_files', 'af', 'af.filename_hash = aa.filename_hash AND + af.filetype = :type AND af.filesize > 0', array(':type' => $type)); + // Select fields and ordering of the query; add in query comment as well. + $query = $query->fields('af', array('filename')) + ->fields($subquery_aggregates, array('settings')) + ->orderBy('porder', 'ASC'); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + + // Add in files that are included in this aggregate. + $files = array(); + foreach ($results as $value) { + $files[$value->filename] = unserialize($value->settings); + } + } + return $files; +} + +/** + * Given a list of files, grab their contents and glue it into one big string. + * + * @param array $files + * Array of filenames. + * @param array $aggregate_settings + * Array of settings. + * @param string $aggregate_filename + * Filename of the aggregeate. + * + * @return string + * String containing all the files. + */ +function advagg_get_css_aggregate_contents(array $files, array $aggregate_settings, $aggregate_filename = '') { + $write_aggregate = TRUE; + // Check if CSS compression is enabled. + $optimize = TRUE; + if (!empty($aggregate_settings['settings']['no_alters'])) { + $optimize = FALSE; + } + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $optimize = FALSE; + } + + module_load_include('inc', 'advagg', 'advagg'); + $info_on_files = advagg_load_files_info_into_static_cache(array_keys($files)); + + $data = ''; + if (!empty($files)) { + $media_changes = FALSE; + $last_media = NULL; + foreach ($files as $settings) { + if (!isset($settings['media'])) { + continue; + } + if (is_null($last_media)) { + $last_media = $settings['media']; + continue; + } + if ($settings['media'] !== $last_media) { + $media_changes = TRUE; + break; + } + } + if ($media_changes) { + $global_file_media = 'all'; + } + else { + $global_file_media = $last_media; + } + + // https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Media_queries + $media_types = array( + 'all', + 'aural', + 'braille', + 'handheld', + 'print', + 'projection', + 'screen', + 'tty', + 'tv', + 'embossed', + ); + + $import_statements = array(); + module_load_include('inc', 'advagg', 'advagg'); + $original_settings = array($optimize, $aggregate_settings); + foreach ($files as $file => $settings) { + $media_changes = FALSE; + if (!isset($settings['media'])) { + $settings['media'] = ''; + } + + if ($settings['media'] !== $global_file_media) { + $media_changes = TRUE; + } + + list($optimize, $aggregate_settings) = $original_settings; + // Allow other modules to modify aggregate_settings optimize. + // Call hook_advagg_get_css_file_contents_pre_alter(). + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_get_css_file_contents_pre', $file, $optimize, $aggregate_settings); + } + if (is_readable($file)) { + // Get the files contents. + $file_contents = (string) @advagg_file_get_contents($file); + // Get a hash of the file's contents. + $file_contents_hash = drupal_hash_base64($file_contents); + $cid = 'advagg:file:' . advagg_drupal_hash_base64($file); + if (empty($info_on_files[$cid]['content_hash'])) { + // If hash was not in the cache, get it from the DB. + $results = db_select('advagg_files', 'af') + ->fields('af', array('content_hash', 'filename_hash')) + ->condition('filename', $file) + ->execute(); + foreach ($results as $row) { + $info_on_files['advagg:file:' . $row->filename_hash]['content_hash'] = $row->content_hash; + } + } + if (isset($info_on_files[$cid]) == FALSE || $info_on_files[$cid]['content_hash'] !== $file_contents_hash) { + // If the content hash doesn't match don't write the file. + $write_aggregate = advagg_missing_file_not_readable($file, $aggregate_filename, FALSE); + } + $contents = advagg_load_css_stylesheet($file, $optimize, $aggregate_settings, $file_contents); + } + else { + // File is not readable. + $write_aggregate = advagg_missing_file_not_readable($file, $aggregate_filename, TRUE); + } + + // Allow other modules to modify this files contents. + // Call hook_advagg_get_css_file_contents_alter(). + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_get_css_file_contents', $contents, $file, $aggregate_settings); + } + + if ($media_changes) { + $media_blocks = advagg_parse_media_blocks($contents); + $contents = ''; + + $file_has_type = FALSE; + if (!empty($settings['media'])) { + foreach ($media_types as $media_type) { + if (stripos($settings['media'], $media_type) !== FALSE) { + $file_has_type = TRUE; + break; + } + } + } + + foreach ($media_blocks as $css_rules) { + if (strpos($css_rules, '@media') !== FALSE) { + // Get start and end of the rules for this media query block. + $start = strpos($css_rules, '{'); + if ($start === FALSE) { + continue; + } + $end = strrpos($css_rules, '}'); + if ($end === FALSE) { + continue; + } + + // Get current media queries for this media block. + $media_rules = substr($css_rules, 6, $start - 6); + // Get everything else besides top level media query. + $css_selectors_rules = substr($css_rules, $start + 1, $end - ($start + 1)); + + // Add in main media rule if needed. + if (!empty($settings['media']) + && strpos($media_rules, $settings['media']) === FALSE + && $settings['media'] !== $global_file_media + ) { + $rule_has_type = FALSE; + if ($file_has_type) { + foreach ($media_types as $media_type) { + if (stripos($media_rules, $media_type) !== FALSE) { + $rule_has_type = TRUE; + break; + } + } + } + if (!$rule_has_type) { + $media_rules = $settings['media'] . ' and ' . $media_rules; + } + } + } + else { + $media_rules = $settings['media']; + $css_selectors_rules = $css_rules; + } + $media_rules = trim($media_rules); + + // Pul all @font-face defentions inside the @media declaration above. + $font_face_string = ''; + $font_blocks = advagg_parse_media_blocks($css_selectors_rules, '@font-face'); + $css_selectors_rules = ''; + foreach ($font_blocks as $rules) { + if (strpos($rules, '@font-face') !== FALSE) { + $font_face_string .= "\n {$rules}"; + } + else { + $css_selectors_rules .= $rules; + } + } + $css_selectors_rules = str_replace("\n", "\n ", $css_selectors_rules); + $font_face_string = str_replace("\n", "\n ", $font_face_string); + + // Wrap css in dedicated media query if it differs from the global + // media query and there actually are media rules. + if (!empty($media_rules) && $media_rules !== $global_file_media) { + $output = "{$font_face_string} \n@media {$media_rules} {\n {$css_selectors_rules} \n}"; + } + else { + $output = "{$font_face_string} \n {$css_selectors_rules}"; + } + + $contents .= trim($output); + } + + } + // Per the W3C specification at + // http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import rules + // must proceed any other style, so we move those to the top. + $regexp = '/@import[^;]+;/i'; + preg_match_all($regexp, $contents, $matches); + $contents = preg_replace($regexp, '', $contents); + // Add the import statements with the media query of the current file. + $import_media = isset($settings['media']) ? $settings['media'] : ''; + $import_media = trim($import_media); + $import_statements[] = array($import_media, $matches[0]); + + // Close any open comment blocks. + $contents .= "\n/*})'\"*/\n"; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $contents .= "\n/* Above code came from $file */\n\n"; + } + + $data .= $contents; + } + + // Add import statements to the top of the stylesheet. + $import_string = ''; + foreach ($import_statements as $values) { + if ($media_changes) { + foreach ($values[1] as $statement) { + $import_string .= str_replace(';', $values[0] . ';', $statement); + } + } + else { + $import_string .= implode('', $values[1]); + } + } + $data = $import_string . $data; + } + + // Allow other modules to modify this aggregates contents. + // Call hook_advagg_get_css_aggregate_contents_alter(). + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_get_css_aggregate_contents', $data, $files, $aggregate_settings); + } + return array($data, $write_aggregate); +} + +/** + * Given a list of files, grab their contents and glue it into one big string. + * + * @param array $files + * Array of filenames. + * @param array $aggregate_settings + * Array of settings. + * @param string $aggregate_filename + * Filename of the aggregeate. + * + * @return string + * String containing all the files. + */ +function advagg_get_js_aggregate_contents(array $files, array $aggregate_settings, $aggregate_filename = '') { + $write_aggregate = TRUE; + $data = ''; + + module_load_include('inc', 'advagg', 'advagg'); + $info_on_files = advagg_load_files_info_into_static_cache(array_keys($files)); + + if (!empty($files)) { + // Build aggregate JS file. + foreach ($files as $filename => $settings) { + $contents = ''; + // Append a ';' and a newline after each JS file to prevent them from + // running together. Also close any comment blocks. + if (is_readable($filename)) { + $file_contents = (string) @advagg_file_get_contents($filename); + $file_contents_hash = drupal_hash_base64($file_contents); + $cid = 'advagg:file:' . advagg_drupal_hash_base64($filename); + if (empty($info_on_files[$cid]['content_hash'])) { + $results = db_select('advagg_files', 'af') + ->fields('af', array('content_hash', 'filename_hash')) + ->condition('filename', $filename) + ->execute(); + foreach ($results as $row) { + $info_on_files['advagg:file:' . $row->filename_hash]['content_hash'] = $row->content_hash; + } + } + if (isset($info_on_files[$cid]['content_hash']) && $info_on_files[$cid]['content_hash'] !== $file_contents_hash) { + // If the content hash doesn't match don't write the file. + $write_aggregate = advagg_missing_file_not_readable($filename, $aggregate_filename, FALSE); + } + + // Make sure that the file is ended properly. + $file_contents .= "\n;/*})'\"*/\n"; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $file_contents .= "/* Above code came from $filename */\n\n"; + } + $contents .= $file_contents; + } + else { + // File is not readable. + $write_aggregate = advagg_missing_file_not_readable($filename, $aggregate_filename, TRUE); + } + // Allow other modules to modify this files contents. + // Call hook_advagg_get_js_file_contents_alter(). + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_get_js_file_contents', $contents, $filename, $aggregate_settings); + } + // Make sure that the file is ended properly. + if (!empty($contents)) { + $contents .= ";/*})'\"*/\n"; + } + $data .= $contents; + } + } + + // Allow other modules to modify this aggregates contents. + // Call hook_advagg_get_js_aggregate_contents_alter(). + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_get_js_aggregate_contents', $data, $files, $aggregate_settings); + } + return array($data, $write_aggregate); +} + +/** + * Let other modules know that this file couldn't be found. + * + * @param string $filename + * Filename of the missing file. + * @param string $aggregate_filename + * Filename of the aggregate that is trying to be generated. + * @param bool $fs_read_failure + * Set to TRUE if the file system couldn't be read. + */ +function advagg_missing_file_not_readable($filename, $aggregate_filename = '', $fs_read_failure = FALSE) { + $write_aggregate = FALSE; + $config_path = advagg_admin_config_root_path(); + list($css_path, $js_path) = advagg_get_root_files_dir(); + + // Get cache of this report. + $cid = 'advagg:file_issue:' . drupal_hash_base64($filename); + $cache = cache_get($cid, 'cache_advagg_info'); + + // Let other modules know about this missing file. + // Call hook_advagg_missing_root_file(). + module_invoke_all('advagg_missing_root_file', $aggregate_filename, $filename, $cache); + + // Report to watchdog if this is not cached and it does not start in the + // public dir and the advagg dirs. + if (empty($cache) + && strpos($filename, 'public://') !== 0 + && strpos($filename, $css_path[1]) !== 0 + && strpos($filename, $js_path[1]) !== 0 + ) { + if ($fs_read_failure) { + watchdog('advagg', 'Reading from the file system failed. This can sometimes happen during a deployment and/or a clear cache operation. Filename: %file Aggregate Filename: %aggregate. If this continues to happen go to the <a href="@operations">Operations page</a> and under Drastic Measures - Reset the AdvAgg Files table click the Truncate advagg_files button.', array( + '%file' => $filename, + '%aggregate' => $aggregate_filename, + '@operations' => url('admin/config/development/performance/advagg/operations', array('fragment' => 'edit-reset-advagg-files')), + ), WATCHDOG_WARNING); + } + else { + watchdog('advagg', 'The content hash for %file does not match the stored content hash from the database. Please <a href="@url">flush the advagg cache</a> under Smart Cache Flush. This can sometimes happen during a deployment. Filename: %file Aggregate Filename: %aggregate', array( + '%file' => $filename, + '%aggregate' => $aggregate_filename, + '@url' => url($config_path . '/advagg/operations', array( + 'fragment' => 'edit-smart-flush', + )), + ), WATCHDOG_WARNING); + } + cache_set($cid, TRUE, 'cache_advagg_info', CACHE_TEMPORARY); + } + elseif (!empty($cache) && $cache->created < (REQUEST_TIME - variable_get('advagg_file_read_failure_timeout', ADVAGG_FILE_READ_FAILURE_TIMEOUT))) { + // Write the aggregate if it's been in a failure state for over 30 minutes. + $write_aggregate = TRUE; + } + return $write_aggregate; +} + +/** + * Save an aggregate given a filename, the files included in it, and the type. + * + * @param string $filename + * Just the filename no path information. + * @param array $files + * Array of filenames. + * @param string $type + * String: css or js. + * @param array $aggregate_settings + * Array of settings. + * + * @return array + * array($files_to_save, $errors). + */ +function advagg_save_aggregate($filename, array $files, $type, array $aggregate_settings = array()) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $uri = ''; + if ($type === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + + if (empty($aggregate_settings)) { + $aggregate_settings = advagg_current_hooks_hash_array(); + } + + // Allow other modules to alter the location, files included, and settings. + if (empty($aggregate_settings['settings']['no_alters'])) { + // Call hook_advagg_save_aggregate_pre_alter(). + drupal_alter('advagg_save_aggregate_pre', $uri, $files, $aggregate_settings); + } + + // Build the aggregates contents. + $contents = ''; + if ($type === 'css') { + list($contents, $write_aggregate) = advagg_get_css_aggregate_contents($files, $aggregate_settings, $filename); + } + elseif ($type === 'js') { + list($contents, $write_aggregate) = advagg_get_js_aggregate_contents($files, $aggregate_settings, $filename); + } + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $contents = "/* This aggregate contains the following files:\n" . implode(",\n", array_keys($files)) . ". */\n\n" . $contents; + } + + // List of files to save. + $files_to_save = array( + $uri => $contents, + ); + + // Allow other modules to alter the contents and add new files to save. + // Call hook_advagg_save_aggregate_alter(). + $other_parameters = array($files, $type); + if (empty($aggregate_settings['settings']['no_alters'])) { + drupal_alter('advagg_save_aggregate', $files_to_save, $aggregate_settings, $other_parameters); + } + + $errors = array(); + if ($write_aggregate) { + foreach ($files_to_save as $uri => $data) { + $errors = advagg_save_data($uri, $data); + if (!file_exists($uri) || filesize($uri) == 0) { + if ($type === 'css') { + $full_dir = DRUPAL_ROOT . '/' . $css_path[1]; + } + elseif ($type === 'js') { + $full_dir = DRUPAL_ROOT . '/' . $js_path[1]; + } + $free_space = @disk_free_space($full_dir); + if ($free_space !== FALSE && strlen($data) > $free_space) { + watchdog('advagg', 'Write to file system failed. Disk is full. %uri. !errors. %full_dir.', array( + '%uri' => $uri, + '!errors' => print_r($errors, TRUE), + '%full_dir' => $full_dir, + ), WATCHDOG_ALERT); + } + elseif (!is_writable($full_dir)) { + watchdog('advagg', 'Write to file system failed. Check directory permissions. %uri. !errors. %full_dir.', array( + '%uri' => $uri, + '!errors' => print_r($errors, TRUE), + '%full_dir' => $full_dir, + ), WATCHDOG_ERROR); + } + else { + watchdog('advagg', 'Write to file system failed. %uri. !errors. %full_dir.', array( + '%uri' => $uri, + '!errors' => print_r($errors, TRUE), + '%full_dir' => $full_dir, + ), WATCHDOG_ERROR); + } + // If the file is empty, remove it. Serving via drupal is better than an + // empty aggregate being served. + if (file_exists($uri) && filesize($uri) == 0) { + @unlink($uri); + } + } + } + } + return array($files_to_save, $errors); +} + +/** + * Save data to a file. + * + * This will use the rename operation ensuring atomic file operations. + * + * @param string $uri + * A string containing the destination location. This must be a stream wrapper + * URI. + * @param string $data + * A string containing the contents of the file. + * @param bool $overwrite + * (optional) Bool, set to TRUE to overwrite a file. + * + * @return array + * Empty array if not errors happened, list of errors if the write had any + * issues. + */ +function advagg_save_data($uri, $data, $overwrite = FALSE) { + $t = get_t(); + $errors = array(); + // Clear the stat cache. + module_load_include('inc', 'advagg', 'advagg'); + advagg_clearstatcache($uri); + + // Prepare dir if needed. + $dir = dirname($uri); + $dir_good = file_prepare_directory($dir, FILE_CREATE_DIRECTORY); + if (!$dir_good) { + $errors[1] = $t('The directory for @file can not be created or is not writable.', array('@file' => $uri)); + return $errors; + } + + // File already exists. + if (!$overwrite && file_exists($uri) && filesize($uri) > 0) { + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'File @uri exists and overwrite is false.', array('@uri' => $uri), WATCHDOG_DEBUG); + } + $errors[2] = $t('File (@file) already exits.', array('@file' => $uri)); + return $errors; + } + + // If data is empty, write a space. + if (empty($data)) { + $data = ' '; + } + + // Perform the replace operation. Since there could be multiple processes + // writing to the same file, the best option is to create a temporary file in + // the same directory and then rename it to the destination. A temporary file + // is needed if the directory is mounted on a separate machine; thus ensuring + // the rename command stays local and atomic. + // + // Get a temporary filename in the destination directory. + $dir = $uri_dir = drupal_dirname($uri) . '/'; + + // Corect the bug with drupal_tempnam where it doesn't pass subdirs to + // tempnam() if the dir is a stream wrapper. + $scheme = file_uri_scheme($uri_dir); + if ($scheme && file_stream_wrapper_valid_scheme($scheme)) { + $wrapper = file_stream_wrapper_get_instance_by_scheme($scheme); + if ($wrapper && method_exists($wrapper, 'getDirectoryPath')) { + $wrapper_dir_path = $wrapper->getDirectoryPath(); + if (!empty($wrapper_dir_path)) { + $dir = $wrapper_dir_path . '/' . substr($uri_dir, strlen($scheme . '://')); + $uri = $dir . substr($uri, strlen($uri_dir)); + } + } + } + + // Get the extension of the original filename and append it to the temp file + // name. Preserves the mime type in different stream wrapper implementations. + $parts = pathinfo($uri); + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Creating URI @uri', array('@uri' => $uri), WATCHDOG_DEBUG); + watchdog('advagg-debug', 'File Parts <pre>@parts</pre>', array('@parts' => print_r($parts, TRUE)), WATCHDOG_DEBUG); + } + $extension = '.' . $parts['extension']; + if ($extension === '.gz' || $extension === '.br') { + $parts = pathinfo($parts['filename']); + $extension = '.' . $parts['extension'] . $extension; + } + + // Create temp filename. + $temporary_file = $dir . 'advagg_file_' . drupal_hash_base64(microtime(TRUE) . mt_rand()) . $extension; + + // Save to temporary filename in the destination directory. + $filepath = file_unmanaged_save_data($data, $temporary_file, FILE_EXISTS_REPLACE); + + if ($filepath) { + // Perform the rename operation. + if (!advagg_rename($filepath, $uri)) { + // Unlink and try again for windows. Rename on windows does not replace + // the file if it already exists. + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Rename failed. @to', array('@to' => $uri), WATCHDOG_WARNING); + } + @unlink($uri); + // Remove temporary_file if rename failed. + if (!advagg_rename($filepath, $uri)) { + $errors[20] = $t('Renaming the filename (@incorrect) to (@correct) failed.', array('@incorrect' => $filepath, '@correct' => $uri)); + + @unlink($filepath); + if (file_exists($filepath)) { + $errors[22] = $t('unlinking @file failed.', array('@file' => $filepath)); + } + watchdog('advagg', 'Rename 4 failed. Current: %current Target: %target', array( + '%current' => $filepath, + '%target' => $uri, + ), WATCHDOG_ERROR); + } + } + + // Check the filesize. + $file_size = @filesize($uri); + $expected_size = _advagg_string_size_in_bytes($data); + if ($file_size === 0) { + // Zero byte file. + $errors[26] = $t('Write successful, but the file is empty. @file', array('@file' => $filepath)); + watchdog('advagg', 'Write successful, but the file is empty. Target: target. The empty file has been removed. If this error continues, performance will be greatly degraded.', array( + '%target' => $uri, + ), WATCHDOG_ERROR); + // Better to serve straight from Drupal than have a broken file. + @unlink($uri); + } + elseif ($file_size > 0 && $file_size != $expected_size) { + // Data written to disk doesn't match. + $errors[28] = $t('Write successful, but the file is the wrong size. @file Expected size is @expected_size, actual size is @file_size', array( + '@file' => $uri, + '@expected_size' => $expected_size, + '@file_size' => $file_size, + )); + watchdog('advagg', 'Write successful, but the file is the wrong size. %file Expected size is %expected_size, actual size is %file_size. The broken file has been removed. If this error continues, performance will be greatly degraded.', array( + '%file' => $uri, + '%expected_size' => $expected_size, + '%file_size' => $file_size, + ), WATCHDOG_ERROR); + // Better to serve straight from Drupal than have a broken file. + @unlink($uri); + } + } + else { + $errors[24] = $t('Write failed. @file', array('@file' => $temporary_file)); + watchdog('advagg', 'Write failed. Target: %target', array( + '%target' => $temporary_file, + ), WATCHDOG_ERROR); + } + // Cleanup leftover files. + if (file_exists($temporary_file)) { + @unlink($temporary_file); + } + if (file_exists($filepath)) { + @unlink($filepath); + } + return $errors; +} + +/** + * Given a string, what is the size that it should be as a file? + * + * @param string $string + * Input data to be sized in bytes. + * @link http://stackoverflow.com/a/3511239/231914. + * + * @return int + * Number of bytes this string uses. + */ +function _advagg_string_size_in_bytes($string) { + if (function_exists('mb_strlen')) { + return mb_strlen($string, '8bit'); + } + else { + return strlen($string); + } +} + +/** + * Rename; fallback to copy delete if this fails. + * + * @param string $source + * A string containing the source location. + * @param string $destination + * A string containing the destination location. + * + * @return mixed + * Destination string on success, FALSE on failure. + */ +function advagg_rename($source, $destination) { + $real_source = drupal_realpath($source); + $real_source = $real_source ? $real_source : $source; + $real_destination = drupal_realpath($destination); + $real_destination = $real_destination ? $real_destination : $destination; + + // Try php rename. + if (!@rename($real_source, $real_destination)) { + // Try drupal move. + if (!file_unmanaged_move($source, $destination)) { + // Try file scheme's rename method if it exists. + $fs_wrapper = file_stream_wrapper_get_instance_by_scheme(file_uri_scheme($source)); + if (!$fs_wrapper || !method_exists($fs_wrapper, 'rename') || !$fs_wrapper->rename($source, $destination)) { + return FALSE; + } + } + } + + return $destination; +} + +/** + * Send out a fast 404 and exit. + * + * @param string $msg + * (optional) Small message reporting why the file didn't get created. + */ +function advagg_missing_fast404($msg = '') { + drupal_page_is_cacheable(FALSE); + + // Strip new lines & separators and limit header message to 512 characters. + $msg = substr(preg_replace("/[^\w\. ]+/", "", $msg), 0, 512); + + // Add in headers if possible. + if (!headers_sent()) { + header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); + header('X-AdvAgg: Failed validation. ' . $msg); + } + + // Output fast 404 message and exit. + print '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' . "\n"; + print '<html xmlns="http://www.w3.org/1999/xhtml">'; + print '<head><title>404 Not Found'; + print '

Not Found

'; + print '

The requested URL was not found on this server.

'; + print '

Home

'; + print ''; + print ''; + exit(); +} + +/** + * Read the atime value for the given aggregate. + * + * @param string $aggregate_filenames_hash + * Hash of the groupings of files. + * @param string $aggregate_contents_hash + * Hash of the files contents. + * @param string $uri + * URI pointing to the aggregate file. + * + * @return mixed + * File atime or FALSE if not found. + */ +function advagg_get_atime($aggregate_filenames_hash, $aggregate_contents_hash, $uri) { + // Try to use the cache to avoid hitting the database with a select query. + $cache_id = 'advagg:db:' . $aggregate_filenames_hash . ADVAGG_SPACE . $aggregate_contents_hash; + $cache = cache_get($cache_id, 'cache_advagg_info'); + if ($cache) { + // If the atime in the cache is less than 12 hours old, use that. + if (!empty($cache->data['atime']) && $cache->data['atime'] > REQUEST_TIME - (12 * 60 * 60)) { + return $cache->data['atime']; + } + } + + // Try to get the atime from the DB. + $atime = db_select('advagg_aggregates_versions', 'aav') + ->fields('aav', array('atime')) + ->condition('aav.aggregate_filenames_hash', $aggregate_filenames_hash) + ->condition('aav.aggregate_contents_hash', $aggregate_contents_hash) + ->execute() + ->fetchField(); + if (!empty($atime)) { + return $atime; + } + + // Return the atime from disk as a last resort. + if (file_exists($uri)) { + return fileatime($uri); + } + // No atime was found, return FALSE. + return FALSE; +} + +/** + * Split up as CSS string by @media queries. + * + * @param string $css + * String of CSS. + * @param string $starting_string + * What to look for when starting to parse the string. + * + * @return array + * array of css with only media queries. + * + * @see http://stackoverflow.com/a/14145856/125684 + */ +function advagg_parse_media_blocks($css, $starting_string = '@media') { + $media_blocks = array(); + $start = 0; + $last_start = 0; + + // Using the string as an array throughout this function. + // http://php.net/types.string#language.types.string.substr + while (($start = strpos($css, $starting_string, $start)) !== FALSE) { + // Stack to manage brackets. + $s = array(); + + // Get the first opening bracket. + $i = strpos($css, "{", $start); + + // If $i is false, then there is probably a css syntax error. + if ($i === FALSE) { + continue; + } + + // Push bracket onto stack. + array_push($s, $css[$i]); + // Move past first bracket. + ++$i; + + // Find the closing bracket for the @media statement. But ensure we don't + // overflow if there's an error. + while (!empty($s) && isset($css[$i])) { + // If the character is an opening bracket, push it onto the stack, + // otherwise pop the stack. + if ($css[$i] === "{") { + array_push($s, "{"); + } + elseif ($css[$i] === "}") { + array_pop($s); + } + ++$i; + } + + // Get CSS before @media and store it. + if ($last_start != $start) { + $insert = trim(substr($css, $last_start, $start - $last_start)); + if (!empty($insert)) { + $media_blocks[] = $insert; + } + } + // Cut @media block out of the css and store. + $media_blocks[] = trim(substr($css, $start, $i - $start)); + // Set the new $start to the end of the block. + $start = $i; + $last_start = $start; + } + + // Add in any remaining css rules after the last @media statement. + if (strlen($css) > $last_start) { + $insert = trim(substr($css, $last_start)); + if (!empty($insert)) { + $media_blocks[] = $insert; + } + } + + return $media_blocks; +} + +/** + * Given a filename create that file; usually works if PHP goes fatal. + * + * @param string $filename + * Just the filename no path information. + * + * @return mixed + * On failure a string saying why it failed. + * On success the $files_to_save array. + */ +function advagg_missing_fatal_handler($filename) { + static $counter = 0; + // Bail out if there is no error. + $error = error_get_last(); + if ($error === NULL) { + return; + } + + $counter++; + // Bail out if this is still in a loop. + if ($counter > 2) { + return; + } + + // Bail out if the file already exists. + $data = advagg_get_hashes_from_filename($filename); + $type = $data[0]; + list($css_path, $js_path) = advagg_get_root_files_dir(); + $uri = ''; + if ($type === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + if (file_exists($uri)) { + return; + } + + // Generate the file with no alters. + set_time_limit(0); + $return = advagg_missing_create_file($filename, TRUE); + if (is_array($return) && !headers_sent()) { + $redirect_counter = isset($_GET['redirect_counter']) ? intval($_GET['redirect_counter']) : 0; + // 307 if headers have not been sent yet. + $uri = advagg_generate_location_uri($filename, $data[0], $data[3]); + ++$redirect_counter; + $uri .= '?redirect_counter=' . $redirect_counter; + header('Location: ' . $uri, TRUE, 307); + exit(); + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg.module b/frontend/drupal/sites/all/modules/advagg/advagg.module new file mode 100644 index 000000000..2b497ef05 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg.module @@ -0,0 +1,6977 @@ +' . $readme . ''; + } + } + else { + $output = '
' . $readme . '
'; + } + return $output; + } +} + +/** + * Implements hook_block_view_alter(). + */ +function advagg_block_view_alter(&$data, $block) { + // Do not run hook if AdvAgg is disabled. + if (!advagg_enabled()) { + return; + } + // Do not run hook if setting is disabled. + if (!variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE)) { + return; + } + + if (empty($data) || empty($data['content'])) { + return; + } + + $block_info = $block->module . ':' . $block->delta; + $prefix = ""; + $suffix = ""; + if (is_string($data['content'])) { + $data['content'] = $prefix . $data['content'] . $suffix; + } + else { + if (!isset($data['content']['#prefix'])) { + $data['content']['#prefix'] = ''; + } + $data['content']['#prefix'] .= $prefix; + if (!isset($data['content']['#suffix'])) { + $data['content']['#suffix'] = ''; + } + $data['content']['#suffix'] .= $suffix; + } +} + +/** + * Implements hook_views_pre_render(). + */ +function advagg_views_pre_render(&$view) { + // Do not run hook if AdvAgg is disabled. + if (!advagg_enabled()) { + return; + } + // Do not run hook if setting is disabled. + if (!variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE)) { + return; + } + + $info = "{$view->name}:{$view->current_display}"; + $prefix = ""; + $suffix = ""; + if (!isset($view->attachment_before)) { + $view->attachment_before = ''; + } + $view->attachment_before .= $prefix; + if (!isset($view->attachment_after)) { + $view->attachment_after = ''; + } + $view->attachment_after .= $suffix; +} + +/** + * Implements hook_panels_pre_render(). + */ +function advagg_panels_pre_render($panels_display, &$renderer) { + // Do not run hook if AdvAgg is disabled. + if (!advagg_enabled()) { + return; + } + // Do not run hook if setting is disabled. + if (!variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE)) { + return; + } + + $info = "{$panels_display->layout}:{$panels_display->css_id}"; + $prefix = ""; + $suffix = ""; + if (!isset($renderer->prefix)) { + $renderer->prefix = ''; + } + $renderer->prefix .= $prefix; + if (!isset($renderer->suffix)) { + $renderer->suffix = ''; + } + $renderer->suffix .= $suffix; +} + +/** + * Implements hook_url_inbound_alter(). + * + * Inbound URL rewrite helper. If host includes subdomain, rewrite URI and + * internal path if necessary. + */ +function advagg_url_inbound_alter(&$path, $original_path, $path_language) { + // Do nothing if this has been disabled. + if (!variable_get('advagg_url_inbound_alter', ADVAGG_URL_INBOUND_ALTER)) { + return; + } + + // Setup static so we only need to run the logic once. + $already_ran = &drupal_static(__FUNCTION__); + if (!isset($already_ran)) { + $already_ran = array(); + } + $request_path = request_path(); + + // Set the path again if we already did this alter. + if (array_key_exists($request_path, $already_ran)) { + $path = $already_ran[$request_path]; + return; + } + + // If requested path was for an advagg file but now it is something else + // switch is back to the advagg file. + if (!empty($path) + && $path != $request_path + && advagg_match_file_pattern($request_path) + ) { + // Get the advagg paths. + $advagg_path = advagg_get_root_files_dir(); + + // Get the top level path. + $top_level = substr($advagg_path[0][1], 0, strpos($advagg_path[0][1], 'advagg_css')); + + // Only change if it's an exact match. + $start = strpos($request_path, $top_level . 'advagg_'); + if ($start === 0) { + // Set path to correct advagg path. + $path = substr($request_path, $start); + $already_ran[$request_path] = $path; + } + else { + // Put all languages prefixes into an array. + $language_list = language_list(); + $prefixes = array(); + foreach ($language_list as $lang) { + if ($lang->enabled && !empty($lang->prefix) && strpos($request_path, $lang->prefix) !== FALSE) { + $prefixes[$lang->prefix] = $lang->prefix; + } + } + if (!empty($prefixes)) { + // Remove all enabled languages prefixes from the beginning of the path. + $substr_to_shrink = substr($request_path, 0, $start); + foreach ($prefixes as $prefix) { + $substr_to_shrink = str_replace($prefix . '/', '', $substr_to_shrink); + } + // Set path to correct advagg path. + $path = $substr_to_shrink . substr($request_path, $start); + $already_ran[$request_path] = $path; + } + } + } +} + +/** + * Implements hook_hook_info(). + */ +function advagg_hook_info() { + // List of hooks that can be inside of *.advagg.inc files. + // All advagg hooks except for: + // advagg_current_hooks_hash_array_alter + // advagg_hooks_implemented_alter + // advagg_get_root_files_dir_alter + // because these 3 hooks are used on most requests. + $advagg_hooks = array( + 'advagg_get_css_file_contents_pre_alter', + 'advagg_get_css_file_contents_alter', + 'advagg_get_js_file_contents_alter', + 'advagg_get_css_aggregate_contents_alter', + 'advagg_get_js_aggregate_contents_alter', + 'advagg_save_aggregate_pre_alter', + 'advagg_save_aggregate_alter', + 'advagg_build_aggregate_plans_alter', + 'advagg_build_aggregate_plans_post_alter', + 'advagg_css_groups_alter', + 'advagg_js_groups_alter', + 'advagg_modify_css_pre_render_alter', + 'advagg_modify_js_pre_render_alter', + 'advagg_changed_files', + 'advagg_removed_aggregates', + 'advagg_scan_for_changes', + 'advagg_get_info_on_files_alter', + 'advagg_context_alter', + 'advagg_missing_root_file', + ); + $hooks = array(); + foreach ($advagg_hooks as $hook) { + $hooks[$hook] = array('group' => 'advagg'); + } + return $hooks; +} + +/** + * Implements hook_module_implements_alter(). + */ +function advagg_module_implements_alter(&$implementations, $hook) { + // Move advagg_theme_registry_alter to the top. + if ($hook === 'theme_registry_alter' && array_key_exists('advagg', $implementations)) { + $item = array('advagg' => $implementations['advagg']); + unset($implementations['advagg']); + $implementations = array_merge($item, $implementations); + } + + // Move advagg_ajax_render_alter to the top. + if ($hook === 'ajax_render_alter' && array_key_exists('advagg', $implementations)) { + $item = array('advagg' => $implementations['advagg']); + unset($implementations['advagg']); + $implementations = array_merge($item, $implementations); + } + + // Move advagg_element_info_alter to the bottom. + if ($hook === 'element_info_alter' && array_key_exists('advagg', $implementations)) { + $item = $implementations['advagg']; + unset($implementations['advagg']); + $implementations['advagg'] = $item; + } + + // Replace locale_js_alter with _advagg_locale_js_alter. + if ($hook === 'js_alter' && array_key_exists('locale', $implementations)) { + unset($implementations['locale']); + $implementations['_advagg_locale'] = FALSE; + } + + // Move advagg_file_url_alter to the bottom. + if ($hook === 'file_url_alter' && array_key_exists('advagg', $implementations)) { + $item = $implementations['advagg']; + unset($implementations['advagg']); + $implementations['advagg'] = $item; + } + + if ($hook === 'requirements') { + // Move advagg_requirements to the bottom. + if (array_key_exists('advagg', $implementations)) { + $item = $implementations['advagg']; + unset($implementations['advagg']); + $implementations['advagg'] = $item; + } + // Move advagg_css_cdn to the bottom. + if (array_key_exists('advagg_css_cdn', $implementations)) { + $item = $implementations['advagg_css_cdn']; + unset($implementations['advagg_css_cdn']); + $implementations['advagg_css_cdn'] = $item; + } + // Move advagg_css_compress to the bottom. + if (array_key_exists('advagg_css_compress', $implementations)) { + $item = $implementations['advagg_css_compress']; + unset($implementations['advagg_css_compress']); + $implementations['advagg_css_compress'] = $item; + } + // Move advagg_js_cdn to the bottom. + if (array_key_exists('advagg_js_cdn', $implementations)) { + $item = $implementations['advagg_js_cdn']; + unset($implementations['advagg_js_cdn']); + $implementations['advagg_js_cdn'] = $item; + } + // Move advagg_js_compress to the bottom. + if (array_key_exists('advagg_js_compress', $implementations)) { + $item = $implementations['advagg_js_compress']; + unset($implementations['advagg_js_compress']); + $implementations['advagg_js_compress'] = $item; + } + } + + // Move advagg_cron to the bottom. + if ($hook === 'cron' && array_key_exists('advagg', $implementations)) { + $item = $implementations['advagg']; + unset($implementations['advagg']); + $implementations['advagg'] = $item; + } +} + +/** + * Implements hook_js_alter(). + * + * This is a locking wrapper for locale_js_alter(). + */ +function _advagg_locale_js_alter(&$js) { + // If the variable is empty then get the latest variable from the database. + $name = 'javascript_parsed'; + $parsed = variable_get($name, array()); + if (empty($parsed)) { + $variables = array_map('unserialize', db_query('SELECT name, value FROM {variable} WHERE name = :name', array(':name' => $name))->fetchAllKeyed()); + if (!empty($variables[$name])) { + $GLOBALS['conf'][$name] = $variables[$name]; + } + } + + // See if locale_js_alter() needs to do anything. + $dir = 'public://' . variable_get('locale_js_directory', 'languages'); + $new_files = FALSE; + // See if a rebuild of the translation file for the current language is + // needed. + if (!empty($parsed['refresh:' . $GLOBALS['language']->language])) { + $new_files = TRUE; + } + // Check for new js source files. + if (empty($new_files)) { + foreach ($js as $item) { + if ($item['type'] === 'file' + && !in_array($item['data'], $parsed) + && substr($item['data'], 0, strlen($dir)) != $dir + ) { + $new_files = TRUE; + break; + } + } + } + if (empty($new_files)) { + // No new files to manage, just add in available i18n files. + advagg_locale_js_add_translations($js, $dir); + // Exit function. + return; + } + + $count = 0; + while (!lock_acquire('locale_js_alter', 10)) { + ++$count; + // If we've waited over 3 times then skip. + if ($count > 3) { + lock_release('locale_js_alter'); + // Add in available i18n files. + advagg_locale_js_add_translations($js, $dir); + + // Disable saving to the cache as translations might be missing. + drupal_page_is_cacheable(FALSE); + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) > 1) { + $GLOBALS['conf']['advagg_cache_level'] = 0; + } + return; + } + + // Wait for the lock to be available. + lock_wait('locale_js_alter'); + } + + try { + // Run the alter. + locale_js_alter($js); + } + catch (PDOException $e) { + // If it fails we don't care, javascript_parsed is either already written or + // it will happen again on the next request. + // Still log it if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + watchdog('advagg', 'Development Mode - Caught PDO Exception: @info', array('@info' => $e)); + } + } + lock_release('locale_js_alter'); +} + +/** + * Implements hook_system_info_alter(). + */ +function advagg_system_info_alter(&$info, $file, $type) { + $config_path = &drupal_static(__FUNCTION__); + // Get advagg config path. + if (empty($config_path)) { + $config_path = advagg_admin_config_root_path(); + } + + // Replace advagg path. + if (!empty($info['configure']) + && strpos($info['configure'], '/advagg') !== FALSE + && ((!empty($info['dependencies']) + && is_array($info['dependencies']) + && in_array('advagg', $info['dependencies']) + ) || $file->name === 'advagg') + ) { + $pos = strpos($info['configure'], '/advagg') + 7; + $substr = substr($info['configure'], 0, $pos); + $info['configure'] = str_replace($substr, $config_path . '/advagg', $info['configure']); + } +} + +/** + * Implements hook_permission(). + */ +function advagg_permission() { + return array( + 'bypass advanced aggregation' => array( + 'title' => t('bypass advanced aggregation'), + 'description' => t('User can use URL query strings to bypass AdvAgg.'), + ), + ); +} + +/** + * Implements hook_file_url_alter(). + */ +function advagg_file_url_alter(&$original_uri) { + // Do nothing if URI does not contain /advagg_ + // OR file does not have the correct pattern. + if (strpos($original_uri, '/advagg_') === FALSE || !advagg_match_file_pattern($original_uri)) { + return; + } + + // CDN fix. + // Do nothing if + // in maintenance_mode + // CDN module does not exist + // CDN far future is disabled + // CDN mode is not basic + // URI does not contain cdn/farfuture/. + if (variable_get('maintenance_mode', FALSE) + || !module_exists('cdn') + || !variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT) + || variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC) != CDN_MODE_BASIC + || strpos($original_uri, 'cdn/farfuture/') === FALSE + ) { + return; + } + + // Remove cdn/farfuture/BASE64/prefix:value/ from the URI. + $original_uri = preg_replace('/cdn\/farfuture\/[A-Za-z0-9-_]{43}\/[A-Za-z]+\:[A-Za-z0-9-_]+\//', '', $original_uri); +} + +/** + * Implements hook_menu(). + */ +function advagg_menu() { + list($css_path, $js_path) = advagg_get_root_files_dir(); + $file_path = drupal_get_path('module', 'advagg'); + $config_path = advagg_admin_config_root_path(); + + $path_defined = FALSE; + if (advagg_s3fs_evaluate_no_rewrite_cssjs(FALSE)) { + $external_css = trim(parse_url(str_replace('/test.css', '/%', file_create_url($css_path[0] . '/test.css')), PHP_URL_PATH)); + if (strpos($external_css, $GLOBALS['base_path']) === 0) { + $external_css = substr($external_css, strlen($GLOBALS['base_path'])); + } + $external_js = trim(parse_url(str_replace('/test.js', '/%', file_create_url($js_path[0] . '/test.js')), PHP_URL_PATH)); + if (strpos($external_js, $GLOBALS['base_path']) === 0) { + $external_js = substr($external_js, strlen($GLOBALS['base_path'])); + } + $items[$external_css] = array( + 'title' => "Generate CSS Aggregate", + 'page callback' => 'advagg_missing_aggregate', + 'type' => MENU_CALLBACK, + // Allow anyone to access these public css files. + 'access callback' => TRUE, + 'file path' => $file_path, + 'file' => 'advagg.missing.inc', + ); + $items[$external_js] = array( + 'title' => "Generate JS Aggregate", + 'page callback' => 'advagg_missing_aggregate', + 'type' => MENU_CALLBACK, + // Allow anyone to access these public js files. + 'access callback' => TRUE, + 'file path' => $file_path, + 'file' => 'advagg.missing.inc', + ); + $path_defined = TRUE; + } + + if (!$path_defined) { + $items[$css_path[1] . '/%'] = array( + 'title' => "Generate CSS Aggregate", + 'page callback' => 'advagg_missing_aggregate', + 'type' => MENU_CALLBACK, + // Allow anyone to access these public css files. + 'access callback' => TRUE, + 'file path' => $file_path, + 'file' => 'advagg.missing.inc', + ); + $items[$js_path[1] . '/%'] = array( + 'title' => "Generate JS Aggregate", + 'page callback' => 'advagg_missing_aggregate', + 'type' => MENU_CALLBACK, + // Allow anyone to access these public js files. + 'access callback' => TRUE, + 'file path' => $file_path, + 'file' => 'advagg.missing.inc', + ); + } + + // If mutiple paths are symlinked to the same location; allow advagg to handle + // those addtional locations. + $advagg_additional_generate_paths = variable_get('advagg_additional_generate_paths', array()); + if (!empty($advagg_additional_generate_paths)) { + foreach ($advagg_additional_generate_paths as $path) { + $items[$path] = array( + 'title' => "Generate CSS/JS Aggregate", + 'page callback' => 'advagg_missing_aggregate', + 'type' => MENU_CALLBACK, + // Allow anyone to access these public css files. + 'access callback' => TRUE, + 'file path' => $file_path, + 'file' => 'advagg.missing.inc', + ); + } + } + + $items[$config_path . '/default'] = array( + 'title' => 'Performance', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'file path' => drupal_get_path('module', 'system'), + 'weight' => -10, + ); + $items[$config_path . '/advagg'] = array( + 'title' => 'Advanced CSS/JS Aggregation', + 'description' => 'Configuration for Advanced CSS/JS Aggregation.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg.admin.inc', + 'weight' => 1, + ); + $items[$config_path . '/advagg/config'] = array( + 'title' => 'Configuration', + 'type' => MENU_DEFAULT_LOCAL_TASK, + 'weight' => -10, + ); + $items[$config_path . '/advagg/info'] = array( + 'title' => 'Information', + 'description' => 'More detailed information about advagg.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_admin_info_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg.admin.inc', + 'weight' => 18, + ); + $items[$config_path . '/advagg/operations'] = array( + 'title' => 'Operations', + 'description' => 'Flush caches, set the bypass cookie, take drastic actions.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_admin_operations_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg.admin.inc', + 'weight' => 20, + ); + return $items; +} + +/** + * Implements hook_cron(). + * + * This will be ran once a day at most. + */ +function advagg_cron($bypass_time_check = FALSE) { + // @param bool $bypass_time_check + // Set to TRUE to skip the 24 hour check. + // + // Execute once a day (24 hours). + if (!$bypass_time_check && variable_get('advagg_cron_timestamp', 0) > (REQUEST_TIME - variable_get('advagg_cron_frequency', ADVAGG_CRON_FREQUENCY))) { + return array(); + } + variable_set('advagg_cron_timestamp', REQUEST_TIME); + + // Flush the cache_advagg_info cache bin. + cache_clear_all(NULL, 'cache_advagg_info'); + + $return = array(); + // Clear out all stale advagg aggregated files. + module_load_include('inc', 'advagg', 'advagg.cache'); + $return[] = advagg_delete_stale_aggregates(); + + // Delete all empty aggregated files. + $return[] = advagg_delete_empty_aggregates(); + + // Delete orphaned aggregates. + $return[] = advagg_delete_orphaned_aggregates(); + + // Remove aggregates that include missing files. + $return[] = advagg_remove_missing_files_from_db(); + + // Remove unused aggregates. + $return[] = advagg_remove_old_unused_aggregates(); + + // Remove expired locks from the semaphore database table. + $return[] = advagg_cleanup_semaphore_table(); + + // Remove old temp files. + $return[] = advagg_remove_temp_files(); + + // Refresh all locale files. + $return[] = advagg_refresh_all_locale_files(); + + // Update libraries data. + advagg_get_remote_libraries_versions(TRUE); + + return $return; +} + +/** + * Implements hook_flush_caches(). + */ +function advagg_flush_caches($all_bins = FALSE, $push_new_changes = TRUE) { + // * @param bool $all_bins + // * TRUE: Get all advagg cache bins. + // * @param bool $push_new_changes + // * FALSE: Do not scan for changes. + // + // Send back a blank array if aav table doesn't exist. + if (!db_table_exists('advagg_aggregates_versions')) { + return array(); + } + + // Scan for and push new changes. + module_load_include('inc', 'advagg', 'advagg.cache'); + if ($push_new_changes) { + advagg_push_new_changes(); + } + + // Get list of cache bins to clear. + $bins = array('cache_advagg_aggregates'); + if ($all_bins) { + $bins[] = 'cache_advagg_info'; + } + return $bins; +} + +/** + * Implements hook_element_info_alter(). + */ +function advagg_element_info_alter(&$type) { + // Replace drupal_pre_render_styles with advagg_pre_render_styles. + $type['styles']['#items'] = array(); + if (!isset($type['styles']['#pre_render'])) { + $type['styles']['#pre_render'] = array(); + } + $key = array_search('drupal_pre_render_styles', $type['styles']['#pre_render']); + if ($key !== FALSE) { + $type['styles']['#pre_render'][$key] = 'advagg_pre_render_styles'; + } + else { + $type['styles']['#pre_render'][] = 'advagg_pre_render_styles'; + } + // Allow for other code to easily change the render with alter hooks. + $type['styles']['#pre_render'][] = 'advagg_modify_css_pre_render'; + $type['styles']['#group_callback'] = 'drupal_group_css'; + // Swap in our own aggregation callback. + $type['styles']['#aggregate_callback'] = '_advagg_aggregate_css'; + $type['styles']['#type'] = 'styles'; + + // Replace drupal_pre_render_scripts with advagg_pre_render_scripts. + $type['scripts']['#items'] = array(); + if (!isset($type['scripts']['#pre_render'])) { + $type['scripts']['#pre_render'] = array(); + } + $key_drupal = array_search('drupal_pre_render_scripts', $type['scripts']['#pre_render']); + $key_omega = array_search('omega_pre_render_scripts', $type['scripts']['#pre_render']); + $key_aurora = array_search('aurora_pre_render_scripts', $type['scripts']['#pre_render']); + if ($key_drupal !== FALSE) { + $type['scripts']['#pre_render'][$key_drupal] = 'advagg_pre_render_scripts'; + } + elseif ($key_omega !== FALSE) { + $type['scripts']['#pre_render'][$key_omega] = 'advagg_pre_render_scripts'; + } + elseif ($key_aurora !== FALSE) { + $type['scripts']['#pre_render'][$key_aurora] = 'advagg_pre_render_scripts'; + } + else { + $type['scripts']['#pre_render'][] = 'advagg_pre_render_scripts'; + } + // Allow for other code to easily change the render with alter hooks. + $type['scripts']['#pre_render'][] = 'advagg_modify_js_pre_render'; + $type['scripts']['#group_callback'] = 'advagg_group_js'; + // Swap in our own aggregation callback. + $type['scripts']['#aggregate_callback'] = '_advagg_aggregate_js'; + $type['scripts']['#type'] = 'scripts'; + + // Copy html_tag to html_script_tag. + $type['html_script_tag'] = $type['html_tag']; + $type['html_script_tag']['#theme'] = 'html_script_tag'; + $type['html_script_tag']['#type'] = 'html_script_tag'; +} + +/** + * Implements hook_theme_registry_alter(). + * + * Replace template_process_html with _advagg_process_html. + */ +function advagg_theme_registry_alter(&$theme_registry) { + if (!isset($theme_registry['html'])) { + return; + } + + // Replace core's process function with our own. + $index = array_search('template_process_html', $theme_registry['html']['process functions']); + if ($index !== FALSE) { + $theme_registry['html']['process functions'][$index] = '_advagg_process_html'; + } + else { + // Put AdvAgg at the bottom if we can't find the replacement. + $theme_registry['html']['process functions'][] = '_advagg_process_html'; + } + + // Copy html_tag to html_script_tag. + $theme_registry['html_script_tag'] = $theme_registry['html_tag']; + $theme_registry['html_script_tag']['function'] = 'theme_html_script_tag'; + + // Fix imce_page. + if (isset($theme_registry['imce_page'])) { + $advagg_path = drupal_get_path('module', 'advagg'); + $imce_path = drupal_get_path('module', 'imce'); + if (strpos($theme_registry['imce_page']['path'], $imce_path) !== FALSE) { + $theme_registry['imce_page']['path'] = $advagg_path . '/tpl'; + } + } +} + +/** + * Implements hook_ajax_render_alter(). + */ +function advagg_ajax_render_alter(&$commands) { + // Do not run hook if AdvAgg is disabled. + if (!advagg_enabled()) { + return; + } + + // Do not run hook if advagg_ajax_render_alter is FALSE. + if (!variable_get('advagg_ajax_render_alter', ADVAGG_AJAX_RENDER_ALTER)) { + return; + } + + // Conditionally adds the default Drupal/jQuery libraries to the page. + // @see http://drupal.org/node/1279226 + if (function_exists('drupal_add_js_page_defaults')) { + drupal_add_js_page_defaults(); + } + + // Get Core JS. + list(, $core_scripts_header, $core_scripts_footer, $items, $settings) = advagg_build_ajax_js_css(); + + // Get AdvAgg JS. + $scripts_header = $scripts_footer = ''; + if (!empty($items['js'])) { + $scripts_footer_array = advagg_get_js('footer', $items['js'], TRUE); + // Function advagg_pre_render_scripts() gets called here. + $scripts_footer = drupal_render($scripts_footer_array); + $scripts_header_array = advagg_get_js('header', $items['js'], TRUE); + // Function advagg_pre_render_scripts() gets called here. + $scripts_header = drupal_render($scripts_header_array); + } + + // Remove core JS. + foreach ($commands as $key => $values) { + // Skip if not an array or not a command. + if (!is_array($values) || empty($values['command'])) { + continue; + } + + if ($values['command'] === 'settings' + && is_array($values['settings']) + && !empty($values['merge']) + ) { + // Remove JS settings. + unset($commands[$key]); + continue; + } + if ($values['command'] === 'insert' + && is_null($values['settings']) + && $values['method'] === 'prepend' + && $values['data'] == $core_scripts_header + ) { + // Remove JS header. + unset($commands[$key]); + continue; + } + if ($values['command'] === 'insert' + && is_null($values['settings']) + && $values['method'] === 'append' + && $values['data'] == $core_scripts_footer + ) { + // Remove JS footer. + unset($commands[$key]); + continue; + } + } + + // Add in AdvAgg JS. + $extra_commands = array(); + if (!empty($scripts_header)) { + $extra_commands[] = ajax_command_prepend('head', $scripts_header); + } + if (!empty($scripts_footer)) { + $extra_commands[] = ajax_command_append('body', $scripts_footer); + } + if (!empty($extra_commands)) { + $commands = array_merge($extra_commands, $commands); + } + if (!empty($settings)) { + array_unshift($commands, ajax_command_settings(advagg_cleanup_settings_array(drupal_array_merge_deep_array(array_filter($settings['data'], 'is_array'))), TRUE)); + } +} + +/** + * Implements hook_preprocess_page(). + */ +function advagg_preprocess_page() { + // Scan for changes to any CSS/JS files if in development mode. + advagg_scan_filesystem_for_changes_live(); +} + +/** + * Implements hook_preprocess_html(). + * + * Add in rendering IE meta tag if "combine CSS" is enabled. + */ +function advagg_preprocess_html() { + // http://www.phpied.com/conditional-comments-block-downloads/#update + // Prevent conditional comments from stalling css downloads. + $fix_blocking_css_ie = array( + '#weight' => '-999999', + '#type' => 'markup', + '#markup' => "\n", + ); + // Add markup for IE conditional comments to head. + drupal_add_html_head($fix_blocking_css_ie, 'fix_blocking_css_ie'); + + // Do not force IE rendering mode if "combine CSS" is disabled. + if (!variable_get('advagg_combine_css_media', ADVAGG_COMBINE_CSS_MEDIA)) { + return; + } + + // Send IE meta tag to force IE rendering mode header. + $x_ua_compatible = 'IE=edge'; + if (variable_get('advagg_chrome_header_enabled', ADVAGG_CHROME_HEADER_ENABLED)) { + $x_ua_compatible .= ',chrome=1'; + } + drupal_add_http_header('X-UA-Compatible', $x_ua_compatible); +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Give advice on how to temporarily disable css/js aggregation. + */ +function advagg_form_system_performance_settings_alter(&$form, &$form_state) { + module_load_include('admin.inc', 'advagg'); + advagg_admin_system_performance_settings_form($form, $form_state); +} + +/** + * Implements hook_js_alter(). + */ +function advagg_js_alter(&$js) { + if (module_exists('admin_menu')) { + // Fix for admin menu; put JS in footer. + $path = drupal_get_path('module', 'admin_menu'); + $filename = $path . '/admin_menu.js'; + if (isset($js[$filename])) { + $js[$filename]['scope'] = 'footer'; + } + } +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @defgroup 3rd_party_hooks 3rd party hook implementations + * @{ + * Hooks that are not apart of core or AdvAgg. + */ + +/** + * Implements hook_cron_alter(). + */ +function advagg_cron_alter(&$data) { + // Run this cron job every 2 minutes. + if (isset($data['advagg_js_compress_cron'])) { + $data['advagg_js_compress_cron']['rule'] = '*/2 * * * *'; + } + // Run this cron job every 5 minutes. + if (isset($data['advagg_relocate_cron'])) { + $data['advagg_relocate_cron']['rule'] = '*/5 * * * *'; + } + // Run this cron job every day. + if (isset($data['advagg_cron'])) { + $data['advagg_cron']['rule'] = '0 0 * * *'; + } +} + +/** + * Implements hook_password_policy_force_change_allowed_paths_alter(). + */ +function advagg_password_policy_force_change_allowed_paths_alter(&$allowed_paths) { + $advagg_items = advagg_menu(); + foreach ($advagg_items as $path => $attributes) { + if (!empty($attributes['page callback']) && $attributes['page callback'] === 'advagg_missing_aggregate') { + $allowed_paths[] = str_replace('/%', '/*', $path); + } + } +} + +/** + * Implements hook_s3fs_upload_params_alter(). + * + * Set headers for advagg files. + */ +function advagg_s3fs_upload_params_alter(&$upload_params) { + // Get advagg dir. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $scheme = file_uri_scheme($css_path[1]); + if ($scheme) { + $css_path_dir = parse_url($css_path[1]); + $css_path_dir = str_replace("$scheme://", '', $css_path[1]); + } + else { + $css_path_dir = ltrim($css_path[1], '/'); + } + $scheme = file_uri_scheme($js_path[1]); + if ($scheme) { + $js_path_dir = parse_url($js_path[1]); + $js_path_dir = str_replace("$scheme://", '', $js_path_dir[1]); + } + else { + $js_path_dir = ltrim($js_path[1], '/'); + } + + // Get file type in advagg dir, css or js. + $type = ''; + if (strpos($upload_params['Bucket'] . '/' . $upload_params['Key'], $css_path_dir) !== FALSE) { + $type = 'css'; + } + if (strpos($upload_params['Bucket'] . '/' . $upload_params['Key'], $js_path_dir) !== FALSE) { + $type = 'js'; + } + if ($js_path_dir === $css_path_dir && !empty($type)) { + $pathinfo = pathinfo($upload_params['Key']); + if ($pathinfo['extension'] === 'gz') { + $pathinfo = pathinfo($pathinfo['filename']); + } + $type = $pathinfo['extension']; + } + if (empty($type)) { + // Only change advagg files. + return; + } + + // Cache control is 52 weeeks. + if (variable_get('advagg_resource_hints_use_immutable', ADVAGG_RESOURCE_HINTS_USE_IMMUTABLE)) { + $upload_params['CacheControl'] = 'max-age=31449600, no-transform, public, immutable'; + } + else { + $upload_params['CacheControl'] = 'max-age=31449600, no-transform, public'; + } + // Expires in 365 days. + $upload_params['Expires'] = gmdate('D, d M Y H:i:s \G\M\T', REQUEST_TIME + 365 * 24 * 60 * 60); + + // The extension is .css or .js. + $pathinfo = pathinfo($upload_params['Key']); + if ($pathinfo['extension'] === $type) { + if (variable_get('advagg_gzip', ADVAGG_GZIP)) { + // Set gzip. + $upload_params['ContentEncoding'] = 'gzip'; + } + elseif (variable_get('advagg_brotli', ADVAGG_BROTLI)) { + // Set br. + $upload_params['ContentEncoding'] = 'br'; + } + } +} + +/** + * Return s3fs configuration settings and values. + * + * @param string $key + * A specific key available in the s3fs configuration. NULL by default. + * + * @return array|string|null + * The full s3fs configuration settings, value of a specific key, + * or NULL if s3fs and the function do not exist. + */ +function advagg_get_s3fs_config($key = NULL) { + if (module_exists('s3fs') && is_callable('_s3fs_get_config')) { + $s3fs_config = _s3fs_get_config(); + return (empty($key)) ? $s3fs_config : $s3fs_config[$key]; + } + else { + return NULL; + } +} + +/** + * Shortcut to evaluate if s3fs no_rewrite_cssjs is set or empty. + * + * If this needs to be accessed in a loop, it is more efficient to call + * advagg_get_s3fs_config() once from outside of the loop. An example + * can be seen in the advagg_install_check_via_http function. + * + * @param bool $is_set + * Check if no_write_cssjs field is set (TRUE) or empty (FALSE). + * + * @return bool + * TRUE or FALSE is returned based on evaluating the field. If + * s3fs_config returns a NULL, evaluate the function to FALSE. + * + * @see advagg_get_s3fs_config() + */ +function advagg_s3fs_evaluate_no_rewrite_cssjs($is_set = TRUE) { + $s3fs_no_rewrite_cssjs = advagg_get_s3fs_config('no_rewrite_cssjs'); + if (!is_null($s3fs_no_rewrite_cssjs)) { + return ($is_set) ? !empty($s3fs_no_rewrite_cssjs) : empty($s3fs_no_rewrite_cssjs); + } + else { + return FALSE; + } +} + +/** + * Implements hook_admin_menu_cache_info(). + * + * Add in a cache flush for advagg. + */ +function advagg_admin_menu_cache_info() { + if (variable_get('advagg_enabled', ADVAGG_ENABLED)) { + $caches['advagg'] = array( + 'title' => t('Adv CSS/JS Agg'), + 'callback' => 'advagg_admin_flush_cache', + ); + return $caches; + } +} + +/** + * Implements hook_admin_menu_output_alter(). + * + * Add in a cache flush for advagg. + */ +function advagg_admin_menu_output_alter(array &$content) { + if (variable_get('advagg_enabled', ADVAGG_ENABLED)) { + // Remove default core aggregation link. + unset($content['icon']['icon']['flush-cache']['assets']); + } +} + +/** + * Implements hook_anonymous_login_paths_alter(). + */ +function advagg_anonymous_login_paths_alter(&$paths) { + // Exclude advagg css/js paths. + list($css_path, $js_path) = advagg_get_root_files_dir(); + $paths['exclude'][] = $css_path[1] . '/*'; + $paths['exclude'][] = $js_path[1] . '/*'; +} + +/** + * Implements hook_pre_flush_all_caches(). + */ +function advagg_pre_flush_all_caches() { + static $run_once; + + if (!isset($run_once)) { + $run_once = TRUE; + // Only invoked by registry_rebuild. + module_load_include('admin.inc', 'advagg'); + // Truncate the advagg_files table. + advagg_admin_truncate_advagg_files(); + } +} + +/** + * @} End of "defgroup 3rd_party_hooks". + */ + +/** + * Only the alter part of locale_js_alter(), not the parsing part. + * + * @param array $javascript + * An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * @param string $dir + * String pointing to the public locale_js_directory. + */ +function advagg_locale_js_add_translations(array &$javascript, $dir) { + // Add the translation JavaScript file to the page. + if (!empty($GLOBALS['language']->javascript)) { + // Add the translation JavaScript file to the page. + $file = $dir . '/' . $GLOBALS['language']->language . '_' . $GLOBALS['language']->javascript . '.js'; + $javascript[$file] = drupal_js_defaults($file); + } +} + +/** + * Callback for pre_render so elements can be modified before they are rendered. + * + * @param array $elements + * A render array containing: + * - #items: The JavaScript items as returned by drupal_add_js() and + * altered by drupal_get_js(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @return array + * A render array that will render to a string of JavaScript tags. + */ +function advagg_modify_js_pre_render(array $elements) { + // Get the children elements. + $children = array_intersect_key($elements, array_flip(element_children($elements))); + + // Allow other modules to modify $children and $elements before they are + // rendered. + // Call hook_advagg_modify_js_pre_render_alter() + drupal_alter('advagg_modify_js_pre_render', $children, $elements); + + // Remove old children elements. + foreach ($children as $key => $value) { + if (isset($elements[$key])) { + unset($elements[$key]); + } + } + // Add in new children elements. + $elements += $children; + return $elements; +} + +/** + * Callback for pre_render so elements can be modified before they are rendered. + * + * @param array $elements + * A render array containing: + * - #items: The CSS items as returned by drupal_add_css() and + * altered by drupal_get_css(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @return array + * A render array that will render to a string of JavaScript tags. + */ +function advagg_modify_css_pre_render(array $elements) { + if (!advagg_enabled()) { + return $elements; + } + + // Put children elements into a reference array. + $children = array(); + foreach ($elements as $key => &$value) { + if ($key !== '' && is_string($key) && (0 === strpos($key, '#'))) { + continue; + } + $children[$key] = &$value; + } + unset($value); + + // Allow other modules to modify $children and $elements before they are + // rendered. + // Call hook_advagg_modify_css_pre_render_alter() + drupal_alter('advagg_modify_css_pre_render', $children, $elements); + return $elements; +} + +/** + * Default callback to aggregate CSS files and inline content. + * + * Having the browser load fewer CSS files results in much faster page loads + * than when it loads many small files. This function aggregates files within + * the same group into a single file unless the site-wide setting to do so is + * disabled (commonly the case during site development). To optimize download, + * it also compresses the aggregate files by removing comments, whitespace, and + * other unnecessary content. Additionally, this functions aggregates inline + * content together, regardless of the site-wide aggregation setting. + * + * @param array $css_groups + * An array of CSS groups as returned by drupal_group_css(). This function + * modifies the group's 'data' property for each group that is aggregated. + * + * @see drupal_aggregate_css() + * @see drupal_group_css() + * @see drupal_pre_render_styles() + * @see system_element_info() + */ +function _advagg_aggregate_css(array &$css_groups) { + if (!advagg_enabled()) { + return drupal_aggregate_css($css_groups); + } + if (variable_get('advagg_debug', ADVAGG_DEBUG)) { + $GLOBALS['_advagg']['debug']['css_groups_before'][] = $css_groups; + } + + $preprocess_css = advagg_file_aggregation_enabled('css'); + + // Allow other modules to modify $css_groups right before it is processed. + // Call hook_advagg_css_groups_alter(). + drupal_alter('advagg_css_groups', $css_groups, $preprocess_css); + + // For each group that needs aggregation, aggregate its items. + $files_to_aggregate = array(); + // Allow for inline CSS to be between aggregated files. + $gap_counter = 0; + foreach ($css_groups as $key => $group) { + switch ($group['type']) { + // If a file group can be aggregated into a single file, do so, and set + // the group's data property to the file path of the aggregate file. + case 'file': + if ($group['preprocess'] && $preprocess_css) { + $files_to_aggregate[$gap_counter][$key] = $group; + } + else { + ++$gap_counter; + } + break; + + // Aggregate all inline CSS content into the group's data property. + case 'inline': + ++$gap_counter; + $css_groups[$key]['data'] = ''; + foreach ($group['items'] as $item) { + $css_groups[$key]['data'] .= advagg_load_stylesheet_content($item['data'], $item['preprocess']); + } + break; + + // Create a gap for external CSS. + case 'external': + ++$gap_counter; + break; + } + } + + if (!empty($files_to_aggregate)) { + $hooks_hash = advagg_get_current_hooks_hash(); + $serialize_function = variable_get('advagg_serialize', ADVAGG_SERIALIZE); + $css_hash = drupal_hash_base64($serialize_function($files_to_aggregate)); + $cache_id = 'advagg:css:' . $hooks_hash . ':' . $css_hash; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 1 && $cache = cache_get($cache_id, 'cache_advagg_aggregates')) { + $plans = $cache->data; + } + else { + module_load_include('inc', 'advagg', 'advagg'); + $plans = advagg_build_aggregate_plans($files_to_aggregate, 'css'); + if (!empty($plans) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 1) { + cache_set($cache_id, $plans, 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + } + $css_groups = advagg_merge_plans($css_groups, $plans); + } + if (variable_get('advagg_debug', ADVAGG_DEBUG)) { + $GLOBALS['_advagg']['debug']['css_groups_after'][] = $css_groups; + } +} + +/** + * Default callback to aggregate JavaScript files. + * + * Having the browser load fewer JavaScript files results in much faster page + * loads than when it loads many small files. This function aggregates files + * within the same group into a single file unless the site-wide setting to do + * so is disabled (commonly the case during site development). To optimize + * download, it also compresses the aggregate files by removing comments, + * whitespace, and other unnecessary content. + * + * @param array $js_groups + * An array of JavaScript groups as returned by drupal_group_js(). For each + * group that is aggregated, this function sets the value of the group's + * 'data' key to the URI of the aggregate file. + * + * @see drupal_group_js() + * @see drupal_pre_render_scripts() + */ +function _advagg_aggregate_js(array &$js_groups) { + if (!advagg_enabled()) { + if (function_exists('drupal_aggregate_js')) { + return drupal_aggregate_js($js_groups); + } + else { + return; + } + } + if (variable_get('advagg_debug', ADVAGG_DEBUG)) { + $GLOBALS['_advagg']['debug']['js_groups_before'][] = $js_groups; + } + + $preprocess_js = advagg_file_aggregation_enabled('js'); + + // Allow other modules to modify $js_groups right before it is processed. + // Call hook_advagg_js_groups_alter(). + drupal_alter('advagg_js_groups', $js_groups, $preprocess_js); + + // For each group that needs aggregation, aggregate its items. + $files_to_aggregate = array(); + // Only aggregate when the site is configured to do so, and not during an + // update. + $gap_counter = 0; + if ($preprocess_js) { + // Set boolean to TRUE if all JS in footer. + $all_in_footer = FALSE; + if (module_exists('advagg_mod') && variable_get('advagg_mod_js_footer', ADVAGG_MOD_JS_FOOTER) >= 2) { + $all_in_footer = TRUE; + } + foreach ($js_groups as $key => &$group) { + switch ($group['type']) { + // If a file group can be aggregated into a single file, do so, and set + // the group's data property to the file path of the aggregate file. + case 'file': + if (!empty($group['preprocess'])) { + // Special handing for when all JS is in the footer. + if ($all_in_footer && $group['scope'] === 'footer' && $group['group'] > 9000) { + ++$gap_counter; + $all_in_footer = FALSE; + } + $files_to_aggregate[$gap_counter][$key] = $group; + } + else { + ++$gap_counter; + } + break; + + // Create a gap for inline JS. + case 'inline': + ++$gap_counter; + break; + + // Create a gap for external JS. + case 'external': + ++$gap_counter; + break; + } + } + unset($group); + } + + if (!empty($files_to_aggregate)) { + $hooks_hash = advagg_get_current_hooks_hash(); + $serialize_function = variable_get('advagg_serialize', ADVAGG_SERIALIZE); + $js_hash = drupal_hash_base64($serialize_function($files_to_aggregate)); + $cache_id = 'advagg:js:' . $hooks_hash . ':' . $js_hash; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 1 && $cache = cache_get($cache_id, 'cache_advagg_aggregates')) { + $plans = $cache->data; + } + else { + module_load_include('inc', 'advagg', 'advagg'); + $plans = advagg_build_aggregate_plans($files_to_aggregate, 'js'); + if (!empty($plans) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 1) { + cache_set($cache_id, $plans, 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + } + $js_groups = advagg_merge_plans($js_groups, $plans); + } + if (variable_get('advagg_debug', ADVAGG_DEBUG)) { + $GLOBALS['_advagg']['debug']['js_groups_after'][] = $js_groups; + } +} + +/** + * Builds the arrays needed for css rendering and caching. + * + * @param bool $skip_alter + * (Optional) If set to TRUE, this function skips calling drupal_alter() on + * css, useful for the aggressive cache. + * + * @return array + * Array contains the 2 arrays used for css. + */ +function _advagg_build_css_arrays_for_rendering($skip_alter = FALSE) { + // Get the raw CSS variable. + $raw_css = drupal_add_css(); + // Process and Sort css. + $full_css = advagg_get_css($raw_css, $skip_alter); + // Add attached js to drupal_add_js() function. + if (!empty($full_css['#attached'])) { + drupal_process_attached($full_css); + // Remove #attached since it's been added to the javascript array now. + unset($full_css['#attached']); + } + return array($raw_css, $full_css); +} + +/** + * Builds the arrays needed for js rendering and caching. + * + * @param bool $skip_alter + * (Optional) If set to TRUE, this function skips calling drupal_alter() on + * js, useful for the aggressive cache. + * + * @return array + * Array contains the 3 arrays used for javascript. + */ +function _advagg_build_js_arrays_for_rendering($skip_alter = FALSE) { + // Get the raw JS variable. + $javascript = drupal_add_js(); + // Process and Sort JS. + $full_javascript = advagg_get_full_js($javascript, $skip_alter); + // Get scopes used in the js. + $scopes = advagg_get_js_scopes($full_javascript); + // Add JS to the header and footer of the page. + $js_scope_array = array(); + $js_scope_settings_array = array(); + foreach ($scopes as $scope => $use) { + if (!$use) { + // If the scope is not being used, skip it. + continue; + } + // advagg_get_js() will sort the JavaScript so that it appears in the + // correct order. + $scripts = advagg_get_js($scope, $full_javascript); + if (isset($scripts['#items']['settings'])) { + // Get the js settings. + $js_scope_settings_array[$scope]['settings'] = $scripts['#items']['settings']; + // Exclude JS Settings from the array; we'll add it back later. + $scripts['#items']['settings'] = array(); + } + $js_scope_array[$scope] = $scripts; + } + + // Fix settings; if more than 1 is set, use the largest one. + if (count($js_scope_settings_array) > 1) { + $max = -1; + $max_scope = ''; + foreach ($js_scope_settings_array as $scope => $settings) { + $count = count($settings); + $max = max($max, $count); + if ($max == $count) { + $max_scope = $scope; + } + } + + foreach ($js_scope_settings_array as $scope => $settings) { + if ($scope !== $max_scope) { + unset($js_scope_settings_array[$scope]); + } + } + } + return array($javascript, $js_scope_settings_array, $js_scope_array); +} + +/** + * Returns TRUE if the CSS is being loaded via JavaScript. + * + * @param object $css_cache + * Cache object from cache_get(). + * + * @return bool + * TRUE if CSS loaded via JS. FALSE if not. + */ +function advagg_css_in_js($css_cache = NULL) { + if (module_exists('advagg_mod') + && variable_get('advagg_mod_css_defer', ADVAGG_MOD_CSS_DEFER) + ) { + return TRUE; + } + if (module_exists('css_delivery') + && css_delivery_enabled() + ) { + return TRUE; + } + // Critical css added by another means. + if (!empty($css_cache->data[1]['#items'])) { + foreach ($css_cache->data[1]['#items'] as $values) { + if (!empty($values['critical-css'])) { + return TRUE; + } + } + } + return variable_get('advagg_css_in_js', ADVAGG_CSS_IN_JS); +} + +/** + * Given the full css and js scope array return back the render cache. + * + * @param array $full_css + * Array from advagg_get_css() with #attached removed because it was built by + * _advagg_build_css_arrays_for_rendering(). + * @param array $js_scope_array + * Array built from iterations of advagg_get_js() inside of + * _advagg_build_js_arrays_for_rendering(). + * + * @return array + * Array containing the $css_cache, $js_cache, $css_cache_id, $js_cache_id. + */ +function advagg_get_render_cache(array $full_css, array $js_scope_array) { + $cids = array(); + $css_cache_id = ''; + $js_cache_id = ''; + + // Get advagg hash. + $hooks_hash = advagg_get_current_hooks_hash(); + $serialize_function = variable_get('advagg_serialize', ADVAGG_SERIALIZE); + if (advagg_file_aggregation_enabled('css')) { + // Generate css cache id. + $cids[] = $css_cache_id = 'advagg:css:full:1.1:' . $hooks_hash . ':' . drupal_hash_base64($serialize_function($full_css)); + } + if (advagg_file_aggregation_enabled('js')) { + // Generate js cache id. + $cids[] = $js_cache_id = 'advagg:js:full:1.1:' . $hooks_hash . ':' . drupal_hash_base64($serialize_function($js_scope_array)); + } + + if (!empty($cids)) { + // Get the cached data. + $cached_data = cache_get_multiple($cids, 'cache_advagg_aggregates'); + + // Set variables from the cache. + if (isset($cached_data[$css_cache_id])) { + $css_cache = $cached_data[$css_cache_id]; + } + if (isset($cached_data[$js_cache_id])) { + $js_cache = $cached_data[$js_cache_id]; + } + } + + // Special handling if the css is loaded via JS. + if (!empty($css_cache) + && empty($js_cache) + && advagg_css_in_js($css_cache) + ) { + // If CSS is being loaded via JavaScript and the css cache is set but the + // js cache is not set; then unset the css cache as well. + unset($css_cache); + } + + // Set to empty arrays on a cache miss. + if (!isset($css_cache)) { + $css_cache = new stdClass(); + } + if (!isset($js_cache)) { + $js_cache = new stdClass(); + } + return array($css_cache, $js_cache, $css_cache_id, $js_cache_id); +} + +/** + * Replacement for template_process_html(). + */ +function _advagg_process_html(&$variables) { + // Don't fail even if the menu router failed. + if (drupal_get_http_header('status') === '404 Not Found') { + // See if the URI contains advagg. + $uri = request_uri(); + if (stripos($uri, '/advagg_') !== FALSE) { + $advagg_items = advagg_menu(); + + // Check css. + $css = reset($advagg_items); + $css_path = key($advagg_items); + $css_path = substr($css_path, 0, strlen($css_path) - 1); + $css_start = strpos($uri, $css_path); + if ($css_start !== FALSE) { + $filename = substr($uri, $css_start + strlen($css_path)); + } + + // Check js. + if (empty($filename)) { + $js = next($advagg_items); + $js_path = key($advagg_items); + $js_path = substr($js_path, 0, strlen($js_path) - 1); + $js_start = strpos($uri, $js_path); + if ($js_start !== FALSE) { + $filename = substr($uri, $js_start + strlen($js_path)); + } + } + + // If we have a filename call the page callback. + if (!empty($filename)) { + $router_item = $css; + if (isset($js)) { + $router_item = $js; + } + + // Include the file if needed. + if ($router_item['file']) { + $included = module_load_include($router_item['file'], 'advagg'); + if (!$included && !function_exists($router_item['page callback'])) { + $file = DRUPAL_ROOT . '/' . drupal_get_path('module', 'advagg') . '/' . $router_item['file']; + if (is_file($file)) { + require_once $file; + } + } + } + + // Call the function. + if (function_exists($router_item['page callback'])) { + // Strip query and fragment form the filename. + if ($pos = strpos($filename, '?')) { + $filename = substr($filename, 0, $pos); + } + if ($pos = strpos($filename, '#')) { + $filename = substr($filename, 0, $pos); + } + // Generate the file. + call_user_func_array($router_item['page callback'], array($filename)); + } + else { + // Report the bigger issue to watchdog. + watchdog('advagg', 'You need to flush your menu cache. This can be done at the top of the performance page. The advagg callback failed while trying to generate this file: @uri', array( + '@performance' => url('admin/config/development/performance'), + '@uri' => $uri, + ), WATCHDOG_CRITICAL); + } + } + } + } + + if (!advagg_enabled()) { + template_process_html($variables); + return; + } + + // Render page_top and page_bottom into top level variables. + if (isset($variables['page']) && is_array($variables['page']) && isset($variables['page']['page_top'])) { + $variables['page_top'] = drupal_render($variables['page']['page_top']); + } + elseif (!isset($variables['page_top'])) { + $variables['page_top'] = ''; + } + if (isset($variables['page']) && is_array($variables['page']) && isset($variables['page']['page_bottom'])) { + $variables['page_bottom'] = drupal_render($variables['page']['page_bottom']); + } + elseif (!isset($variables['page_bottom'])) { + $variables['page_bottom'] = ''; + } + // Place the rendered HTML for the page body into a top level variable. + if (isset($variables['page']) && is_array($variables['page']) && isset($variables['page']['#children'])) { + $variables['page'] = $variables['page']['#children']; + } + $advagg_script_alt_scope_scripts = array(); + if (variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE)) { + $prefix = ""; + $suffix = ""; + $variables['page'] = $prefix . $variables['page'] . $suffix; + $prefix = ""; + $suffix = ""; + $variables['page_top'] = $prefix . $variables['page_top'] . $suffix; + $prefix = ""; + $suffix = ""; + $variables['page_bottom'] = $prefix . $variables['page_bottom'] . $suffix; + + $matches = array(); + preg_match_all('//', $variables['page_top'], $matches); + $advagg_script_alt_scope_scripts = array_merge($matches[1], $advagg_script_alt_scope_scripts); + preg_match_all('//', $variables['page'], $matches); + $advagg_script_alt_scope_scripts = array_merge($matches[1], $advagg_script_alt_scope_scripts); + preg_match_all('//', $variables['page_bottom'], $matches); + $advagg_script_alt_scope_scripts = array_merge($matches[1], $advagg_script_alt_scope_scripts); + } + + // Parts of drupal_get_html_head(). + $elements = drupal_add_html_head(); + if (is_callable('advagg_mod_html_head_post_alter')) { + advagg_mod_html_head_post_alter($elements); + } + + // Get default javascript. + // @see http://drupal.org/node/1279226 + if (function_exists('drupal_add_js_page_defaults')) { + drupal_add_js_page_defaults(); + } + $javascript = array(); + + // Try the render cache. + if (!variable_get('advagg_debug', ADVAGG_DEBUG)) { + // No Alter. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 5 && !module_exists('advagg_relocate')) { + // Get all CSS and JS variables needed; running no alters. + list($variables['css'], $full_css) = _advagg_build_css_arrays_for_rendering(TRUE); + list($javascript, $js_scope_settings_array, $js_scope_array) = _advagg_build_js_arrays_for_rendering(TRUE); + + // Get the render cache. + list($css_cache, $js_cache, $css_cache_id_no_alter, $js_cache_id_no_alter) = advagg_get_render_cache($full_css, $js_scope_array); + } + + // With Alter. + if ((empty($css_cache->data) || empty($js_cache->data)) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 3) { + // Get all CSS and JS variables needed; running alters. + list($variables['css'], $full_css) = _advagg_build_css_arrays_for_rendering(); + list($javascript, $js_scope_settings_array, $js_scope_array) = _advagg_build_js_arrays_for_rendering(); + + // Get the render cache. + list($css_cache, $js_cache, $css_cache_id, $js_cache_id) = advagg_get_render_cache($full_css, $js_scope_array); + } + } + + // CSS has nice hooks so we don't need to work around it. + if (!empty($css_cache->data)) { + // Use render cache. + list($variables['styles'], $full_css) = $css_cache->data; + } + else { + // Get the css if we have not done so. + if (empty($full_css)) { + list($variables['css'], $full_css) = _advagg_build_css_arrays_for_rendering(); + } + // Render the CSS; advagg_pre_render_styles() gets called here. + $variables['styles'] = drupal_render($full_css); + if (!empty($css_cache_id) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 3) { + // Save to the cache. + cache_set($css_cache_id, array($variables['styles'], $full_css), 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + if (!empty($css_cache_id_no_alter) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 5) { + // Save to the cache. + cache_set($css_cache_id_no_alter, array($variables['styles'], $full_css), 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + } + + if (module_exists('advagg_font') && variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER)) { + $fonts = array(); + foreach ($full_css['#groups'] as $groups) { + if (isset($groups['items']['files'])) { + foreach ($groups['items']['files'] as $file) { + if (isset($file['advagg_font'])) { + foreach ($file['advagg_font'] as $class => $name) { + $fonts[$class] = $name; + } + } + } + } + } + if (!empty($fonts)) { + if (isset($js_scope_settings_array)) { + $key = key($js_scope_settings_array); + $js_scope_settings_array[$key]['settings']['data'][] = array('advagg_font' => $fonts); + } + drupal_add_js(array('advagg_font' => $fonts), array('type' => 'setting')); + } + } + + if (variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD)) { + foreach ($full_css['#groups'] as $groups) { + if (empty($groups['data']) || $groups['type'] === 'inline') { + continue; + } + advagg_add_preload_header(advagg_convert_abs_to_rel(file_create_url($groups['data'])), 'style'); + } + } + + // JS needs hacks. + // Clear out all old scripts. + if (variable_get('advagg_clear_scripts', ADVAGG_CLEAR_SCRIPTS)) { + $variables['scripts'] = ''; + } + if (!isset($variables['scripts'])) { + $variables['scripts'] = ''; + } + if (!isset($variables['page_bottom']) || !is_string($variables['page_bottom'])) { + $variables['page_bottom'] = ''; + } + + $use_cache = FALSE; + if (!empty($js_cache->data) && !variable_get('advagg_debug', ADVAGG_DEBUG)) { + // Use render cache. + $use_cache = TRUE; + $add_to_variables = array(); + // Replace cached settings with current ones. + $js_settings_used = array(); + + $js_scope_settings_array_copy = $js_scope_settings_array; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 5) { + if (!empty($js_scope_settings_array_copy['header']) && empty($js_scope_settings_array_copy['footer'])) { + // Copy header settings into the footer. + $js_scope_settings_array_copy['footer'] = $js_scope_settings_array_copy['header']; + } + } + + list($js_cache_data, $js_scope_array) = $js_cache->data; + + foreach ($js_cache_data as $scope => $value) { + $scope_settings = $scope; + if ($scope_settings === 'scripts') { + $scope_settings = 'header'; + } + if ($scope === 'page_bottom') { + $scope_settings = 'footer'; + } + // Search $value for Drupal.settings. + $start = strpos($value, 'jQuery.extend(Drupal.settings,'); + if ($start !== FALSE) { + // If the cache and current settings scope's do not match; do not use + // the cached version. + if (!isset($js_scope_settings_array_copy[$scope_settings]['settings'])) { + $use_cache = FALSE; + break; + } + + // Replace cached Drupal.settings with current Drupal.settings for this + // page. + $merged = advagg_cleanup_settings_array(drupal_array_merge_deep_array(array_filter($js_scope_settings_array_copy[$scope_settings]['settings']['data'], 'is_array'))); + $json_data = advagg_json_encode($merged); + if (!empty($json_data)) { + // Record that this is being used. + $js_settings_used[$scope_settings] = TRUE; + + // Replace the drupal settings string. + $value = advagg_replace_drupal_settings_string($value, $json_data); + } + } + $add_to_variables[$scope] = $value; + } + + if ($use_cache) { + $all_used = array_diff(array_keys($js_scope_settings_array_copy), array_keys($js_settings_used)); + // Ignore this check if the cache level is less than 5. + if (!empty($all_used) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 5 && !empty($js_settings_used)) { + // Some js settings did not make it into the output. Skip cache. + $use_cache = FALSE; + } + } + + if ($use_cache) { + // Using the cache; write to the $variables array. + foreach ($add_to_variables as $scope => $value) { + // Set the scope variable if not set. + if (!isset($variables[$scope]) || !is_string($variables[$scope])) { + $variables[$scope] = ''; + } + // Append the js to the scope. + $variables[$scope] .= $value; + } + } + } + + // If the cache isn't used. + if (!$use_cache) { + if (!empty($js_cache->data) && !empty($css_cache->data) && advagg_css_in_js($css_cache)) { + // Render the css so it will be added to the js array; + // advagg_pre_render_styles() gets called here. + $variables['styles'] = drupal_render($full_css); + } + + // Check if the js has changed. + $new_js = drupal_add_js(); + $diff = array_diff(array_keys($new_js), array_keys($javascript)); + if (!empty($diff) || empty($javascript)) { + // Get all JS variables needed again because js changed; or because we + // never got them in the first place. + list($javascript, $js_scope_settings_array, $js_scope_array) = _advagg_build_js_arrays_for_rendering(); + } + + $js_cache = array(); + $js_cache['scripts'] = ''; + if (!empty($js_scope_array)) { + // Add JS to the header and footer of the page. + foreach ($js_scope_array as $scope => &$scripts_array) { + // Add js settings. + if (!empty($js_scope_settings_array[$scope]['settings'])) { + $scripts_array['#items']['settings'] = $js_scope_settings_array[$scope]['settings']; + } + // Render js; advagg_pre_render_scripts() gets called here. + $scripts = drupal_render($scripts_array); + + if ($scope === 'header') { + // Add to the top of this section. + $variables['scripts'] = $scripts . $variables['scripts']; + $js_cache['scripts'] = $scripts . $js_cache['scripts']; + } + // Footer scripts. + elseif ($scope === 'footer') { + // Add to the bottom of this section. + $variables['page_bottom'] .= $scripts; + $js_cache['page_bottom'] = $scripts; + } + // Above css scripts. + elseif ($scope === 'above_css') { + // Put in this new section. + $variables['above_css'] = $scripts; + $js_cache['above_css'] = $scripts; + } + elseif (variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE)) { + // Scripts in other places. + if (isset($variables[$scope]) + && is_string($variables[$scope]) + && array_key_exists($scope, $GLOBALS['theme_info']->info['regions']) + ) { + // Add to the bottom of this section. + $variables[$scope] .= $scripts; + $js_cache[$scope] = $scripts; + } + elseif (array_search($scope, $advagg_script_alt_scope_scripts, TRUE) !== FALSE) { + // Add to the inline html. + $pos_page_top = strpos($variables['page_top'], ""); + $pos_page = strpos($variables['page'], ""); + $pos_page_bottom = strpos($variables['page_bottom'], ""); + if ($pos_page_top !== FALSE) { + $pos_page_top += strlen(""); + $variables['page_top'] = substr_replace($variables['page_top'], "\n$scripts", $pos_page_top, 0); + $js_cache[$scope] = $scripts; + } + elseif ($pos_page !== FALSE) { + $pos_page += strlen(""); + $variables['page'] = substr_replace($variables['page'], "\n$scripts", $pos_page, 0); + $js_cache[$scope] = $scripts; + } + elseif ($pos_page_bottom !== FALSE) { + $pos_page_bottom += strlen(""); + $variables['page_bottom'] = substr_replace($variables['page_bottom'], "\n$scripts", $pos_page_bottom, 0); + $js_cache[$scope] = $scripts; + } + } + // Add javascript to scripts if we can't find the region in the theme. + elseif (strpos($scope, ':') === FALSE) { + // Add to the bottom of this section. + $variables['scripts'] .= $scripts; + $js_cache['scripts'] .= $scripts; + } + } + } + unset($scripts_array); + + // Clear drupal settings so cache is smaller. + foreach ($js_cache as &$string) { + $string = advagg_replace_drupal_settings_string($string, '{}'); + } + unset($string); + + // Clear drupal settings and not needed items from render cache. + $js_scope_array = array_intersect_key($js_scope_array, array_flip(element_children($js_scope_array))); + foreach ($js_scope_array as $scope => &$scripts_array) { + // Clear element children. + $scripts_array = array_diff_key($scripts_array, array_flip(element_children($scripts_array))); + if (isset($scripts_array['#children'])) { + unset($scripts_array['#children']); + } + // Clear drupal settings. + if (isset($scripts_array['#items']['settings']['data']) && is_array($scripts_array['#items']['settings']['data'])) { + $scripts_array['#items']['settings']['data'] = array(); + } + // Clear printed keys. + if (isset($scripts_array['#printed'])) { + unset($scripts_array['#printed']); + } + // Clear not used groups. + foreach ($scripts_array['#groups'] as $key => $groups) { + if (!isset($groups['items']['files'])) { + unset($scripts_array['#groups'][$key]); + } + } + } + unset($scripts_array); + + if (!empty($js_cache_id) && !empty($js_cache) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 3) { + cache_set($js_cache_id, array($js_cache, $js_scope_array), 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + if (!empty($js_cache_id_no_alter) && !empty($js_cache) && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 5) { + cache_set($js_cache_id_no_alter, array($js_cache, $js_scope_array), 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + } + } + if (!empty($variables['above_css'])) { + $variables['styles'] = $variables['above_css'] . $variables['styles']; + } + + if (variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD)) { + foreach ($js_scope_array as $scope => &$scripts_array) { + if ($scope !== 'header' + && $scope !== 'footer' + && $scope !== 'above_css' + && !variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE) + ) { + continue; + } + + foreach ($scripts_array['#groups'] as $groups) { + if (empty($groups['data']) || $groups['type'] === 'inline') { + continue; + } + advagg_add_preload_header(advagg_convert_abs_to_rel(file_create_url($groups['data'])), 'script'); + } + } + } + + $head_elements_before = drupal_add_html_head(); + if (variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT) + || variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD) + ) { + // Prefetch css domains. + foreach ($full_css['#items'] as $file) { + advagg_add_resource_hints_array($file); + } + foreach ($full_css['#groups'] as $groups) { + if (isset($groups['items']['files'])) { + foreach ($groups['items']['files'] as $file) { + advagg_add_resource_hints_array($file); + } + } + } + + // Prefetch js domains. + foreach ($js_scope_array as $scope_js) { + foreach ($scope_js['#items'] as $file) { + advagg_add_resource_hints_array($file); + } + if (isset($scope_js['#groups'])) { + foreach ($scope_js['#groups'] as $groups) { + if (isset($groups['items']['files'])) { + foreach ($groups['items']['files'] as $file) { + advagg_add_resource_hints_array($file); + } + } + } + } + } + } + + // Add in preload link headers. + advagg_add_preload_header(); + + // Add in the headers added by advagg. + $head_elements_after = drupal_add_html_head(); + $elements += array_diff_key($head_elements_after, $head_elements_before); + + // Parts of drupal_get_html_head(). + drupal_alter('html_head', $elements); + $head = drupal_render($elements); + if (variable_get('advagg_html_head_in_css_location', ADVAGG_HTML_HEAD_IN_CSS_LOCATION)) { + $variables['styles'] = $head . $variables['styles']; + $variables['head'] = ''; + } + else { + $variables['head'] = $head; + } + + // Remove AdvAgg comments. + if (variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE) + && !empty($advagg_script_alt_scope_scripts) + && !variable_get('theme_debug', FALSE) + ) { + $variables['page_top'] = preg_replace('//', '', $variables['page_top']); + $variables['page'] = preg_replace('//', '', $variables['page']); + $variables['page_bottom'] = preg_replace('//', '', $variables['page_bottom']); + } + + // Output debug info. + if (variable_get('advagg_debug', ADVAGG_DEBUG)) { + $debug = $GLOBALS['_advagg']['debug']; + if (is_callable('httprl_pr')) { + $output = ' ' . httprl_pr($debug); + } + else { + $output = '
' . str_replace(array('<', '>'), array('<', '>'), print_r($debug, TRUE)) . '
'; + } + watchdog('advagg_debug', $output, array(), WATCHDOG_DEBUG); + } +} + +/** + * Replace inline drupal settings script. + * + * @param string $subject + * Inline js. + * @param string $replace + * JS settings replacement. + * + * @return string + * Returns the subject with the replacement in place if this is a drupal + * settings json blob. + */ +function advagg_replace_drupal_settings_string($subject, $replace) { + $start = strpos($subject, 'jQuery.extend(Drupal.settings,'); + if ($start === FALSE) { + return $subject; + } + + // Find the end of the Drupal.settings. + $script_end = stripos($subject, '', $start); + $settings_substring = substr($subject, $start, $script_end - $start); + $json_end = strripos($settings_substring, '});'); + + // Check if LABjs has added an additional wrapper around Drupal settings. + $script_tag_start = strripos(substr($subject, 0, $start), ' $value) { + if (advagg_remove_short_keys($key)) { + if (is_array($data['ajaxPageState']['js']) && isset($data['ajaxPageState']['js'][$key])) { + unset($data['ajaxPageState']['js'][$key]); + } + elseif (is_object($data['ajaxPageState']['js']) && isset($data['ajaxPageState']['js']->{$key})) { + unset($data['ajaxPageState']['js']->{$key}); + } + } + } + } + // Remove inline css from the ajaxPageState data. + if (isset($data['ajaxPageState']['css'])) { + foreach ((array) $data['ajaxPageState']['css'] as $key => $value) { + if (advagg_remove_short_keys($key, 6)) { + if (is_object($data['ajaxPageState']['css']) && isset($data['ajaxPageState']['css']->{$key})) { + unset($data['ajaxPageState']['css']->{$key}); + } + elseif (is_array($data['ajaxPageState']['css']) && isset($data['ajaxPageState']['css'][$key])) { + unset($data['ajaxPageState']['css'][$key]); + } + } + } + } + // Remove settings from the js ajaxPageState data. + if (isset($data['ajaxPageState']['js']['settings'])) { + unset($data['ajaxPageState']['js']['settings']); + } + if (isset($data['ajaxPageState']['js']->settings)) { + unset($data['ajaxPageState']['js']->settings); + } + return $data; +} + +/** + * Find dns_prefetch and call advagg_add_dns_prefetch(). + * + * @param array $values + * Attributes added via code for the file. + */ +function advagg_add_resource_hints_array(array $values) { + if (variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH) + || variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT)) { + if (!empty($values['type']) + && ($values['type'] === 'external' || $values['type'] === 'file') + ) { + // Get external domains. + advagg_add_dns_prefetch($values['data']); + } + if (!empty($values['dns_prefetch'])) { + // Grab domains that will be access when this file is loaded. + if (is_array($values['dns_prefetch'])) { + foreach ($values['dns_prefetch'] as $url) { + advagg_add_dns_prefetch($url); + } + } + else { + advagg_add_dns_prefetch($values['dns_prefetch']); + } + } + } + if (!empty($values['preload']) && variable_get('advagg_resource_hints_preload', ADVAGG_RESOURCE_HINTS_PRELOAD)) { + if (is_array($values['preload'])) { + foreach ($values['preload'] as $url) { + advagg_add_preload_header($url); + } + } + else { + advagg_add_preload_header($values['preload']); + } + } +} + +/** + * Add in the dns-prefetch header for CSS and JS external files. + * + * @param string $url + * The url of the external host. + * + * @return bool + * TRUE if it was added to the head. + */ +function advagg_add_dns_prefetch($url) { + // Keep the order. + $advagg_resource_hints_location = variable_get('advagg_resource_hints_location', ADVAGG_RESOURCE_HINTS_LOCATION); + static $weight = -1001; + if ($advagg_resource_hints_location == 3) { + $weight = -999.9; + } + $weight += 0.0001; + + // Get the host. + $parse = @parse_url($url); + if (empty($parse['host'])) { + // If just the hostname was given, build proper url. + if (strpos($url, '.') && strpos($url, '/') === FALSE) { + $parse['scheme'] = '//'; + $parse['host'] = $url; + // Check for fragment. + $pos = strpos($url, '#'); + if ($pos !== FALSE) { + $parse['fragment'] = substr($url, $pos + 1); + $parse['host'] = substr($url, 0, $pos); + } + // Put it back together and parse again. + $url = advagg_glue_url($parse); + $parse = @parse_url($url); + } + if (empty($parse['host'])) { + return FALSE; + } + } + + // Filter out wrong schemes. + if (!empty($parse['scheme']) + && $parse['scheme'] !== 'http' + && $parse['scheme'] !== 'https' + ) { + return FALSE; + } + + // Filter out local host. + $host = @parse_url($GLOBALS['base_root'], PHP_URL_HOST); + if ($parse['host'] === $host) { + return FALSE; + } + + // Add DNS information for more domains. + if (strpos($parse['host'], 'fonts.googleapis.com') !== FALSE) { + // Add fonts.gstatic.com when fonts.googleapis.com is added. + advagg_add_dns_prefetch('https://fonts.gstatic.com/#crossorigin'); + } + + // Build render array. + if (variable_get('advagg_resource_hints_dns_prefetch', ADVAGG_RESOURCE_HINTS_DNS_PREFETCH)) { + $element = array( + '#type' => 'html_tag', + '#tag' => 'link', + '#attributes' => array( + 'rel' => 'dns-prefetch', + 'href' => '//' . $parse['host'], + ), + '#weight' => $weight, + ); + // Add markup for dns-prefetch to html_head. + drupal_add_html_head($element, 'advagg_resource_hints_dns_prefetch:' . $parse['host']); + } + if (variable_get('advagg_resource_hints_preconnect', ADVAGG_RESOURCE_HINTS_PRECONNECT)) { + // HTTPS use Protocol Relative; HTTP and scheme defined use given scheme. + $href = '//' . $parse['host']; + if (!$GLOBALS['is_https'] && isset($parse['scheme'])) { + $href = "{$parse['scheme']}://{$parse['host']}"; + } + + $element = array( + '#type' => 'html_tag', + '#tag' => 'link', + '#attributes' => array( + 'rel' => 'preconnect', + 'href' => $href, + ), + '#weight' => $weight, + ); + if (!empty($parse['fragment']) && $parse['fragment'] === 'crossorigin') { + $element['#attributes']['crossorigin'] = ''; + } + // Add markup for dns-prefetch to html_head. + drupal_add_html_head($element, 'advagg_resource_hints_preconnect:' . $parse['host']); + } + + // Build render array. Goes after charset tag. + if (!empty($parse['fragment']) && $parse['fragment'] === 'prefetch') { + // Hacky way to open up a connection to the remote host. + $element = array( + '#type' => 'html_tag', + '#tag' => 'link', + '#attributes' => array( + 'rel' => 'prefetch', + 'href' => '//' . $parse['host'] . '/robots.txt', + ), + '#weight' => $weight, + ); + drupal_add_html_head($element, 'advagg_prefetch:' . $parse['host']); + } + return TRUE; +} + +/** + * Returns a themed representation of all stylesheets to attach to the page. + * + * It loads the CSS in order, with 'module' first, then 'theme' afterwards. + * This ensures proper cascading of styles so themes can easily override + * module styles through CSS selectors. + * + * Themes may replace module-defined CSS files by adding a stylesheet with the + * same filename. For example, themes/bartik/system-menus.css would replace + * modules/system/system-menus.css. This allows themes to override complete + * CSS files, rather than specific selectors, when necessary. + * + * If the original CSS file is being overridden by a theme, the theme is + * responsible for supplying an accompanying RTL CSS file to replace the + * module's. + * + * @param array $css + * (Optional) An array of CSS files. If no array is provided, the default + * stylesheets array is used instead. + * @param bool $skip_alter + * (Optional) If set to TRUE, this function skips calling drupal_alter() on + * $css, useful when the calling function passes a $css array that has already + * been altered. + * + * @return array + * An array ready to be passed into drupal_render(). + * + * @see drupal_add_css() + */ +function advagg_get_css(array $css = array(), $skip_alter = FALSE) { + if (empty($css)) { + $css = drupal_add_css(); + } + + // Allow modules and themes to alter the CSS items. + if (!$skip_alter) { + advagg_add_default_dns_lookups($css, 'css'); + // Call hook_css_alter(). + drupal_alter('css', $css); + // Call hook_css_post_alter(). + drupal_alter('css_post', $css); + // Call these advagg functions after the hook_css_alter was called. + advagg_fix_type($css, 'css'); + } + + // Sort CSS items, so that they appear in the correct order. + advagg_drupal_sort_css_js_stable($css); + + // Provide the page with information about the individual CSS files used, + // information not otherwise available when CSS aggregation is enabled. The + // setting is attached later in this function, but is set here, so that CSS + // files removed below are still considered "used" and prevented from being + // added in a later AJAX request. + // Skip if no files were added to the page or jQuery.extend() will overwrite + // the Drupal.settings.ajaxPageState.css object with an empty array. + if (!empty($css)) { + // Cast the array to an object to be on the safe side even if not empty. + $setting['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1); + } + + // Remove the overridden CSS files. Later CSS files override former ones. + $previous_item = array(); + foreach ($css as $key => $item) { + if ($item['type'] == 'file') { + // If defined, force a unique basename for this file. + $basename = isset($item['basename']) ? $item['basename'] : drupal_basename($item['data']); + if (isset($previous_item[$basename])) { + // Remove the previous item that shared the same base name. + unset($css[$previous_item[$basename]]); + } + $previous_item[$basename] = $key; + } + } + + // Remove empty files. + advagg_remove_empty_files($css); + + // Render the HTML needed to load the CSS. + $styles = array( + '#type' => 'styles', + '#items' => $css, + ); + + if (!empty($setting)) { + $styles['#attached']['js'][] = array('type' => 'setting', 'data' => $setting); + } + + return $styles; +} + +/** + * Get full JS array. + * + * Note that hook_js_alter(&$javascript) is called during this function call + * to allow alterations of the JavaScript during its presentation. Calls to + * drupal_add_js() from hook_js_alter() will not be added to the output + * presentation. The correct way to add JavaScript during hook_js_alter() + * is to add another element to the $javascript array, deriving from + * drupal_js_defaults(). See locale_js_alter() for an example of this. + * + * @param array $javascript + * (optional) An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * @param bool $skip_alter + * (optional) If set to TRUE, this function skips calling drupal_alter() on + * $javascript, useful when the calling function passes a $javascript array + * that has already been altered. + * + * @return array + * The raw JavaScript array. + * + * @see drupal_add_js() + * @see locale_js_alter() + * @see drupal_js_defaults() + */ +function advagg_get_full_js(array $javascript = array(), $skip_alter = FALSE) { + if (empty($javascript)) { + $javascript = drupal_add_js(); + } + + // Return an empty array if + // no javascript is used, + // only the settings array is used and scope is header. + if (empty($javascript) + || (isset($javascript['settings']) && count($javascript) == 1) + ) { + return array(); + } + + // Allow modules to alter the JavaScript. + if (!$skip_alter) { + advagg_add_default_dns_lookups($javascript, 'js'); + if (is_callable('advagg_mod_js_pre_alter')) { + advagg_mod_js_pre_alter($javascript); + } + // Call hook_js_alter(). + drupal_alter('js', $javascript); + // Call hook_js_post_alter(). + drupal_alter('js_post', $javascript); + // Call these advagg functions after the hook_js_alter was called. + advagg_fix_type($javascript, 'js'); + } + elseif (is_callable('advagg_mod_js_move_to_footer')) { + if (variable_get('advagg_mod_js_footer', ADVAGG_MOD_JS_FOOTER) == 3) { + advagg_mod_js_move_to_footer($javascript); + } + } + + // If in development mode make sure the ajaxPageState css is there. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $have_css = FALSE; + foreach ($javascript['settings']['data'] as $setting) { + if (!empty($setting['ajaxPageState']['css'])) { + $have_css = TRUE; + break; + } + } + if (!$have_css) { + $css = drupal_add_css(); + if (!empty($css)) { + // Cast the array to an object to be on the safe side even if not empty. + $javascript['settings']['data'][]['ajaxPageState']['css'] = (object) array_fill_keys(array_keys($css), 1); + } + } + } + + // Remove empty files. + advagg_remove_empty_files($javascript); + + return $javascript; +} + +/** + * Returns a themed presentation of all JavaScript code for the current page. + * + * References to JavaScript files are placed in a certain order: first, all + * 'core' files, then all 'module' and finally all 'theme' JavaScript files + * are added to the page. Then, all settings are output, followed by 'inline' + * JavaScript code. If running update.php, all preprocessing is disabled. + * + * Note that hook_js_alter(&$javascript) is called during this function call + * to allow alterations of the JavaScript during its presentation. Calls to + * drupal_add_js() from hook_js_alter() will not be added to the output + * presentation. The correct way to add JavaScript during hook_js_alter() + * is to add another element to the $javascript array, deriving from + * drupal_js_defaults(). See locale_js_alter() for an example of this. + * + * @param string $scope + * (optional) The scope for which the JavaScript rules should be returned. + * Defaults to 'header'. + * @param array $javascript + * (optional) An array with all JavaScript code. Defaults to the default + * JavaScript array for the given scope. + * @param bool $ajax + * (optional) If set to TRUE, this function will not output Drupal.settings. + * + * @return array + * An array ready to be passed into drupal_render() containing all JavaScript + * code segments and includes for the scope as HTML tags. + * + * @see drupal_add_js() + * @see locale_js_alter() + * @see drupal_js_defaults() + */ +function advagg_get_js($scope = 'header', array $javascript = array(), $ajax = FALSE) { + // Keep track of js added for ajaxPageState. + $page_state = &drupal_static(__FUNCTION__, array()); + + // Add in javascript if none was passed in. + if (empty($javascript) && !$ajax) { + $javascript = advagg_get_full_js(); + } + + // Return an empty array if no javascript is used. + if (empty($javascript)) { + return array(); + } + + // Filter out elements of the given scope. + $items = array(); + foreach ($javascript as $key => $item) { + if (!empty($item['scope']) && $item['scope'] === $scope) { + $items[$key] = $item; + } + } + + // Sort the JavaScript so that it appears in the correct order. + advagg_drupal_sort_css_js_stable($items); + + // In Drupal 8, there's a JS_SETTING group for making setting variables + // appear last after libraries have loaded. In Drupal 7, this is forced + // without that group. We do not use the $key => $item type of iteration, + // because PHP uses an internal array pointer for that, and we're modifying + // the array order inside the loop. + if ($scope === 'footer' && !empty($items['settings'])) { + // Remove settings array from items. + $settings_js['settings'] = $items['settings']; + unset($items['settings']); + + // Move $settings_js to the bottom of the js that was added to the + // header, but has now been moved to the footer via advagg_mod. + $counter = 0; + foreach ($items as $key => $item) { + if ($item['group'] > 9000) { + advagg_array_splice_assoc($items, $counter, 0, $settings_js); + unset($settings_js); + break; + } + ++$counter; + } + // Nothing in the footer, add settings to the bottom of the array. + if (isset($settings_js)) { + $items = array_merge($items, $settings_js); + } + } + else { + foreach (array_keys($items) as $key) { + if ($items[$key]['type'] === 'setting') { + $item = $items[$key]; + unset($items[$key]); + $items[$key] = $item; + } + } + } + + // Provide the page with information about the individual JavaScript files + // used, information not otherwise available when aggregation is enabled. + $page_state = array_merge($page_state, array_fill_keys(array_keys($items), 1)); + + // If we're outputting the header scope, then this should be the final time + // that drupal_get_js() is running, so add the setting to this output as well + // as to the drupal_add_js() cache. If $items['settings'] doesn't exist, it's + // because drupal_get_js() was intentionally passed a $javascript argument + // stripped of settings, potentially in order to override how settings get + // output, so in this case, do not add the setting to this output. + // Also output the settings if we have pushed all javascript to the footer. + if (isset($items['settings'])) { + $items['settings']['data'][] = array( + 'ajaxPageState' => array( + 'js' => $page_state, + ), + ); + } + + // Do not include jQuery.extend(Drupal.settings) if the output is ajax. + if ($ajax) { + unset($items['settings']['data']); + } + + // Semi support of the attributes array. + foreach ($items as $key => $item) { + if (!isset($item['attributes'])) { + continue; + } + if (isset($item['attributes']['defer'])) { + $items[$key]['defer'] = $item['attributes']['defer']; + } + if (isset($item['attributes']['async'])) { + $items[$key]['async'] = $item['attributes']['async']; + } + if (isset($item['attributes']['onload'])) { + $items[$key]['onload'] = $item['attributes']['onload']; + } + if (isset($item['attributes']['onerror'])) { + $items[$key]['onerror'] = $item['attributes']['onerror']; + } + } + + // Render the HTML needed to load the JavaScript. + $elements = array( + '#type' => 'scripts', + '#items' => $items, + ); + + // Aurora and Omega themes uses alter without checking previous value. + if (variable_get('advagg_enforce_scripts_callback', TRUE)) { + // Get the element_info for scripts. + $scripts = element_info('scripts'); + if (empty($scripts) || $scripts['#aggregate_callback'] !== '_advagg_aggregate_js') { + // Directly alter the static. + $element_info = &drupal_static('element_info'); + advagg_element_info_alter($element_info); + if (function_exists('advagg_mod_element_info_alter')) { + advagg_mod_element_info_alter($element_info); + } + } + } + + // Remove ajaxPageState CSS/JS from Drupal.settings if ajax.js is not used. + if (function_exists('advagg_mod_js_no_ajaxpagestate')) { + if (variable_get('advagg_mod_js_no_ajaxpagestate', ADVAGG_MOD_JS_NO_AJAXPAGESTATE)) { + advagg_mod_js_no_ajaxpagestate($elements); + } + } + return $elements; +} + +/** + * Remove a portion of the array and replace it with something else. + * + * @param array $input + * The input array. + * @param int $offset + * If offset is positive then the start of removed portion is at that offset + * from the beginning of the input array. If offset is negative then it starts + * that far from the end of the input array. + * @param int $length + * If length is omitted, removes everything from offset to the end of the + * array. If length is specified and is positive, then that many elements will + * be removed. If length is specified and is negative then the end of the + * removed portion will be that many elements from the end of the array. Tip: + * to remove everything from offset to the end of the array when replacement + * is also specified, use count($input) for length. + * @param mixed $replacement + * If replacement array is specified, then the removed elements are replaced + * with elements from this array. + * If offset and length are such that nothing is removed, then the elements + * from the replacement array are inserted in the place specified by the + * offset. Note that keys in replacement array are preserved. + * If replacement is just one element it is not necessary to put array() + * around it, unless the element is an array itself, an object or NULL. + * + * @see http://php.net/array-splice#111204 + */ +function advagg_array_splice_assoc(array &$input, $offset, $length, $replacement) { + $replacement = (array) $replacement; + $key_indices = array_flip(array_keys($input)); + if (isset($input[$offset]) && is_string($offset)) { + $offset = $key_indices[$offset]; + } + if (isset($input[$length]) && is_string($length)) { + $length = $key_indices[$length] - $offset; + } + + $input = array_slice($input, 0, $offset, TRUE) + $replacement + array_slice($input, $offset + $length, NULL, TRUE); +} + +/** + * Callback for array_filter. Will return FALSE if strlen < 3. + * + * @param string $value + * A value from an array/object. + * @param int $min_len + * The strlen check length. + * + * @return bool + * TRUE or FALSE. + */ +function advagg_remove_short_keys($value, $min_len = 3) { + if (strlen($value) < $min_len) { + return TRUE; + } + else { + return FALSE; + } +} + +/** + * Get all javascript scopes set in the $javascript array. + * + * @param array $javascript + * An array with all JavaScript code. + * + * @return array + * Array of scopes that are currently being used. + */ +function advagg_get_js_scopes(array $javascript) { + // Return if nothing given to us. + if (empty($javascript)) { + return array(); + } + + // Filter out elements of the given scope. + $scopes = array(); + $js_settings_in_footer = FALSE; + foreach ($javascript as $name => $item) { + // Skip if the scope is not set. + if (!is_array($item) || empty($item['scope'])) { + continue; + } + if (!isset($scopes[$item['scope']])) { + $scopes[$item['scope']] = TRUE; + } + if ($name === 'settings' && $item['scope'] === 'footer') { + $js_settings_in_footer = TRUE; + } + } + + // Default to header if nothing found. + if (empty($scopes)) { + $scopes['header'] = TRUE; + } + + // Process header last. + if (isset($scopes['header']) && count($scopes) > 1) { + $temp = $scopes['header']; + unset($scopes['header']); + $scopes['header'] = $temp; + } + + // Process footer last if everything has been moved to the footer. + if (isset($scopes['footer']) + && count($scopes) > 1 + && $js_settings_in_footer + ) { + $temp = $scopes['footer']; + unset($scopes['footer']); + $scopes['footer'] = $temp; + } + + // Return the scopes. + return $scopes; +} + +/** + * Apply the advagg changes to the $css_js_groups array. + * + * @param array $css_js_groups + * An array of CSS or JS groups as returned by drupal_group_css/js(). + * @param array $plans + * An array of changes to do to the $css_js_groups array. + * + * @return array + * New version of $css_js_groups. + */ +function advagg_merge_plans(array $css_js_groups, array $plans) { + $used_keys = array(); + foreach ($plans as $plan) { + $plan_added = FALSE; + foreach ($css_js_groups as $key => $group) { + // Remove files from the old css/js array. + $file_removed = FALSE; + foreach ($css_js_groups[$key]['items'] as $k => $values) { + if (is_array($values) + && array_key_exists('data', $values) + && is_array($plan['items']['files']) + && is_string($values['data']) + ) { + // If the CSS is a split file, the first file is very meaningful, and + // is probably the only file. + $first_file = reset($plan['items']['files']); + if (array_key_exists($values['data'], $plan['items']['files'])) { + unset($css_js_groups[$key]['items'][$k]); + $file_removed = TRUE; + } + // This part will try to add each split part matching the original CSS + // path and only remove the original group if the current part is the + // last part. + elseif (!empty($first_file['split'])) { + if ($values['data'] == $first_file['split_original']) { + if (!empty($first_file['split_last_part'])) { + unset($css_js_groups[$key]['items'][$k]); + } + $file_removed = TRUE; + } + } + } + } + + // Replace first file of the old css/js array with one from advagg. + if ($file_removed && !$plan_added) { + $step = 0; + do { + ++$step; + $insert_key = '' . floatval($key) . '.' . sprintf('%03d', $step); + } while (array_key_exists($insert_key, $css_js_groups)); + $css_js_groups[(string) $insert_key] = $plan; + $plan_added = TRUE; + } + } + + // Remove old css/js grouping if no files are left in it. + foreach ($css_js_groups as $key => $group) { + if (empty($css_js_groups[$key]['items'])) { + unset($css_js_groups[$key]); + } + } + + if (!$plan_added) { + foreach ($css_js_groups as $key => $group) { + if (empty($group['items']['aggregate_filenames_hash']) + || $group['items']['aggregate_filenames_hash'] != $plan['items']['aggregate_filenames_hash'] + || empty($group['items']['aggregate_contents_hash']) + || $group['items']['aggregate_contents_hash'] != $plan['items']['aggregate_contents_hash'] + ) { + continue; + } + + // Insert a unique key. + do { + $key = '' . (floatval($key) + 0.01); + } while (array_key_exists((string) $key, $css_js_groups) || array_key_exists((string) $key, $used_keys)); + $used_keys[(string) $key] = TRUE; + $css_js_groups[(string) $key] = $plan; + $plan_added = TRUE; + break; + } + } + + } + + // Key sort and normalize the array before returning it. + ksort($css_js_groups); + $css_js_groups = array_values($css_js_groups); + return $css_js_groups; +} + +/** + * Function used to see if aggregation is enabled. + * + * @return bool + * The value of the advagg_enabled variable. + */ +function advagg_enabled() { + $init = &drupal_static(__FUNCTION__); + + if (!empty($init)) { + return variable_get('advagg_enabled', ADVAGG_ENABLED); + } + + // Set base_path if not set. + if (empty($GLOBALS['base_path'])) { + $GLOBALS['base_path'] = rtrim(dirname($_SERVER['SCRIPT_NAME']), '\/') . '/'; + } + + $init = TRUE; + // Disable AdvAgg if module needs to be upgraded from 1.x to 2.x. + if (variable_get('advagg_needs_update', ADVAGG_NEEDS_UPDATE)) { + if (!db_table_exists('advagg_aggregates_versions')) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + if (user_access('administer site configuration')) { + drupal_set_message(t('Please run database updates. AdvAgg will remain disabled until done.', array('@link' => url('update.php'))), 'error'); + } + } + else { + variable_del('advagg_needs_update'); + } + } + else { + // Get values and fill in defaults if needed. + $config_path = advagg_admin_config_root_path(); + $current_path = current_path(); + $arg = arg(); + $arg += array(1 => '', 2 => '', 3 => '', 4 => '', 5 => ''); + $admin_theme = variable_get('admin_theme'); + + // List of all the pages which will not have Advanced Aggregator enabled. + $list_of_pages = variable_get('advagg_disable_on_listed_pages'); + + $pages = trim(drupal_strtolower($list_of_pages)); + // Convert the Drupal path to lowercase. + $path = drupal_strtolower(drupal_get_path_alias(current_path())); + + // Compare the lowercase internal and lowercase path alias (if any). + $page_match = drupal_match_path($path, $pages); + if ($page_match) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + $GLOBALS['conf']['preprocess_css'] = FALSE; + $GLOBALS['conf']['preprocess_js'] = FALSE; + } + + // Disable advagg if on admin page and configured to do so. + // AND theme is admin theme + // AND NOT /admin/reports/status + // AND NOT /admin/config/development/performance/. + // AND NOT /admin/appearance/settings/*. + // AND NOT /admin/config/development/performance/advagg/*. + if (variable_get('advagg_disable_on_admin', ADVAGG_DISABLE_ON_ADMIN) + && $GLOBALS['theme'] === $admin_theme + && path_is_admin($current_path) + && !($arg[1] === 'reports' && $arg[2] === 'status') + && !($arg[2] === 'development' && $arg[3] === 'performance' && empty($arg[4])) + && !($arg[1] === 'appearance' && $arg[2] === 'settings' && !empty($arg[3])) + && stripos($current_path, $config_path . '/advagg') !== 0 + ) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + $GLOBALS['conf']['preprocess_css'] = FALSE; + $GLOBALS['conf']['preprocess_js'] = FALSE; + } + + // Check if the advagg cookie is set. + $cookie_name = 'AdvAggDisabled'; + $bypass_cookie = FALSE; + $key = drupal_hmac_base64('advagg_cookie', drupal_get_private_key() . drupal_get_hash_salt() . variable_get('cron_key', 'drupal')); + if (!empty($_COOKIE[$cookie_name]) && $_COOKIE[$cookie_name] == $key) { + $bypass_cookie = TRUE; + } + + // Allow for AdvAgg to be enabled per request. + if (isset($_GET['advagg']) + && $_GET['advagg'] == 1 + && !defined('MAINTENANCE_MODE') + && (user_access('bypass advanced aggregation') || $bypass_cookie) + ) { + $GLOBALS['conf']['advagg_enabled'] = TRUE; + $GLOBALS['conf']['preprocess_css'] = TRUE; + $GLOBALS['conf']['preprocess_js'] = TRUE; + } + + // Disable AdvAgg if maintenance mode is defined. + if (defined('MAINTENANCE_MODE')) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + } + // Only run code below if advagg is enabled. + if (variable_get('advagg_enabled', ADVAGG_ENABLED)) { + // Do not use AdvAgg or preprocessing functions if the disable cookie is + // set. + if ($bypass_cookie && !isset($_GET['advagg'])) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + $GLOBALS['conf']['preprocess_css'] = FALSE; + $GLOBALS['conf']['preprocess_js'] = FALSE; + $bypass_cookie = TRUE; + + // Let the user know that the AdvAgg bypass cookie is currently set. + static $msg_set; + if (!isset($msg_set) && variable_get('advagg_show_bypass_cookie_message', ADVAGG_SHOW_BYPASS_COOKIE_MESSAGE)) { + $msg_set = TRUE; + if (user_access('administer site configuration')) { + drupal_set_message(t('The AdvAgg bypass cookie is currently enabled. Turn it off by going to the AdvAgg Operations page and clicking the Toggle the "aggregation bypass cookie" for this browser button.', array( + '@advagg_operations' => url(advagg_admin_config_root_path() . '/advagg/operations', array('fragment' => 'edit-bypass')), + ))); + } + else { + drupal_set_message(t('The AdvAgg bypass cookie is currently enabled. Turn it off by logging in with a user with the "administer site configuration" permissions and going to the AdvAgg Operations page (located at @advagg_operations) and clicking the Toggle the "aggregation bypass cookie" for this browser button.', array( + '@login' => 'user/login', + '@advagg_operations' => advagg_admin_config_root_path() . '/advagg/operations', + ))); + } + } + } + // Disable advagg if requested. + if (isset($_GET['advagg']) + && $_GET['advagg'] == -1 + && (user_access('bypass advanced aggregation') || $bypass_cookie) + ) { + $GLOBALS['conf']['advagg_enabled'] = FALSE; + $GLOBALS['conf']['preprocess_css'] = FALSE; + $GLOBALS['conf']['preprocess_js'] = FALSE; + } + // Disable core preprocessing if requested. + if (isset($_GET['advagg-core']) + && $_GET['advagg-core'] == 0 + && (user_access('bypass advanced aggregation') || $bypass_cookie) + ) { + $GLOBALS['conf']['preprocess_css'] = FALSE; + $GLOBALS['conf']['preprocess_js'] = FALSE; + } + // Enable core preprocessing if requested. + if (isset($_GET['advagg-core']) + && $_GET['advagg-core'] == 1 + && (user_access('bypass advanced aggregation') || $bypass_cookie) + ) { + $GLOBALS['conf']['preprocess_css'] = TRUE; + $GLOBALS['conf']['preprocess_js'] = TRUE; + } + // Enable debugging if requested. + if (isset($_GET['advagg-debug']) + && (user_access('bypass advanced aggregation') || $bypass_cookie) + ) { + // Cast to an int. + $GLOBALS['conf']['advagg_debug'] = (int) $_GET['advagg-debug']; + } + } + } + + return variable_get('advagg_enabled', ADVAGG_ENABLED); +} + +/** + * Get the current path used for advagg admin configuration. + * + * @return string + * Path to root advagg config. + */ +function advagg_admin_config_root_path() { + return variable_get('advagg_admin_config_root_path', ADVAGG_ADMIN_CONFIG_ROOT_PATH); +} + +/** + * Get an array of all hooks and settings that affect aggregated files contents. + * + * @return array + * array('variables' => array(...), 'hooks' => array(...)) + */ +function advagg_current_hooks_hash_array() { + $aggregate_settings = &drupal_static(__FUNCTION__); + if (!empty($aggregate_settings)) { + return $aggregate_settings; + } + list($css_path, $js_path) = advagg_get_root_files_dir(); + + // Put all enabled hooks and settings into a big array. + $aggregate_settings = array( + 'variables' => array( + 'advagg_gzip' => variable_get('advagg_gzip', ADVAGG_GZIP), + 'advagg_brotli' => variable_get('advagg_brotli', ADVAGG_BROTLI), + 'advagg_no_zopfli' => variable_get('advagg_no_zopfli', ADVAGG_NO_ZOPFLI), + 'is_https' => $GLOBALS['is_https'], + 'advagg_global_counter' => advagg_get_global_counter(), + 'base_path' => $GLOBALS['base_path'], + 'advagg_ie_css_selector_limiter' => variable_get('advagg_ie_css_selector_limiter', ADVAGG_IE_CSS_SELECTOR_LIMITER), + 'advagg_ie_css_selector_limiter_value' => variable_get('advagg_ie_css_selector_limiter_value', ADVAGG_IE_CSS_SELECTOR_LIMITER_VALUE), + 'advagg_scripts_scope_anywhere' => variable_get('advagg_scripts_scope_anywhere', ADVAGG_SCRIPTS_SCOPE_ANYWHERE), + 'advagg_devel' => variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0 ? TRUE : FALSE, + 'advagg_convert_absolute_to_relative_path' => variable_get('advagg_convert_absolute_to_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_RELATIVE_PATH), + 'advagg_convert_absolute_to_protocol_relative_path' => variable_get('advagg_convert_absolute_to_protocol_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_PROTOCOL_RELATIVE_PATH), + 'advagg_css_absolute_path' => variable_get('advagg_css_absolute_path', ADVAGG_CSS_ABSOLUTE_PATH), + 'advagg_force_https_path' => variable_get('advagg_force_https_path', ADVAGG_FORCE_HTTPS_PATH), + 'advagg_css_dir' => $css_path[0], + 'advagg_js_dir' => $js_path[0], + ), + 'hooks' => advagg_hooks_implemented(FALSE), + ); + // Add in language if locale is enabled. + if (module_exists('locale')) { + $aggregate_settings['variables']['language'] = isset($GLOBALS['language']->language) ? $GLOBALS['language']->language : ''; + } + + // Add the base url if so desired to. + if (variable_get('advagg_include_base_url', ADVAGG_INCLUDE_BASE_URL)) { + $aggregate_settings['variables']['base_url'] = $GLOBALS['base_url']; + } + + // CDN module settings. + // Patch in https://www.drupal.org/node/1942230#comment-7927171 is fine + // on a technical level but I got frustrated with all the reports about it not + // working with no good reason as to why it doesn't work. + if (!function_exists('cdn_advagg_current_hooks_hash_array_alter') && module_exists('cdn')) { + $aggregate_settings['variables'][CDN_MODE_VARIABLE] = variable_get(CDN_MODE_VARIABLE, CDN_MODE_BASIC); + $aggregate_settings['variables'][CDN_BASIC_FARFUTURE_VARIABLE] = variable_get(CDN_BASIC_FARFUTURE_VARIABLE, CDN_BASIC_FARFUTURE_DEFAULT); + $aggregate_settings['variables'][CDN_HTTPS_SUPPORT_VARIABLE] = variable_get(CDN_HTTPS_SUPPORT_VARIABLE, FALSE); + $aggregate_settings['variables'][CDN_STATUS_VARIABLE] = variable_get(CDN_STATUS_VARIABLE, CDN_DISABLED); + $aggregate_settings['variables']['cdn_request_is_https'] = cdn_request_is_https(); + $aggregate_settings['variables']['cdn_check_drupal_path'] = cdn_check_drupal_path($_GET['q']); + } + + // Allow other modules to add in their own settings and hooks. + // Call hook_advagg_current_hooks_hash_array_alter(). + drupal_alter('advagg_current_hooks_hash_array', $aggregate_settings); + + return $aggregate_settings; +} + +/** + * Get the hash of all hooks and settings that affect aggregated files contents. + * + * @return string + * hash value. + */ +function advagg_get_current_hooks_hash() { + $current_hash = &drupal_static(__FUNCTION__); + + if (empty($current_hash)) { + // Get all advagg hooks and variables in use. + $aggregate_settings = advagg_current_hooks_hash_array(); + + // Generate the hash. + $serialize_function = variable_get('advagg_serialize', ADVAGG_SERIALIZE); + $current_hash = drupal_hash_base64($serialize_function($aggregate_settings)); + + // Save into variables for verification purposes later on if not found. + $settings = advagg_get_hash_settings($current_hash); + if (empty($settings)) { + // Save new hash into. + advagg_set_hash_settings($current_hash, $aggregate_settings); + } + } + + return $current_hash; +} + +/** + * Store settings associated with hash. + * + * @param string $hash + * The hash. + * @param array $settings + * The settings associated with this hash. + * + * @return MergeQuery + * value from db_merge + */ +function advagg_set_hash_settings($hash, array $settings = array()) { + return db_merge('advagg_aggregates_hashes') + ->key(array('hash' => $hash)) + ->fields(array( + 'hash' => $hash, + 'settings' => serialize($settings), + )) + ->execute(); +} + +/** + * Get back what hooks are implemented. + * + * @param bool $all + * If TRUE get all hooks related to css/js files. + * if FALSE get only the subset of hooks that alter the filename/contents. + * + * @return array + * List of hooks and what modules have implemented them. + */ +function advagg_hooks_implemented($all = TRUE) { + // Get hooks in use. + $hooks = array( + 'advagg_get_css_file_contents_pre_alter' => array(), + 'advagg_get_css_file_contents_alter' => array(), + 'advagg_get_css_aggregate_contents_alter' => array(), + 'advagg_get_js_file_contents_alter' => array(), + 'advagg_get_js_aggregate_contents_alter' => array(), + 'advagg_save_aggregate_pre_alter' => array(), + 'advagg_save_aggregate_alter' => array(), + 'advagg_current_hooks_hash_array_alter' => array(), + 'advagg_get_root_files_dir_alter' => array(), + 'advagg_context_alter' => array(), + ); + if ($all) { + $hooks += array( + 'advagg_build_aggregate_plans_alter' => array(), + 'advagg_build_aggregate_plans_post_alter' => array(), + 'advagg_changed_files' => array(), + 'advagg_css_groups_alter' => array(), + 'advagg_js_groups_alter' => array(), + 'advagg_modify_css_pre_render_alter' => array(), + 'advagg_modify_js_pre_render_alter' => array(), + 'advagg_get_info_on_files_alter' => array(), + 'advagg_hooks_implemented_alter' => array(), + 'advagg_removed_aggregates' => array(), + 'advagg_scan_for_changes' => array(), + 'advagg_missing_root_file' => array(), + 'js_alter' => array(), + 'css_alter' => array(), + ); + } + // Call hook_advagg_hooks_implemented_alter(). + drupal_alter('advagg_hooks_implemented', $hooks, $all); + + // Cache module_implements as this will load up .inc files. + $serialize_function = variable_get('advagg_serialize', ADVAGG_SERIALIZE); + $cid = 'advagg_hooks_implemented:' . (int) $all . ':' . drupal_hash_base64($serialize_function($hooks)); + $cache = cache_get($cid, 'cache_bootstrap'); + if (!empty($cache->data)) { + $hooks = $cache->data; + } + else { + foreach ($hooks as $hook => $values) { + $hooks[$hook] = module_implements($hook); + + // Also check themes as drupal_alter() allows for themes to alter things. + $theme_keys = array_keys(list_themes()); + if (!empty($theme_keys)) { + foreach ($theme_keys as $theme_key) { + $function = $theme_key . '_' . $hook; + if (function_exists($function)) { + $hooks[$hook][] = $theme_key; + } + } + } + } + cache_set($cid, $hooks, 'cache_bootstrap', CACHE_TEMPORARY); + } + + return $hooks; +} + +/** + * Returns the hashes settings. + * + * @param string $hash + * The name of the variable to return. + * + * @return array + * The settings array or an empty array if not found. + */ +function advagg_get_hash_settings($hash) { + $settings = db_select('advagg_aggregates_hashes', 'aah') + ->fields('aah', array('settings')) + ->condition('hash', $hash) + ->execute() + ->fetchField(); + + return !empty($settings) ? unserialize($settings) : array(); +} + +/** + * Get the CSS and JS path for advagg. + * + * @param bool $reset + * Set to TRUE to reset the static variables. + * + * @return array + * Example return below: + * + * @code + * array( + * array( + * public://advagg_css, + * sites/default/files/advagg_css, + * ), + * array( + * public://advagg_js, + * sites/default/files/advagg_js, + * ), + * ) + * @endcode + */ +function advagg_get_root_files_dir($reset = FALSE) { + $css_paths = &drupal_static(__FUNCTION__ . '_css'); + $js_paths = &drupal_static(__FUNCTION__ . '_js'); + + // Make sure directories are available and writable. + if (empty($css_paths) || empty($js_paths) || $reset) { + // Default is public://. + $prefix = variable_get('advagg_root_dir_prefix', ADVAGG_ROOT_DIR_PREFIX); + $css_paths[0] = $prefix . 'advagg_css'; + $js_paths[0] = $prefix . 'advagg_js'; + + file_prepare_directory($css_paths[0], FILE_CREATE_DIRECTORY); + file_prepare_directory($js_paths[0], FILE_CREATE_DIRECTORY); + + // Set the URI of the directory. + $css_paths[1] = advagg_get_relative_path($css_paths[0], 'css'); + $js_paths[1] = advagg_get_relative_path($js_paths[0], 'js'); + + // If the css or js got a path, use it for the other missing one. + if (empty($css_paths[1]) && !empty($js_paths[1])) { + $css_paths[1] = str_replace('/advagg_js', '/advagg_css', $js_paths[1]); + } + elseif (empty($js_paths[1]) && !empty($css_paths[1])) { + $js_paths[1] = str_replace('/advagg_css', '/advagg_js', $css_paths[1]); + } + + // Fix if empty. + if (empty($css_paths[1])) { + $css_paths[1] = $css_paths[0]; + } + if (empty($js_paths[1])) { + $js_paths[1] = $js_paths[0]; + } + + // Allow other modules to alter css and js paths. + // Call hook_advagg_get_root_files_dir_alter() + drupal_alter('advagg_get_root_files_dir', $css_paths, $js_paths); + } + + return array($css_paths, $js_paths); +} + +/** + * Given a uri, get the relative_path. + * + * @param string $uri + * The uri for the stream wrapper. + * @param string $type + * (Optional) String: css or js. + * + * @return string + * The relative path of the uri. + * + * @see https://www.drupal.org/node/837794#comment-9124435 + */ +function advagg_get_relative_path($uri, $type = '') { + $wrapper = file_stream_wrapper_get_instance_by_uri($uri); + if ($wrapper instanceof DrupalLocalStreamWrapper) { + $relative_path = $wrapper->getDirectoryPath() . '/' . file_uri_target($uri); + } + else { + $relative_path = parse_url(file_create_url($uri), PHP_URL_PATH); + if (empty($relative_path) && !empty($uri)) { + $filename = advagg_generate_advagg_filename_from_db($type); + $relative_path = parse_url(file_create_url("{$uri}/{$filename}"), PHP_URL_PATH); + $end = strpos($relative_path, "/{$filename}"); + if ($end !== FALSE) { + $relative_path = substr($relative_path, 0, $end); + } + } + if (substr($relative_path, 0, strlen($GLOBALS['base_path'])) == $GLOBALS['base_path']) { + $relative_path = substr($relative_path, strlen($GLOBALS['base_path'])); + } + $relative_path = ltrim($relative_path, '/'); + } + return $relative_path; +} + +/** + * Builds the requested CSS/JS aggregates. + * + * @param array $filenames + * Array of AdvAgg filenames to generate. + * @param string $type + * String: css or js. + * + * @return array + * Array keyed by filename, value is result from advagg_missing_create_file(). + */ +function advagg_build_aggregates(array $filenames, $type) { + // Check input values. + if (empty($filenames)) { + return array(); + } + if (empty($type)) { + $filename = reset($filenames); + $type = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + } + + // Call the file generation function directly. + module_load_include('inc', 'advagg', 'advagg.missing'); + list($css_path, $js_path) = advagg_get_root_files_dir(); + $return = array(); + foreach ($filenames as $filename) { + // Skip if the file exists. + if ($type === 'css') { + $uri = $css_path[0] . '/' . $filename; + } + elseif ($type === 'js') { + $uri = $js_path[0] . '/' . $filename; + } + if (file_exists($uri)) { + continue; + } + + // Only create the file if we have a lock. + $lock_name = 'advagg_' . $filename; + if (variable_get('advagg_no_locks', ADVAGG_NO_LOCKS)) { + $return[$filename] = advagg_missing_create_file($filename); + } + elseif (lock_acquire($lock_name, 10)) { + $return[$filename] = advagg_missing_create_file($filename); + lock_release($lock_name); + } + } + return $return; +} + +/** + * Gets the core CSS/JS included in this ajax request. + * + * Used so core JS can be rendered through the AdvAgg pipeline. + * + * @see ajax_render() + * + * @return array + * Returns an array containing $styles, $scripts_header, $scripts_footer, + * $items, and $settings. + */ +function advagg_build_ajax_js_css() { + $settings = array(); + // Ajax responses aren't rendered with html.tpl.php, so we have to call + // drupal_get_css() and drupal_get_js() here, in order to have new files added + // during this request to be loaded by the page. We only want to send back + // files that the page hasn't already loaded, so we implement simple diffing + // logic using array_diff_key(). + foreach (array('css', 'js') as $type) { + // It is highly suspicious if $_POST['ajax_page_state'][$type] is empty, + // since the base page ought to have at least one JS file and one CSS file + // loaded. It probably indicates an error, and rather than making the page + // reload all of the files, instead we return no new files. + if (empty($_POST['ajax_page_state'][$type])) { + $items[$type] = array(); + $scripts = drupal_add_js(); + if (!empty($scripts['settings'])) { + $settings = $scripts['settings']; + } + } + else { + $function = 'drupal_add_' . $type; + // Get the current css/js needed for this page. + $items[$type] = $function(); + // Call hook_js_alter() OR hook_css_alter(). + drupal_alter($type, $items[$type]); + // @todo Inline CSS and JS items are indexed numerically. These can't be + // reliably diffed with array_diff_key(), since the number can change + // due to factors unrelated to the inline content, so for now, we strip + // the inline items from Ajax responses, and can add support for them + // when drupal_add_css() and drupal_add_js() are changed to use a hash + // of the inline content as the array key. + foreach ($items[$type] as $key => $item) { + if (is_numeric($key)) { + unset($items[$type][$key]); + } + } + // Ensure that the page doesn't reload what it already has. + // @ignore security_17 + $items[$type] = array_diff_key($items[$type], $_POST['ajax_page_state'][$type]); + } + } + + // Render the HTML to load these files, and add AJAX commands to insert this + // HTML in the page. We pass TRUE as the $skip_alter argument to prevent the + // data from being altered again, as we already altered it above. Settings are + // handled separately, afterwards. + $scripts = drupal_add_js(); + if (isset($scripts['settings'])) { + $settings = $scripts['settings']; + unset($items['js']['settings']); + } + + $styles = drupal_get_css($items['css'], TRUE); + $scripts_footer = drupal_get_js('footer', $items['js'], TRUE); + $scripts_header = drupal_get_js('header', $items['js'], TRUE); + return array($styles, $scripts_header, $scripts_footer, $items, $settings); +} + +/** + * Given the type lets us know if advagg is enabled or disabled. + * + * @param string $type + * String: css or js. + * + * @return bool + * TRUE or FALSE. + */ +function advagg_file_aggregation_enabled($type) { + if (defined('MAINTENANCE_MODE') && MAINTENANCE_MODE === 'update') { + return FALSE; + } + if (isset($_GET['advagg']) && $_GET['advagg'] == 0 && user_access('bypass advanced aggregation')) { + return FALSE; + } + if ($type === 'css') { + return variable_get('preprocess_css', FALSE); + } + if ($type === 'js') { + return variable_get('preprocess_js', FALSE); + } +} + +/** + * Update atime inside advagg_aggregates_versions and cache_advagg_info. + * + * @param array $files + * List of files in the aggregate as well as the aggregate name. + * + * @return bool + * Return TRUE if anything was written to the database. + */ +function advagg_multi_update_atime(array $files) { + $write_done = FALSE; + $records = array(); + foreach ($files as $values) { + // Create the cache id. + $cid = 'advagg:db:' . $values['aggregate_filenames_hash'] . ADVAGG_SPACE . $values['aggregate_contents_hash']; + // Create the db record. + $records[$cid] = array( + 'aggregate_filenames_hash' => $values['aggregate_filenames_hash'], + 'aggregate_contents_hash' => $values['aggregate_contents_hash'], + 'atime' => REQUEST_TIME, + ); + } + + // Use the cache. + $cids = array_keys($records); + $caches = cache_get_multiple($cids, 'cache_advagg_info'); + if (!empty($caches)) { + foreach ($caches as $cache) { + // See if the atime value needs to be updated. + if (!empty($cache->data['atime']) && $cache->data['atime'] > REQUEST_TIME - (12 * 60 * 60)) { + // If atime is less than 12 hours old, do nothing. + unset($records[$cache->cid]); + } + } + } + if (empty($records)) { + return $write_done; + } + + foreach ($records as $cid => $record) { + // Update atime in DB. + $result = db_merge('advagg_aggregates_versions') + ->key(array( + 'aggregate_filenames_hash' => $record['aggregate_filenames_hash'], + 'aggregate_contents_hash' => $record['aggregate_contents_hash'], + )) + ->fields(array('atime' => $record['atime'])) + ->execute(); + if (!$write_done && $result) { + $write_done = TRUE; + } + + // Update the atime in the cache. + // Get fresh copy of the cache. + $cache = cache_get($cid, 'cache_advagg_info'); + // Set the atime. + if (empty($cache->data)) { + $cache = new stdClass(); + } + $cache->data['atime'] = REQUEST_TIME; + + // Write to the cache. + // CACHE_PERMANENT isn't good here. Use 2 weeks from now + 0-45 days. + // The random 0 to 45 day addition is to prevent a cache stampede. + cache_set($cid, $cache->data, 'cache_advagg_info', round(REQUEST_TIME + 1209600 + mt_rand(0, 3888000), -3)); + } + return $write_done; +} + +/** + * Return the advagg_global_counter variable. + * + * @return int + * Int value. + */ +function advagg_get_global_counter() { + $global_counter = variable_get('advagg_global_counter', ADVAGG_GLOBAL_COUNTER); + + return $global_counter; +} + +/** + * Cache clear callback for admin_menu/flush-cache/advagg. + */ +function advagg_admin_flush_cache() { + module_load_include('inc', 'advagg', 'advagg.admin'); + advagg_admin_flush_cache_button(); +} + +/** + * Returns HTML for a generic HTML tag with attributes. + * + * @param array $variables + * An associative array containing: + * - element: An associative array describing the tag: + * - #tag: The tag name to output. Typical tags added to the HTML HEAD: + * - meta: To provide meta information, such as a page refresh. + * - link: To refer to stylesheets and other contextual information. + * - script: To load JavaScript. + * - #attributes: (optional) An array of HTML attributes to apply to the + * tag. + * - #value: (optional) A string containing tag content, such as inline + * CSS. + * - #value_prefix: (optional) A string to prepend to #value, e.g. a CDATA + * wrapper prefix. + * - #value_suffix: (optional) A string to append to #value, e.g. a CDATA + * wrapper suffix. + */ +function theme_html_script_tag(array $variables) { + $element = $variables['element']; + $attributes = ''; + $onload = ''; + $onerror = ''; + if (isset($element['#attributes'])) { + // On Load. + if (!empty($element['#attributes']['onload'])) { + $onload = $element['#attributes']['onload']; + unset($element['#attributes']['onload']); + } + // On Error. + if (!empty($element['#attributes']['onerror'])) { + $onerror = $element['#attributes']['onerror']; + unset($element['#attributes']['onerror']); + } + $attributes = !empty($element['#attributes']) ? drupal_attributes($element['#attributes']) : ''; + if (!empty($onload)) { + $attributes .= ' onload="' . advagg_jsspecialchars($onload) . '"'; + } + if (!empty($onerror)) { + $attributes .= ' onerror="' . advagg_jsspecialchars($onerror) . '"'; + } + } + + if (!isset($element['#value'])) { + return '<' . $element['#tag'] . $attributes . " />\n"; + } + else { + $output = '<' . $element['#tag'] . $attributes . '>'; + if (isset($element['#value_prefix'])) { + $output .= $element['#value_prefix']; + } + $output .= $element['#value']; + if (isset($element['#value_suffix'])) { + $output .= $element['#value_suffix']; + } + $output .= '\n"; + return $output; + } +} + +/** + * Replace quotes with the html version of it. + * + * @param string $string + * Input string. Convert quotes to html chars. + * + * @return string + * Transformed string. + */ +function advagg_jsspecialchars($string = '') { + $string = str_replace('"', '"', $string); + $string = str_replace("'", ''', $string); + + return $string; +} + +/** + * Callback for pre_render to add elements needed for JavaScript to be rendered. + * + * This function evaluates the aggregation enabled/disabled condition on a group + * by group basis by testing whether an aggregate file has been made for the + * group rather than by testing the site-wide aggregation setting. This allows + * this function to work correctly even if modules have implemented custom + * logic for grouping and aggregating files. + * + * @param array $elements + * A render array containing: + * - #items: The JavaScript items as returned by drupal_add_js() and + * altered by drupal_get_js(). + * - #group_callback: A function to call to group #items. Following + * this function, #aggregate_callback is called to aggregate items within + * the same group into a single file. + * - #aggregate_callback: A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @return array + * A render array that will render to a string of JavaScript tags. + * + * @see drupal_get_js() + */ +function advagg_pre_render_scripts(array $elements) { + // Don't run it twice. + if (!empty($elements['#groups'])) { + return $elements; + } + + // Group and aggregate the items. + if (isset($elements['#group_callback'])) { + // Call advagg_group_js(). + $elements['#groups'] = $elements['#group_callback']($elements['#items']); + } + if (isset($elements['#aggregate_callback'])) { + // Call _advagg_aggregate_js(). + $elements['#aggregate_callback']($elements['#groups']); + } + + // A dummy query-string is added to filenames, to gain control over + // browser-caching. The string changes on every update or full cache + // flush, forcing browsers to load a new copy of the files, as the + // URL changed. Files that should not be cached (see drupal_add_js()) + // get REQUEST_TIME as query-string instead, to enforce reload on every + // page request. + $default_query_string = variable_get('css_js_query_string', '0'); + + // For inline JavaScript to validate as XHTML, all JavaScript containing + // XHTML needs to be wrapped in CDATA. To make that backwards compatible + // with HTML 4, we need to comment out the CDATA-tag. + $embed_prefix = "\n\n"; + + // Since JavaScript may look for arguments in the URL and act on them, some + // third-party code might require the use of a different query string. + $js_version_string = variable_get('drupal_js_version_query_string', 'v='); + + // Defaults for each SCRIPT element. + $element_defaults = array( + '#type' => 'html_script_tag', + '#tag' => 'script', + '#value' => '', + '#attributes' => array( + 'type' => 'text/javascript', + ), + ); + $hooks = theme_get_registry(FALSE); + if (empty($hooks['html_script_tag'])) { + $element_defaults['#type'] = 'html_tag'; + } + + // Loop through each group. + foreach ($elements['#groups'] as $group) { + // If a group of files has been aggregated into a single file, + // $group['data'] contains the URI of the aggregate file. Add a single + // script element for this file. + if (isset($group['type']) && $group['type'] === 'file' && isset($group['data'])) { + $element = $element_defaults; + $element['#attributes']['src'] = advagg_file_create_url($group['data']) . ($group['cache'] ? '' : '?' . REQUEST_TIME); + $element['#browsers'] = $group['browsers']; + if (!empty($group['defer'])) { + $element['#attributes']['defer'] = 'defer'; + } + if (!empty($group['async'])) { + $element['#attributes']['async'] = 'async'; + } + if (!empty($group['onload'])) { + if (!isset($element['#attributes']['onload'])) { + $element['#attributes']['onload'] = ''; + } + $element['#attributes']['onload'] .= $group['onload']; + } + if (!empty($group['onerror'])) { + if (!isset($element['#attributes']['onerror'])) { + $element['#attributes']['onerror'] = ''; + } + $element['#attributes']['onerror'] .= $group['onerror']; + } + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + // For non-file types, and non-aggregated files, add a script element per + // item. + else { + foreach ($group['items'] as $item) { + // Skip if data is empty. + if (empty($item['data'])) { + continue; + } + + // Element properties that do not depend on item type. + $element = $element_defaults; + if (!empty($item['defer'])) { + $element['#attributes']['defer'] = 'defer'; + } + if (!empty($item['async'])) { + $element['#attributes']['async'] = 'async'; + } + if (!empty($item['onload'])) { + if (!isset($element['#attributes']['onload'])) { + $element['#attributes']['onload'] = ''; + } + $element['#attributes']['onload'] .= $item['onload']; + } + if (!empty($item['onerror'])) { + if (!isset($element['#attributes']['onerror'])) { + $element['#attributes']['onerror'] = ''; + } + $element['#attributes']['onerror'] .= $item['onerror']; + } + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $element['#browsers'] = isset($item['browsers']) ? $item['browsers'] : array(); + + // Crude type detection if needed. + if (empty($item['type'])) { + if (is_array($item['data'])) { + $item['type'] = 'setting'; + } + elseif (strpos($item['data'], 'http://') === 0 || strpos($item['data'], 'https://') === 0 || strpos($item['data'], '//') === 0) { + $item['type'] = 'external'; + } + elseif (file_exists(trim($item['data']))) { + $item['type'] = 'file'; + } + else { + $item['type'] = 'inline'; + } + } + // Element properties that depend on item type. + switch ($item['type']) { + case 'setting': + $data = advagg_cleanup_settings_array(drupal_array_merge_deep_array(array_filter($item['data'], 'is_array'))); + $json_data = advagg_json_encode($data); + $element['#value_prefix'] = $embed_prefix; + $element['#value'] = 'jQuery.extend(Drupal.settings, ' . $json_data . ");"; + $element['#value_suffix'] = $embed_suffix; + break; + + case 'inline': + // If a BOM is found, convert the string to UTF-8. + $encoding = advagg_get_encoding_from_bom($item['data']); + if (!empty($encoding)) { + $item['data'] = advagg_convert_to_utf8($item['data'], $encoding); + } + + $element['#value_prefix'] = $embed_prefix; + $element['#value'] = $item['data']; + $element['#value_suffix'] = $embed_suffix; + break; + + case 'file': + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $cache_validator = REQUEST_TIME; + if (!empty($item['cache'])) { + $cache_validator = empty($item['version']) ? $default_query_string : $js_version_string . $item['version'];; + } + + $element['#attributes']['src'] = advagg_file_create_url($item['data']) . $query_string_separator . $cache_validator; + break; + + case 'external': + // Convert to protocol relative path. + $file_uri = $item['data']; + if (variable_get('advagg_convert_absolute_to_protocol_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_PROTOCOL_RELATIVE_PATH)) { + $file_uri = advagg_convert_abs_to_protocol($item['data']); + } + $element['#attributes']['src'] = $file_uri; + break; + } + + $elements[] = $element; + } + } + } + + return $elements; +} + +/** + * Get the prefix and suffix for inline css. + * + * @return array + * An array where the prefix is key 0 and suffix is key 1. + */ +function advagg_get_css_prefix_suffix() { + $embed_prefix = "\n/* */\n"; + return array($embed_prefix, $embed_suffix); +} + +/** + * A #pre_render callback to add elements needed for CSS tags to be rendered. + * + * For production websites, LINK tags are preferable to STYLE tags with @import + * statements, because: + * - They are the standard tag intended for linking to a resource. + * - On Firefox 2 and perhaps other browsers, CSS files included with @import + * statements don't get saved when saving the complete web page for offline + * use: http://drupal.org/node/145218. + * - On IE, if only LINK tags and no @import statements are used, all the CSS + * files are downloaded in parallel, resulting in faster page load, but if + * the @import statements are used and span across multiple STYLE tags, all + * the ones from 1 STYLE tag must be downloaded before downloading begins for + * the next STYLE tag. Furthermore, IE7 does not support media declaration on + * the @import statement, so multiple STYLE tags must be used when different + * files are for different media types. Non-IE browsers always download in + * parallel, so this is an IE-specific performance quirk: + * http://www.stevesouders.com/blog/2009/04/09/dont-use-import/. + * + * However, IE has an annoying limit of 31 total CSS inclusion tags + * (http://drupal.org/node/228818) and LINK tags are limited to one file per + * tag, whereas STYLE tags can contain multiple @import statements allowing + * multiple files to be loaded per tag. When CSS aggregation is disabled, a + * Drupal site can easily have more than 31 CSS files that need to be loaded, so + * using LINK tags exclusively would result in a site that would display + * incorrectly in IE. Depending on different needs, different strategies can be + * employed to decide when to use LINK tags and when to use STYLE tags. + * + * The strategy employed by this function is to use LINK tags for all aggregate + * files and for all files that cannot be aggregated (e.g., if 'preprocess' is + * set to FALSE or the type is 'external'), and to use STYLE tags for groups + * of files that could be aggregated together but aren't (e.g., if the site-wide + * aggregation setting is disabled). This results in all LINK tags when + * aggregation is enabled, a guarantee that as many or only slightly more tags + * are used with aggregation disabled than enabled (so that if the limit were to + * be crossed with aggregation enabled, the site developer would also notice the + * problem while aggregation is disabled), and an easy way for a developer to + * view HTML source while aggregation is disabled and know what files will be + * aggregated together when aggregation becomes enabled. + * + * This function evaluates the aggregation enabled/disabled condition on a group + * by group basis by testing whether an aggregate file has been made for the + * group rather than by testing the site-wide aggregation setting. This allows + * this function to work correctly even if modules have implemented custom + * logic for grouping and aggregating files. + * + * @param array $elements + * A render array containing: + * - '#items': The CSS items as returned by drupal_add_css() and altered by + * drupal_get_css(). + * - '#group_callback': A function to call to group #items to enable the use + * of fewer tags by aggregating files and/or using multiple @import + * statements within a single tag. + * - '#aggregate_callback': A function to call to aggregate the items within + * the groups arranged by the #group_callback function. + * + * @return array + * A render array that will render to a string of XHTML CSS tags. + * + * @see drupal_get_css() + */ +function advagg_pre_render_styles(array $elements) { + // Skip if advagg is disabled. + if (!advagg_enabled()) { + return drupal_pre_render_styles($elements); + } + + // Don't run it twice. + if (!empty($elements['#groups'])) { + return $elements; + } + + // Group and aggregate the items. + if (isset($elements['#group_callback'])) { + // Call drupal_group_css(). + $elements['#groups'] = $elements['#group_callback']($elements['#items']); + } + if (isset($elements['#aggregate_callback'])) { + // Call _advagg_aggregate_css(). + $elements['#aggregate_callback']($elements['#groups']); + } + + // A dummy query-string is added to filenames, to gain control over + // browser-caching. The string changes on every update or full cache + // flush, forcing browsers to load a new copy of the files, as the + // URL changed. + $query_string = variable_get('css_js_query_string', '0'); + + // For inline CSS to validate as XHTML, all CSS containing XHTML needs to be + // wrapped in CDATA. To make that backwards compatible with HTML 4, we need to + // comment out the CDATA-tag. + // @see https://www.drupal.org/node/1021622 + list($embed_prefix, $embed_suffix) = advagg_get_css_prefix_suffix(); + + // Defaults for LINK and STYLE elements. + $link_element_defaults = array( + '#type' => 'html_tag', + '#tag' => 'link', + '#attributes' => array( + 'type' => 'text/css', + 'rel' => 'stylesheet', + ), + ); + $style_element_defaults = array( + '#type' => 'html_tag', + '#tag' => 'style', + '#attributes' => array( + 'type' => 'text/css', + ), + ); + + // Loop through each group. + foreach ($elements['#groups'] as $group) { + switch ($group['type']) { + // For file items, there are three possibilities. + // - The group has been aggregated: in this case, output a LINK tag for + // the aggregate file. + // - The group can be aggregated but has not been (most likely because + // the site administrator disabled the site-wide setting): in this case, + // output as few STYLE tags for the group as possible, using @import + // statement for each file in the group. This enables us to stay within + // IE's limit of 31 total CSS inclusion tags. + // - The group contains items not eligible for aggregation (their + // 'preprocess' flag has been set to FALSE): in this case, output a LINK + // tag for each file. + case 'file': + // The group has been aggregated into a single file: output a LINK tag + // for the aggregate file. + if (isset($group['data'])) { + $element = $link_element_defaults; + $element['#attributes']['href'] = advagg_file_create_url($group['data']); + $element['#attributes']['media'] = $group['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + // The group can be aggregated, but hasn't been: combine multiple items + // into as few STYLE tags as possible. + elseif ($group['preprocess']) { + $import = array(); + foreach ($group['items'] as $item) { + // A theme's .info file may have an entry for a file that doesn't + // exist as a way of overriding a module or base theme CSS file from + // being added to the page. Normally, file_exists() calls that need + // to run for every page request should be minimized, but this one + // is okay, because it only runs when CSS aggregation is disabled. + // On a server under heavy enough load that file_exists() calls need + // to be minimized, CSS aggregation should be enabled, in which case + // this code is not run. When aggregation is enabled, + // drupal_load_stylesheet() checks file_exists(), but only when + // building the aggregate file, which is then reused for many page + // requests. + if (file_exists($item['data'])) { + // The dummy query string needs to be added to the URL to control + // browser-caching. IE7 does not support a media type on the + // "@import" statement, so we instead specify the media for the + // group on the STYLE tag. + $import[] = '@import url("' . check_plain(advagg_file_create_url($item['data']) . '?' . $query_string) . '");'; + } + } + // In addition to IE's limit of 31 total CSS inclusion tags, it also + // has a limit of 31 @import statements per STYLE tag. + while (!empty($import)) { + $import_batch = array_slice($import, 0, 31); + $import = array_slice($import, 31); + $element = $style_element_defaults; + // This simplifies the JavaScript regex, allowing each line + // (separated by \n) to be treated as a completely different string. + // This means that we can use ^ and $ on one line at a time, and not + // worry about style tags since they'll never match the regex. + $element['#value'] = "\n" . implode("\n", $import_batch) . "\n"; + $element['#attributes']['media'] = $group['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + } + // The group contains items ineligible for aggregation: output a LINK + // tag for each file. + else { + foreach ($group['items'] as $item) { + $element = $link_element_defaults; + // We do not check file_exists() here, because this code runs for + // files whose 'preprocess' is set to FALSE, and therefore, even + // when aggregation is enabled, and we want to avoid needlessly + // taxing a server that may be under heavy load. The file_exists() + // performed above for files whose 'preprocess' is TRUE is done for + // the benefit of theme .info files, but code that deals with files + // whose 'preprocess' is FALSE is responsible for ensuring the file + // exists. + // The dummy query string needs to be added to the URL to control + // browser-caching. + $query_string_separator = (strpos($item['data'], '?') !== FALSE) ? '&' : '?'; + $element['#attributes']['href'] = advagg_file_create_url($item['data']) . $query_string_separator . $query_string; + $element['#attributes']['media'] = $item['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + } + break; + + // For inline content, the 'data' property contains the CSS content. If + // the group's 'data' property is set, then output it in a single STYLE + // tag. Otherwise, output a separate STYLE tag for each item. + case 'inline': + if (isset($group['data'])) { + $element = $style_element_defaults; + $element['#value'] = $group['data']; + $element['#value_prefix'] = $embed_prefix; + $element['#value_suffix'] = $embed_suffix; + $element['#attributes']['media'] = $group['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + else { + foreach ($group['items'] as $item) { + $element = $style_element_defaults; + $element['#value'] = $item['data']; + $element['#value_prefix'] = $embed_prefix; + $element['#value_suffix'] = $embed_suffix; + $element['#attributes']['media'] = $item['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + } + break; + + // Output a LINK tag for each external item. The item's 'data' property + // contains the full URL. + case 'external': + foreach ($group['items'] as $item) { + $element = $link_element_defaults; + // Convert to protocol relative path. + $file_uri = $item['data']; + if (variable_get('advagg_convert_absolute_to_protocol_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_PROTOCOL_RELATIVE_PATH)) { + $file_uri = advagg_convert_abs_to_protocol($item['data']); + } + $element['#attributes']['href'] = $file_uri; + $element['#attributes']['media'] = $item['media']; + $element['#browsers'] = $group['browsers']; + if (!empty($group['attributes'])) { + $element['#attributes'] += $group['attributes']; + } + $elements[] = $element; + } + break; + + } + } + + return $elements; +} + +/** + * Default callback to group JavaScript items. + * + * This function arranges the JavaScript items that are in the #items property + * of the scripts element into groups. When aggregation is enabled, files within + * a group are aggregated into a single file, significantly improving page + * loading performance by minimizing network traffic overhead. + * + * This function puts multiple items into the same group if they are groupable + * and if they are for the same browsers. Items of the 'file' type are groupable + * if their 'preprocess' flag is TRUE. Items of the 'inline', 'settings', or + * 'external' type are not groupable. + * + * This function also ensures that the process of grouping items does not change + * their relative order. This requirement may result in multiple groups for the + * same type and browsers, if needed to accommodate other items in + * between. + * + * @param array $javascript + * An array of JavaScript items, as returned by drupal_add_js(), but after + * alteration performed by drupal_get_js(). + * + * @return array + * An array of JavaScript groups. Each group contains the same keys (e.g., + * 'data', etc.) as a JavaScript item from the $javascript parameter, with the + * value of each key applying to the group as a whole. Each group also + * contains an 'items' key, which is the subset of items from $javascript that + * are in the group. + * + * @see drupal_pre_render_scripts() + */ +function advagg_group_js(array $javascript) { + $groups = array(); + // If a group can contain multiple items, we track the information that must + // be the same for each item in the group, so that when we iterate the next + // item, we can determine if it can be put into the current group, or if a + // new group needs to be made for it. + $current_group_keys = NULL; + $index = -1; + foreach ($javascript as $key => $item) { + if (empty($item)) { + continue; + } + // The browsers for which the JavaScript item needs to be loaded is part of + // the information that determines when a new group is needed, but the order + // of keys in the array doesn't matter, and we don't want a new group if all + // that's different is that order. + if (isset($item['browsers'])) { + ksort($item['browsers']); + } + else { + $item['browsers'] = array(); + } + + // Fix missing types. + if (empty($item['type'])) { + // Setting is easy. + if ($key === 'settings') { + $item['type'] = 'setting'; + } + // Check for the schema or // for protocol-relative. + elseif ((stripos($item['data'], 'http://') === 0 + || stripos($item['data'], 'https://') === 0 + || (strpos($item['data'], '//') === 0 && strpos($item['data'], '///') === FALSE)) + ) { + $item['type'] = 'external'; + } + // See if the data contains a semicolon, new line, $, or quotes. + elseif (strpos($item['data'], ';') !== FALSE + || strpos($item['data'], "\n") + || strpos($item['data'], "$") + || strpos($item['data'], "'") + || strpos($item['data'], '"') + ) { + $item['type'] = 'inline'; + } + // Ends in .js. + elseif (stripos(strrev($item['data']), strrev('.js')) === 0) { + $item['type'] = 'file'; + } + } + + switch ($item['type']) { + case 'file': + // Group file items if their 'preprocess' flag is TRUE. + // Help ensure maximum reuse of aggregate files by only grouping + // together items that share the same 'group' value and 'every_page' + // flag. See drupal_add_js() for details about that. + $group_keys = !empty($item['preprocess']) ? array( + $item['type'], + $item['group'], + $item['every_page'], + $item['browsers'], + ) : FALSE; + break; + + case 'external': + case 'setting': + case 'inline': + // Do not group external, settings, and inline items. + $group_keys = FALSE; + break; + + default: + // Define this here so we don't get undefined alerts down below. + $group_keys = NULL; + // Log the error as well. + watchdog('advagg', 'Bad javascript was added. Type is unknown. @key - @item', array( + '@key' => $key, + '@item' => print_r($item, TRUE), + ), WATCHDOG_NOTICE); + break; + + } + + // If the group keys don't match the most recent group we're working with, + // then a new group must be made. + if ($group_keys !== $current_group_keys) { + ++$index; + // Initialize the new group with the same properties as the first item + // being placed into it. The item's 'data' and 'weight' properties are + // unique to the item and should not be carried over to the group. + $groups[$index] = $item; + unset($groups[$index]['data'], $groups[$index]['weight']); + $groups[$index]['items'] = array(); + $current_group_keys = $group_keys ? $group_keys : NULL; + } + + // Add the item to the current group. + $groups[$index]['items'][] = $item; + } + + return $groups; +} + +/** + * Stable sort for CSS and JS items. + * + * Preserves the order of items with equal sort criteria. + * + * The function will sort by: + * - $item['group'], integer, ascending + * - $item['every_page'], boolean, first TRUE then FALSE + * - $item['weight'], integer, ascending + * + * @param array &$items + * Array of JS or CSS items, as in drupal_add_css() and drupal_add_js(). + * The array keys can be integers or strings. The items themselves are arrays. + * + * @see drupal_get_css() + * @see drupal_get_js() + * @see drupal_add_css() + * @see drupal_add_js() + * @see https://drupal.org/node/1388546 + */ +function advagg_drupal_sort_css_js_stable(array &$items) { + // Within a group, order all infrequently needed, page-specific files after + // common files needed throughout the website. Separating this way allows for + // the aggregate file generated for all of the common files to be reused + // across a site visit without being cut by a page using a less common file. + $nested = array(); + foreach ($items as $key => &$item) { + // If weight is not set, make it 0. + if (!isset($item['weight'])) { + $item['weight'] = 0; + } + // If every_page is not set, make it FALSE. + if (!isset($item['every_page'])) { + $item['every_page'] = FALSE; + } + // If group is not set, make it CSS_DEFAULT/JS_DEFAULT (0). + if (!isset($item['group'])) { + $item['group'] = 0; + } + // If scope is not set, make it header. + if (!isset($item['scope'])) { + $item['scope'] = 'header'; + } + + // Weight cast to string to preserve float. + $weight = (string) $item['weight']; + $nested[$item['group']][$item['every_page'] ? 1 : 0][$weight][$key] = $item; + } + // First order by group, so that, for example, all items in the CSS_SYSTEM + // group appear before items in the CSS_DEFAULT group, which appear before + // all items in the CSS_THEME group. Modules may create additional groups by + // defining their own constants. + $sorted = array(); + // Sort group; then iterate over it. + ksort($nested); + foreach ($nested as &$group_items) { + // Reverse sort every_page; then iterate over it. + krsort($group_items); + foreach ($group_items as &$ep_items) { + // Sort weight; then iterate over it. + ksort($ep_items); + // Finally, order by weight. + foreach ($ep_items as &$weight_items) { + foreach ($weight_items as $key => &$item) { + $sorted[$key] = $item; + } + unset($item); + } + } + unset($ep_items); + } + unset($group_items); + $items = $sorted; +} + +/** + * Converts a PHP variable into its JavaScript equivalent. + * + * @param mixed $data + * Usually an array of data to be converted into a JSON string. + * + * @return string + * If there are no errors, this will return a JSON string. FALSE will be + * returned on failure. + */ +function advagg_json_encode($data) { + // Different versions of PHP handle json_encode() differently. + static $php550; + static $php540; + static $php530; + if (!isset($php550)) { + $php550 = version_compare(PHP_VERSION, '5.5.0', '>='); + } + if (!isset($php540)) { + $php540 = version_compare(PHP_VERSION, '5.4.0', '>='); + } + if (!isset($php530)) { + $php530 = version_compare(PHP_VERSION, '5.3.0', '>='); + } + + // Use fallback drupal encoder if PHP < 5.3.0. + if (!$php530) { + return @drupal_json_encode($data); + } + + // Default json encode options. + $options = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; + if ($php550 && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 0) { + // Output partial json if not in development mode and PHP >= 5.5.0. + $options |= JSON_PARTIAL_OUTPUT_ON_ERROR; + } + if ($php540 && variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + // Pretty print JSON if in development mode and PHP >= 5.4.0. + $options |= JSON_PRETTY_PRINT; + } + // Encode to JSON. + $json_data = @json_encode($data, $options); + + // Uses json_last_error() if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $error_number = json_last_error(); + switch ($error_number) { + case JSON_ERROR_NONE: + $error_message = ''; + break; + + case JSON_ERROR_DEPTH: + $error_message = 'Maximum stack depth exceeded'; + break; + + case JSON_ERROR_STATE_MISMATCH: + $error_message = 'Underflow or the modes mismatch'; + break; + + case JSON_ERROR_CTRL_CHAR: + $error_message = 'Unexpected control character found'; + break; + + case JSON_ERROR_SYNTAX: + $error_message = 'Syntax error, malformed JSON'; + break; + + case JSON_ERROR_UTF8: + $error_message = 'Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + + default: + $error_message = 'Unknown error: ' . $error_number; + break; + } + if (!empty($error_message)) { + if (is_callable('httprl_pr')) { + $pretty_data = httprl_pr($data); + } + elseif (is_callable('kprint_r')) { + // @codingStandardsIgnoreLine + $pretty_data = kprint_r($data, TRUE); + } + else { + $pretty_data = '
' . filter_xss(print_r($data, TRUE)) . '
'; + } + watchdog('advagg_json', 'Error with json encoding the Drupal.settings value. Error Message: %error_message. JSON Data: !data', array( + '%error_message' => $error_message, + '!data' => $pretty_data, + ), WATCHDOG_ERROR); + } + } + return $json_data; +} + +/** + * Will scan, flush, use, and report any changes to css/js files in aggregates. + */ +function advagg_scan_filesystem_for_changes_live() { + static $function_has_ran; + if (isset($function_has_ran)) { + return; + } + $function_has_ran = TRUE; + + $bypass_cookie = FALSE; + $cookie_name = 'AdvAggDisabled'; + $key = drupal_hmac_base64('advagg_cookie', drupal_get_private_key() . drupal_get_hash_salt() . variable_get('cron_key', 'drupal')); + if (!empty($_COOKIE[$cookie_name]) && $_COOKIE[$cookie_name] == $key) { + $bypass_cookie = TRUE; + } + if ((!advagg_enabled() && !$bypass_cookie) || variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) >= 0) { + return; + } + + // Scan for changes to any CSS/JS files. + module_load_include('inc', 'advagg', 'advagg.cache'); + $flushed = advagg_push_new_changes(); + + // Report back the results. + if (empty($flushed) || !user_is_logged_in()) { + return; + } + + list($css_path) = advagg_get_root_files_dir(); + $parts_uri = $css_path[1] . '/parts'; + + foreach ($flushed as $filename => $data) { + if (strpos($filename, $parts_uri) === 0) { + // Do not report on css files manged in the parts directory. + continue; + } + if (variable_get('advagg_show_file_changed_message', ADVAGG_SHOW_FILE_CHANGED_MESSAGE)) { + $ext = pathinfo($filename, PATHINFO_EXTENSION); + drupal_set_message(t('The file %filename has changed. %db_usage aggregates are using this file. %db_count db cache entries and all %type full cache entries have been flushed from the cache bins. Trigger: @changes', array( + '%filename' => $filename, + '%db_usage' => count($data[0]), + '%db_count' => count($data[1]), + '@changes' => print_r($data[2], TRUE), + '%type' => $ext, + ))); + } + } +} + +/** + * Checks if the filename matches the advagg file pattern. + * + * @param string $filename + * Path to check. + * + * @return int + * Returns 1 if the pattern matches, 0 if it does not. + */ +function advagg_match_file_pattern($filename) { + return preg_match('/.*(j|cs)s' . ADVAGG_SPACE . '[A-Za-z0-9-_]{43}' . ADVAGG_SPACE . '[A-Za-z0-9-_]{43}' . ADVAGG_SPACE . '[A-Za-z0-9-_]{43}\.(j|cs)s$/', $filename); +} + +/** + * Converts absolute paths to be self references. + * + * @param string $path + * Path to check. + * @param bool $strip_base_path + * Do no add the base path to the given path if TRUE. + * + * @return string + * The path. + */ +function advagg_convert_abs_to_rel($path, $strip_base_path = FALSE) { + $base_url = $GLOBALS['base_url']; + // Add a slash to end if none is found. + if (strpos(strrev($base_url), '/') !== 0) { + $base_url .= '/'; + } + // Set base path. + $base_path = $GLOBALS['base_path']; + if ($strip_base_path) { + $base_path = ''; + } + + // Do conversion of https and http to self references. + $base_url_https = advagg_force_https_path($base_url); + $path = str_replace($base_url_https, $base_path, $path); + $base_url_http = advagg_force_http_path($base_url); + $path = str_replace($base_url_http, $base_path, $path); + + $base_url = advagg_convert_abs_to_protocol($GLOBALS['base_url']); + // Add a slash to end if none is found. + if (strpos(strrev($base_url), '/') !== 0) { + $base_url .= '/'; + } + // Do conversion of protocol relative to self references. + $path = str_replace($base_url, $base_path, $path); + + return $path; +} + +/** + * Converts absolute paths to be protocol relative paths. + * + * @param string $path + * Path to check. + * + * @return string + * The path. + */ +function advagg_convert_abs_to_protocol($path) { + if (strpos($path, 'http://') === 0) { + $path = substr($path, 5); + } + return $path; +} + +/** + * Convert http:// and // to https://. + * + * @param string $path + * Path to check. + * + * @return string + * The path. + */ +function advagg_force_https_path($path) { + if (strpos($path, 'http://') === 0) { + $path = 'https://' . substr($path, 7); + } + elseif (strpos($path, '//') === 0) { + $path = 'https:' . $path; + } + return $path; +} + +/** + * Convert https:// to http://. + * + * @param string $path + * Path to check. + * + * @return string + * The path. + */ +function advagg_force_http_path($path) { + if (strpos($path, 'https://') === 0) { + $path = 'http://' . substr($path, 8); + } + return $path; +} + +/** + * Wrapper around file_create_url() to do post-processing on the created url. + * + * @param string $path + * Path to check. + * @param array $aggregate_settings + * Array of settings used. + * @param bool $run_file_create_url + * If TRUE then run the given path through file_create_url(). + * @param string $source_type + * CSS or JS; if empty url in not embedded in another file. + * + * @return string + * The file uri. + */ +function advagg_file_create_url($path, array $aggregate_settings = array(), $run_file_create_url = TRUE, $source_type = '') { + $file_uri = $path; + if ($run_file_create_url) { + // This calls hook_file_url_alter(). + $file_uri = file_create_url($path); + } + elseif (strpos($path, '/') !== 0 && !advagg_is_external($path)) { + $file_uri = '/' . $path; + } + // Ideally convert to relative path. + if ((isset($aggregate_settings['variables']['advagg_convert_absolute_to_relative_path']) + && $aggregate_settings['variables']['advagg_convert_absolute_to_relative_path']) + || (!isset($aggregate_settings['variables']['advagg_convert_absolute_to_relative_path']) + && variable_get('advagg_convert_absolute_to_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_RELATIVE_PATH)) + ) { + $file_uri = advagg_convert_abs_to_rel($file_uri); + } + // Next try protocol relative path. + if ((isset($aggregate_settings['variables']['advagg_convert_absolute_to_protocol_relative_path']) + && $aggregate_settings['variables']['advagg_convert_absolute_to_protocol_relative_path']) + || (!isset($aggregate_settings['variables']['advagg_convert_absolute_to_protocol_relative_path']) + && variable_get('advagg_convert_absolute_to_protocol_relative_path', ADVAGG_CONVERT_ABSOLUTE_TO_PROTOCOL_RELATIVE_PATH)) + ) { + $file_uri = advagg_convert_abs_to_protocol($file_uri); + } + if ($source_type === 'css' + && !advagg_is_external($file_uri) + && ((isset($aggregate_settings['variables']['advagg_css_absolute_path']) + && $aggregate_settings['variables']['advagg_css_absolute_path']) + || (!isset($aggregate_settings['variables']['advagg_css_absolute_path']) + && variable_get('advagg_css_absolute_path', ADVAGG_CSS_ABSOLUTE_PATH))) + ) { + // Get public dir. + list($css_path) = advagg_get_root_files_dir(); + $parsed = parse_url($css_path[0]); + $new_parsed = array(); + if (!empty($parsed['host'])) { + $new_parsed['host'] = $parsed['host']; + } + if (!empty($parsed['path'])) { + $new_parsed['path'] = $parsed['path']; + } + $css_path_0 = advagg_glue_url($new_parsed); + $parsed = parse_url($css_path[1]); + $new_parsed = array(); + if (!empty($parsed['host'])) { + $new_parsed['host'] = $parsed['host']; + } + if (!empty($parsed['path'])) { + $new_parsed['path'] = $parsed['path']; + } + $css_path_1 = advagg_glue_url($new_parsed); + $pos = strpos($css_path_1, $css_path_0); + if (!empty($pos)) { + $public_dir = substr($css_path_1, 0, $pos); + + // If public dir is not in the file uri, use absolute URL. + if (strpos($file_uri, $public_dir) === FALSE) { + $file_uri = url($path, array('absolute' => TRUE)); + } + } + } + // Finally force https. + if ((isset($aggregate_settings['variables']['advagg_force_https_path']) + && $aggregate_settings['variables']['advagg_force_https_path']) + || (!isset($aggregate_settings['variables']['advagg_force_https_path']) + && variable_get('advagg_force_https_path', ADVAGG_FORCE_HTTPS_PATH)) + ) { + $file_uri = advagg_force_https_path($file_uri); + } + return $file_uri; +} + +/** + * Loads the stylesheet and resolves all @import commands. + * + * Loads a stylesheet and replaces @import commands with the contents of the + * imported file. Use this instead of file_get_contents when processing + * stylesheets. + * + * The returned contents are compressed removing white space and comments only + * when CSS aggregation is enabled. This optimization will not apply for + * color.module enabled themes with CSS aggregation turned off. + * + * @param string $file + * Name of the stylesheet to be processed. + * @param bool $optimize + * Defines if CSS contents should be compressed or not. + * @param bool $reset_basepath + * Used internally to facilitate recursive resolution of @import commands. + * + * @return string + * Contents of the stylesheet, including any resolved @import commands. + * + * @see drupal_load_stylesheet() + */ +function advagg_load_stylesheet($file, $optimize = FALSE, $reset_basepath = TRUE, $contents = '') { + // These static's are not cache variables, so we don't use drupal_static(). + static $_optimize, $basepath; + if ($reset_basepath) { + $basepath = ''; + } + // Store the value of $optimize for preg_replace_callback with nested @import + // loops. + if (isset($optimize)) { + $_optimize = $optimize; + } + + // Stylesheets are relative one to each other. Start by adding a base path + // prefix provided by the parent stylesheet (if necessary). + if ($basepath && !file_uri_scheme($file)) { + $file = $basepath . '/' . $file; + } + // Store the parent base path to restore it later. + $parent_base_path = $basepath; + // Set the current base path to process possible child imports. + $basepath = dirname($file); + + // Load the CSS stylesheet. We suppress errors because themes may specify + // stylesheets in their .info file that don't exist in the theme's path, + // but are merely there to disable certain module CSS files. + $content = ''; + if (empty($contents) && !empty($file)) { + $contents = (string) @advagg_file_get_contents($file); + } + if ($contents) { + // Return the processed stylesheet. + $content = advagg_load_stylesheet_content($contents, $_optimize); + } + + // Restore the parent base path as the file and its children are processed. + $basepath = $parent_base_path; + if ($_optimize) { + $content = trim($content); + } + return $content; +} + +/** + * Decodes UTF byte-order mark (BOM) into the encoding's name. + * + * @param string $data + * The data possibly containing a BOM. This can be the entire contents of + * a file, or just a fragment containing at least the first five bytes. + * + * @return string|bool + * The name of the encoding, or FALSE if no byte order mark was present. + * + * @see https://api.drupal.org/api/drupal/core!lib!Drupal!Component!Utility!Unicode.php/function/Unicode%3A%3AencodingFromBOM/8 + */ +function advagg_get_encoding_from_bom($data) { + static $bom_map = array( + "\xEF\xBB\xBF" => 'UTF-8', + "\xFE\xFF" => 'UTF-16BE', + "\xFF\xFE" => 'UTF-16LE', + "\x00\x00\xFE\xFF" => 'UTF-32BE', + "\xFF\xFE\x00\x00" => 'UTF-32LE', + "\x2B\x2F\x76\x38" => 'UTF-7', + "\x2B\x2F\x76\x39" => 'UTF-7', + "\x2B\x2F\x76\x2B" => 'UTF-7', + "\x2B\x2F\x76\x2F" => 'UTF-7', + "\x2B\x2F\x76\x38\x2D" => 'UTF-7', + ); + + foreach ($bom_map as $bom => $encoding) { + if (strpos($data, $bom) === 0) { + return $encoding; + } + } + return FALSE; +} + +/** + * Converts data to UTF-8. + * + * Requires the iconv, GNU recode or mbstring PHP extension. + * + * @param string $data + * The data to be converted. + * @param string $encoding + * The encoding that the data is in. + * + * @return string|bool + * Converted data or FALSE. + */ +function advagg_convert_to_utf8($data, $encoding) { + if (function_exists('iconv')) { + return @iconv($encoding, 'utf-8', $data); + } + elseif (function_exists('mb_convert_encoding')) { + return @mb_convert_encoding($data, 'utf-8', $encoding); + } + elseif (function_exists('recode_string')) { + return @recode_string($encoding . '..utf-8', $data); + } + // Cannot convert. + return FALSE; +} + +/** + * Processes the contents of a stylesheet for aggregation. + * + * @param string $contents + * The contents of the stylesheet. + * @param bool $optimize + * (Optional) Boolean whether CSS contents should be minified. Defaults to + * FALSE. + * + * @return string + * Contents of the stylesheet including the imported stylesheets. + * + * @see drupal_load_stylesheet_content() + */ +function advagg_load_stylesheet_content($contents, $optimize = FALSE) { + // If a BOM is found, convert the file to UTF-8. Used for inline CSS here. + $encoding = advagg_get_encoding_from_bom($contents); + if (!empty($encoding)) { + $contents = advagg_convert_to_utf8($contents, $encoding); + } + + if ($optimize) { + // Perform some safe CSS optimizations. + // Regexp to match comment blocks. + // Regexp to match double quoted strings. + // Regexp to match single quoted strings. + $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'; + $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"'; + $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"; + // Strip all comment blocks, but keep double/single quoted strings. + $contents = preg_replace( + "<($double_quot|$single_quot)|$comment>Ss", + "$1", + $contents + ); + + // Remove certain whitespace. + // There are different conditions for removing leading and trailing + // whitespace. + // @see http://php.net/manual/regexp.reference.subpatterns.php + $contents = preg_replace('< + # Do not strip any space from within single or double quotes + (' . $double_quot . '|' . $single_quot . ') + # Strip leading and trailing whitespace. + | \s*([@{};,])\s* + # Strip only leading whitespace from: + # - Closing parenthesis: Retain "@media (bar) and foo". + | \s+([\)]) + # Strip only trailing whitespace from: + # - Opening parenthesis: Retain "@media (bar) and foo". + # - Colon: Retain :pseudo-selectors. + | ([\(:])\s+ + >xSs', + // Only one of the three capturing groups will match, so its reference + // will contain the wanted value and the references for the + // two non-matching groups will be replaced with empty strings. + '$1$2$3$4', + $contents + ); + // End the file with a new line. + $contents = trim($contents); + $contents .= "\n"; + } + + // Remove multiple charset declarations for standards compliance (and fixing + // Safari problems). + $contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents); + + // Replaces @import commands with the actual stylesheet content. + // This happens recursively but omits external files. + $contents = preg_replace_callback('%@import\s*+(?:url\(\s*+)?+[\'"]?+(?![a-z]++:|/)([^\'"()\s]++)[\'"]?+\s*+\)?+\s*+;%i', '_advagg_load_stylesheet', $contents); + return $contents; +} + +/** + * Loads stylesheets recursively and returns contents with corrected paths. + * + * This function is used for recursive loading of stylesheets and + * returns the stylesheet content with all url() paths corrected. + * + * @param array $matches + * The matches from preg_replace_callback(). + * + * @return array + * String with altered internal url() paths. + * + * @see _drupal_load_stylesheet() + */ +function _advagg_load_stylesheet(array $matches) { + $filename = $matches[1]; + // Load the imported stylesheet and replace @import commands in there as well. + $file = advagg_load_stylesheet($filename, NULL, FALSE); + + if (empty($file)) { + if (strpos($matches[0], 'http://') === 0 + || strpos($matches[0], 'https://') === 0 + || strpos($matches[0], '//') === 0 + ) { + return $matches[0]; + } + if (variable_get('advagg_debug', ADVAGG_DEBUG) >= 2) { + watchdog('advagg-debug', 'Trying to load @file via @import statement but it was not found.', array('@file' => $filename), WATCHDOG_DEBUG); + } + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) <= 1) { + return $matches[0]; + } + else { + return ''; + } + } + + // Determine the file's directory. + $directory = dirname($filename); + // If the file is in the current directory, make sure '.' doesn't appear in + // the url() path. + $directory = $directory == '.' ? '' : $directory . '/'; + + // Alter all internal url() paths. Leave external paths alone. We don't need + // to normalize absolute paths here (i.e. remove folder/... segments) because + // that will be done later. + return preg_replace('%url\(\s*+([\'"]?+)(?![a-z]++:|/)([^\'")]+)([\'"]?+)\s*\)%i', 'url(\1' . $directory . '\2\3)', $file); +} + +/** + * Check and see if the aggressive cache can safely be enabled. + * + * @return array + * If there are no conflicts, this will return an empty array. + */ +function advagg_aggressive_cache_conflicts() { + $hooks = array('css_alter' => TRUE, 'js_alter' => TRUE); + foreach ($hooks as $hook => $values) { + $hooks[$hook] = module_implements($hook); + + // Also check themes as drupal_alter() allows for themes to alter things. + $themes = list_themes(); + $theme_keys = array_keys($themes); + if (!empty($theme_keys)) { + foreach ($theme_keys as $theme_key) { + $function = $theme_key . '_' . $hook; + // Search loaded themes. + if (function_exists($function)) { + $hooks[$hook][] = $theme_key; + continue; + } + // Skip disabled themes. + if (empty($themes[$theme_key]->status)) { + continue; + } + // Search enabled but not loaded themes. + $file = dirname($themes[$theme_key]->filename) . '/template.php'; + if (file_exists($file)) { + $contents = (string) @advagg_file_get_contents($file); + if (stripos($contents, $function)) { + $hooks[$hook][] = $theme_key; + } + } + } + } + } + + $whitelist = array( + // Core. + // + // locale_js_directory variable; default: languages. + // javascript_parsed variable; default: array(). + 'locale', + + // No control; same every time. + 'simpletest', + + // No control; same every time. + 'seven', + + // Popular contrib. + // + // No control; same every time. + 'at_commerce', + + // ais_adaptive_styles variable; Default: array(). + // ais_adaptive_styles_method; Default: 'both-max'. + // 'ais', + // + // No control; same every time. + 'bluecheese', + + // drupal_static('clientside_validation_settings') array. + // 'clientside_validation', + // + // version_compare(VERSION, '7.14', '<'). + 'conditional_fields', + + // _css_injector_load_rule() function. + // Changes the weight of all files added in init so no special handling. + // 'css_injector', + // + // disable_css_ . $theme . _all variable; default: FALSE. + // disable_css_ . $theme . _modules; default: array(). + // disable_css_ . $theme . _files; default: array(). + // 'disable_css', + // + // Empty call; commented code is same every time. + 'elfinder', + + // excluded_css_custom variable; Default: ''. + // excluded_javascript_custom variable; Default: ''. + // 'excluded', + // + // No control; same every time. + 'fences', + + // jqmulti_jquery_path() function. + // jqmulti_get_files() function. + // jqmulti_load_always variable; Default: FALSE. + // 'jqmulti', + // + // No control; same every time. + 'jquery_dollar', + + // labjs_suppress() function. + 'labjs_js', + + // Empty call. + 'panopoly_core', + + // speedy_js_production variable; Default: TRUE. + 'speedy', + + // logintoboggan_validating_id() function. + 'logintoboggan', + ); + // Allow other modules to modify the $whitelist. + // Call hook_advagg_aggressive_cache_conflicts_alter() + drupal_alter('advagg_aggressive_cache_conflicts', $whitelist); + + $questionable_modules = array(); + foreach ($hooks as $hook => $modules) { + foreach ($modules as $key => $module) { + // Anything from advagg is ok. + if (strpos($module, 'advagg') === 0 || strpos($module, '_advagg') === 0) { + unset($modules[$key]); + continue; + } + + // Remove known modules that should work with aggressive caching. + if (in_array($module, $whitelist)) { + unset($modules[$key]); + } + else { + $questionable_modules[$module] = $module; + } + } + } + return $questionable_modules; +} + +/** + * Alt to http_build_url(). + * + * @param array $parsed + * Array from parse_url(). + * @param bool $strip_query_and_fragment + * If set to TRUE the query and fragment will be removed from the output. + * + * @return string + * URI is returned. + * + * @see http://php.net/parse-url#85963 + */ +function advagg_glue_url(array $parsed, $strip_query_and_fragment = FALSE) { + $uri = ''; + if (isset($parsed['scheme'])) { + switch (strtolower($parsed['scheme'])) { + // Mailto uri. + case 'mailto': + $uri .= $parsed['scheme'] . ':'; + break; + + // Protocol relative uri. + case '//': + $uri .= $parsed['scheme']; + break; + + // Standard uri. + default: + $uri .= $parsed['scheme'] . '://'; + } + } + $uri .= isset($parsed['user']) ? $parsed['user'] . (isset($parsed['pass']) ? ':' . $parsed['pass'] : '') . '@' : ''; + $uri .= isset($parsed['host']) ? $parsed['host'] : ''; + $uri .= !empty($parsed['port']) ? ':' . $parsed['port'] : ''; + + if (isset($parsed['path'])) { + $uri .= (substr($parsed['path'], 0, 1) === '/') ? $parsed['path'] : ((!empty($uri) ? '/' : '') . $parsed['path']); + } + + if (!$strip_query_and_fragment) { + $uri .= isset($parsed['query']) ? '?' . $parsed['query'] : ''; + $uri .= isset($parsed['fragment']) ? '#' . $parsed['fragment'] : ''; + } + return $uri; +} + +/** + * Clear certain caches on form submit. + */ +function advagg_cache_clear_admin_submit() { + $cache_bins = advagg_flush_caches(); + foreach ($cache_bins as $bin) { + cache_clear_all('*', $bin, TRUE); + } + cache_clear_all('hook_info', 'cache_bootstrap'); + cache_clear_all('advagg_hooks_implemented:', 'cache_bootstrap', TRUE); +} + +/** + * Get the resource hint settings for the preload attribute. + * + * @param bool $return_defaults + * Default FALSE, TRUE returns the default values. + * + * @return array + * Ordered 2 dimensional array. + */ +function advagg_get_resource_hints_preload_settings($return_defaults = FALSE) { + $sub_defaults = array( + 'enabled' => 1, + 'push' => 0, + 'local' => 1, + 'external' => 1, + ); + // Collect your data. + $advagg_resource_hints_preload_settings_defaults = array( + 'style' => $sub_defaults + array( + '#weight' => -10, + 'title' => t('CSS Files'), + ), + 'font' => $sub_defaults + array( + '#weight' => -9, + 'title' => t('Font Files'), + ), + 'script' => $sub_defaults + array( + '#weight' => -8, + 'title' => t('JS Files'), + ), + 'svg' => $sub_defaults + array( + '#weight' => -7, + 'title' => t('SVG Files'), + ), + 'image' => $sub_defaults + array( + '#weight' => -6, + 'title' => t('Image Files'), + ), + 'all_others' => $sub_defaults + array( + '#weight' => -5, + 'title' => t('All Other Files'), + ), + ); + if ($return_defaults) { + return $advagg_resource_hints_preload_settings_defaults; + } + $advagg_resource_hints_preload_settings = variable_get('advagg_resource_hints_preload_settings', $advagg_resource_hints_preload_settings_defaults); + // Merge in defaults. + foreach ($advagg_resource_hints_preload_settings as $id => &$entry) { + if (isset($advagg_resource_hints_preload_settings_defaults[$id])) { + $entry += $advagg_resource_hints_preload_settings_defaults[$id]; + } + ksort($entry); + } + unset($entry); + // Sort the rows. + uasort($advagg_resource_hints_preload_settings, 'element_sort'); + return $advagg_resource_hints_preload_settings; +} + +/** + * See if the .htaccess file uses the RewriteBase directive. + * + * @param string $location + * The location of the .htaccess file. + * + * @return string + * The last active RewriteBase entry in htaccess. + */ +function advagg_htaccess_rewritebase($location = DRUPAL_ROOT) { + if (is_readable($location . '/.htaccess')) { + $htaccess = advagg_file_get_contents($location . '/.htaccess'); + $matches = array(); + $found = preg_match_all('/\\n\s*RewriteBase\s.*/i', $htaccess, $matches); + if ($found && !empty($matches[0])) { + $matches[0] = array_map('trim', $matches[0]); + return array_pop($matches[0]); + } + } + return ''; +} + +/** + * Get the latest version number for the remote version. + * + * @param array $library + * An associative array containing all information about the library. + * @param array $options + * An associative array containing options for the version parser. + * @param string $url + * URL for the remote version to lookup. + * + * @return string + * The latest version number as a string or 0 on failure. + */ +function advagg_get_github_version_json(array $library, array $options, $url) { + $http_options = array('timeout' => 2); + $package = drupal_http_request($url, $http_options); + if (empty($package->data)) { + // Try again. + $package = drupal_http_request($url, array('timeout' => 5)); + } + if (empty($package->data)) { + // Try again but force http. + $url = advagg_force_http_path($url); + $package = drupal_http_request($url, $http_options); + } + if (!empty($package->data)) { + $package = json_decode($package->data); + if (isset($package->version)) { + return (string) $package->version; + } + } + return 0; +} + +/** + * Get the latest version number for the remote version. + * + * @param array $library + * An associative array containing all information about the library. + * @param array $options + * An associative array containing options for the version parser. + * @param string $url + * URL for the remote version to lookup. + * + * @return string + * The latest version number as a string or 0 on failure. + */ +function advagg_get_github_version_txt(array $library, array $options, $url) { + $http_options = array('timeout' => 2); + $request = drupal_http_request($url, $http_options); + if (empty($request->data)) { + // Try again. + $request = drupal_http_request($url, array('timeout' => 5)); + } + if (empty($request->data)) { + // Try again but force http. + $url = advagg_force_http_path($url); + $request = drupal_http_request($url, $http_options); + } + if (!empty($request->data)) { + $matches = array(); + if (preg_match($options['pattern'], $request->data, $matches)) { + return $matches[1]; + } + } + return '0'; +} + +/** + * Update github version numbers to the latest. + * + * @param bool $refresh + * Set to TRUE to skip the cache and force a refresh of the data. + * + * @return mixed + * Key Value pair of the project name and remote version number. If $target is + * set then that version number is returned. + */ +function advagg_get_remote_libraries_versions($refresh = FALSE) { + $cid = __FUNCTION__; + $versions = array(); + if (!$refresh) { + $versions = advagg_get_remote_libraries_versions_cache($cid); + if (!empty($versions)) { + return $versions; + } + } + + if (is_callable('libraries_info')) { + $libraries = libraries_info(); + foreach ($libraries as $key => $library) { + // Get current version. + $libraries_detect = libraries_detect($key); + if (isset($libraries_detect['version'])) { + $versions[$key]['local'] = $libraries_detect['version']; + } + elseif (!empty($libraries_detect['local version'])) { + $versions[$key]['local'] = $libraries_detect['local version']; + } + + // Check if callback is live. + $remote = advagg_get_remote_libraries_version($key, $library, FALSE); + if (!empty($remote)) { + $versions[$key]['remote'] = $remote; + } + } + } + + if (!empty($versions)) { + cache_set($cid, $versions, 'cache_advagg_info'); + } + return $versions; +} + +/** + * Get the remote and local versions cache of the available libraries. + * + * @param string $cid + * Cache ID. + * + * @return array + * Library name => (local, remote). + */ +function advagg_get_remote_libraries_versions_cache($cid = '') { + if (empty($cid)) { + $cid = 'advagg_get_remote_libraries_versions'; + } + $versions = &drupal_static($cid, array()); + if (empty($versions)) { + $cache = cache_get($cid, 'cache_advagg_info'); + if (!empty($cache) && !empty($cache->data)) { + $versions = $cache->data; + } + } + return $versions; +} + +/** + * Get the latest version number for the remote version. + * + * @param string $name + * Name of the library. + * @param array $library + * An associative array containing all information about the library. + * @param bool $use_cache + * TRUE try the cache first. + * + * @return string + * The latest version number as a string or 0 on failure. + */ +function advagg_get_remote_libraries_version($name, array $library, $use_cache = TRUE) { + if ($use_cache) { + $cid = 'advagg_get_remote_libraries_versions'; + $versions = advagg_get_remote_libraries_versions_cache($cid); + if (isset($versions[$name]['remote'])) { + return $versions[$name]['remote']; + } + } + + // Remote url is not set, see if we can generate it given the current data. + if (empty($library['remote']['url']) + && !empty($library['version arguments']) + ) { + if (!isset($library['version arguments']['file']) + && isset($library['version arguments']['variants']) + ) { + // Use the first variant. + $file = reset($library['version arguments']['variants']); + $library['version arguments']['file'] = $file['file']; + $library['version arguments']['pattern'] = $file['pattern']; + } + + if (!empty($library['version arguments']['file'])) { + if (!empty($library['vendor url'])) { + // Use vendor url if it's a github one. + if (strpos($library['vendor url'], 'https://github.com/') === 0) { + $parsed_vendor = @parse_url($library['vendor url']); + // Previously: https://rawgit.com{$parsed_vendor['path']}/master/{$library['version arguments']['file']} . + $library['remote']['url'] = "https://cdn.jsdelivr.net/gh{$parsed_vendor['path']}@master/{$library['version arguments']['file']}"; + } + } + if (empty($library['remote']['url']) && !empty($library['download url'])) { + // Use download url if it's a github one. + if (strpos($library['download url'], 'https://github.com/') === 0) { + $parsed_download = @parse_url($library['download url']); + $paths = explode('/', $parsed_download['path']); + $parsed_download['path'] = "/{$paths[1]}/{$paths[2]}"; + // Previously: https://rawgit.com{$parsed_download['path']}/master/{$library['version arguments']['file']} . + $library['remote']['url'] = "https://cdn.jsdelivr.net/gh{$parsed_download['path']}@master/{$library['version arguments']['file']}"; + } + } + } + } + + // Remote callback is not set, try to generate it given the current data. + if (empty($library['remote']['callback']) + && isset($library['version arguments']['file']) + ) { + if (!empty($library['version callback'])) { + // Use defined parser. + $library['remote']['callback'] = $library['version callback']; + } + else { + if ($library['version arguments']['file'] === 'package.json') { + // JSON parser. + $library['remote']['callback'] = 'advagg_get_github_version_json'; + } + else { + // Text parser. + $library['remote']['callback'] = 'advagg_get_github_version_txt'; + } + } + } + + // Get remote version. + $return = 0; + if (!empty($library['remote']) + && !empty($library['remote']['callback']) + && !empty($library['remote']['url']) + && isset($library['version arguments']) + && is_callable($library['remote']['callback']) + ) { + $return = $library['remote']['callback']($library, $library['version arguments'], $library['remote']['url']); + + // Try package.json on failure. + if (empty($return) && $library['version arguments']['file'] !== 'package.json') { + $pos = strrpos($library['remote']['url'], $library['version arguments']['file']); + $library['remote']['url'] = substr($library['remote']['url'], 0, $pos) . 'package.json'; + $library['remote']['callback'] = 'advagg_get_github_version_json'; + $return = $library['remote']['callback']($library, $library['version arguments'], $library['remote']['url']); + } + } + return $return; +} + +/** + * Get the latest version number for the remote version. + * + * @param string $name + * Name of the library. + * @param string $module_name + * Name of the module where the library is registered. + * + * @return array + * The library array. + */ +function advagg_get_library($name, $module_name) { + $library = cache_get($name, 'cache_libraries'); + if ($library) { + $library = $library->data; + } + else { + if (is_callable('libraries_detect')) { + $library = libraries_detect($name); + } + elseif (is_callable("{$module_name}_libraries_info")) { + $library = call_user_func("{$module_name}_libraries_info"); + $library = $library[$name]; + } + } + return $library; +} + +/** + * Alter the js array fixing the type key if set incorrectly. + * + * @param array $array + * CSS or JS array. + * @param string $type + * CSS or JS. + */ +function advagg_fix_type(array &$array, $type) { + // Skip if advagg is disabled. + if (!advagg_enabled()) { + return; + } + + // Skip if setting is turned off. + if ($type === 'css' && !variable_get('advagg_css_fix_type', ADVAGG_CSS_FIX_TYPE)) { + return; + } + if ($type === 'js' && !variable_get('advagg_js_fix_type', ADVAGG_JS_FIX_TYPE)) { + return; + } + + // Fix type if it was incorrectly set. + // Get hostname and base path. + $mod_base_url = substr($GLOBALS['base_root'] . $GLOBALS['base_path'], strpos($GLOBALS['base_root'] . $GLOBALS['base_path'], '//') + 2); + $mod_base_url_len = strlen($mod_base_url); + + foreach ($array as &$value) { + // Skip if the data is empty or not a string. + if (empty($value['data']) || !is_string($value['data'])) { + continue; + } + + // Default to file if type is not set. + if (!isset($value['type'])) { + $value['type'] = 'file'; + } + + // If inline, be done with processing. + if ($value['type'] === 'inline') { + continue; + } + + // Default to file if not file, inline, external, setting. + if ($value['type'] !== 'file' + && $value['type'] !== 'inline' + && $value['type'] !== 'external' + && $value['type'] !== 'setting' + ) { + if ($value['type'] === 'settings') { + $value['type'] = 'setting'; + } + else { + $value['type'] = 'file'; + } + } + + $lower = strtolower($value['data']); + $http_pos = strpos($lower, 'http://'); + $https_pos = strpos($lower, 'https://'); + $double_slash_pos = strpos($lower, '//'); + $tripple_slash_pos = strpos($lower, '///'); + $mod_base_url_pos = stripos($value['data'], $mod_base_url); + + // If type is external but doesn't start with http, https, or // change it + // to file. + if ($value['type'] === 'external' + && $http_pos !== 0 + && $https_pos !== 0 + && $double_slash_pos !== 0 + ) { + if (is_readable($value['data'])) { + $value['type'] = 'file'; + } + else { + // Fix for subdir issues. + $parsed = parse_url($value['data']); + if (strpos($parsed['path'], $GLOBALS['base_path']) === 0) { + $path = substr($parsed['path'], strlen($GLOBALS['base_path'])); + if (is_readable($path)) { + $value['data'] = $path; + $value['type'] = 'file'; + } + } + } + } + + // If type is file but it starts with http, https, or // change it to + // external. Skip tripple slash for local files. + if ($value['type'] === 'file' + && ($http_pos === 0 + || $https_pos === 0 + || ($double_slash_pos === 0 && $tripple_slash_pos === FALSE)) + ) { + $value['type'] = 'external'; + } + + // If type is external and starts with http, https, or // but points to + // this host change to file, but move it to the top of the aggregation + // stack. + if ($value['type'] === 'external' + && $mod_base_url_pos - 2 === $double_slash_pos + && ($http_pos === 0 + || $https_pos === 0 + || $double_slash_pos === 0 + ) + ) { + $path = substr($value['data'], $mod_base_url_pos + $mod_base_url_len); + if (is_readable($path)) { + $value['data'] = $path; + $value['type'] = 'file'; + $value['group'] = JS_LIBRARY; + $value['every_page'] = TRUE; + $value['weight'] = -40000; + } + else { + // Fix for subdir issues. + $parsed = parse_url($path); + if (strpos($parsed['path'], $GLOBALS['base_path']) === 0) { + $path = substr($parsed['path'], strlen($GLOBALS['base_path'])); + if (is_readable($path)) { + $value['data'] = $path; + $value['type'] = 'file'; + $value['group'] = JS_LIBRARY; + $value['every_page'] = TRUE; + $value['weight'] = -40000; + } + } + } + } + } + unset($value); +} + +/** + * Alter the CSS or JS array removing empty files from the aggregates. + * + * @param array $array + * CSS or JS array. + */ +function advagg_remove_empty_files(array &$array) { + if (!variable_get('advagg_js_remove_empty_files', ADVAGG_JS_REMOVE_EMPTY_FILES)) { + return; + } + + if (variable_get('advagg_fast_filesystem', ADVAGG_FAST_FILESYSTEM)) { + foreach ($array as $key => $value) { + if ($value['type'] !== 'file') { + continue; + } + // Check locally. + if (!is_readable($value['data']) || filesize($value['data']) == 0) { + unset($array[$key]); + } + } + } + else { + module_load_include('inc', 'advagg', 'advagg'); + $files = array(); + foreach ($array as $key => $value) { + if ($value['type'] !== 'file') { + continue; + } + $files[$key] = $value['data']; + } + // Check cache/db. + $info = advagg_get_info_on_files($files); + foreach ($info as $key => $values) { + if (empty($values['filesize'])) { + $key = array_search($values['data'], $files); + if (isset($array[$key])) { + unset($array[$key]); + } + } + } + } +} + +/** + * Alter the CSS or JS array adding in DNS domain info. + * + * @param array $array + * CSS or JS array. + * @param string $type + * CSS or JS. + */ +function advagg_add_default_dns_lookups(array &$array, $type) { + // Skip if advagg is disabled. + if (!advagg_enabled()) { + return; + } + // Remove this return once CSS lookups are needed. + if ($type !== 'js') { + return; + } + + // Add DNS information for some of the more popular modules. + foreach ($array as &$value) { + if (!is_string($value['data'])) { + continue; + } + // Google Ad Manager. + if (strpos($value['data'], '/google_service.') !== FALSE) { + if (!empty($value['dns_prefetch']) && is_string($value['dns_prefetch'])) { + $temp = $value['dns_prefetch']; + unset($value['dns_prefetch']); + $value['dns_prefetch'] = array($temp); + } + // Domains in the google_service.js file. + $value['dns_prefetch'][] = 'https://csi.gstatic.com'; + $value['dns_prefetch'][] = 'https://pubads.g.doubleclick.net'; + $value['dns_prefetch'][] = 'https://partner.googleadservices.com'; + $value['dns_prefetch'][] = 'https://securepubads.g.doubleclick.net'; + + // Domains in the google_ads.js file. + $value['dns_prefetch'][] = 'https://pagead2.googlesyndication.com'; + + // Other domains that usually get hit. + $value['dns_prefetch'][] = 'https://cm.g.doubleclick.net'; + $value['dns_prefetch'][] = 'https://tpc.googlesyndication.com'; + } + + // Google Analytics. + if (strpos($value['data'], 'GoogleAnalyticsObject') !== FALSE + || strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE + ) { + if (!empty($value['dns_prefetch']) && is_string($value['dns_prefetch'])) { + $temp = $value['dns_prefetch']; + unset($value['dns_prefetch']); + $value['dns_prefetch'] = array($temp); + } + if ($GLOBALS['is_https'] && strpos($value['data'], '.google-analytics.com/ga.js') !== FALSE) { + $value['dns_prefetch'][] = 'https://ssl.google-analytics.com'; + } + else { + $value['dns_prefetch'][] = 'https://www.google-analytics.com'; + } + $value['dns_prefetch'][] = 'https://stats.g.doubleclick.net'; + } + } +} + +/** + * Insert element into an array at a specific position. + * + * @param array $input_array + * The original array. + * @param array $new_value + * The element that is getting inserted. + * @param int $location_key + * The key location. + * + * @return array + * The new array with the element merged in. + */ +function advagg_insert_into_array_at_location(array $input_array, array $new_value, $location_key) { + return array_merge(array_slice($input_array, 0, $location_key), $new_value, array_slice($input_array, $location_key)); +} + +/** + * Insert element into an array at a specific key location. + * + * @param array $input_array + * The original array. + * @param array $insert + * The element that is getting inserted; array(key => value). + * @param string $target_key + * The key name. + * @param int $location + * After is 1 , 0 is replace, -1 is before. + * + * @return array + * The new array with the element merged in. + */ +function advagg_insert_into_array_at_key(array $input_array, array $insert, $target_key, $location = 1) { + $output = array(); + $new_value = reset($insert); + $new_key = key($insert); + foreach ($input_array as $key => $value) { + if ($key === $target_key) { + // Insert before. + if ($location == -1) { + $output[$new_key] = $new_value; + $output[$key] = $value; + } + // Replace. + if ($location == 0) { + $output[$new_key] = $new_value; + } + // After. + if ($location == 1) { + $output[$key] = $value; + $output[$new_key] = $new_value; + } + } + else { + // Pick next key if there is an number collision. + if (is_numeric($key)) { + while (isset($output[$key])) { + $key++; + } + } + $output[$key] = $value; + } + } + // Add to array if not found. + if (!isset($output[$new_key])) { + // Before everything. + if ($location == -1) { + $output = $insert + $output; + } + // After everything. + if ($location == 1) { + $output[$new_key] = $new_value; + } + + } + return $output; +} + +/** + * Given a URL output a filename. + * + * @param string $url + * The url. + * @param string $strict + * If FALSE then slashes will be kept. + * + * @return string + * The filename. + */ +function advagg_url_to_filename($url, $strict = TRUE) { + // Keep filtering till there are no more changes. + $decoded1 = _advagg_url_to_filename_filter($url, $strict); + $decoded2 = _advagg_url_to_filename_filter($decoded1, $strict); + while ($decoded1 != $decoded2) { + $decoded1 = _advagg_url_to_filename_filter($decoded2, $strict); + $decoded2 = _advagg_url_to_filename_filter($decoded1, $strict); + } + $filename = $decoded1; + + // Shorten filename to a max of 250 characters. + $filename = mb_strcut($filename, 0, 250, mb_detect_encoding($filename)); + return $filename; +} + +/** + * Given a URL output a filtered filename. + * + * @param string $url + * The url. + * @param string $strict + * If FALSE then slashes will be kept. + * + * @return string + * The filename. + */ +function _advagg_url_to_filename_filter($url, $strict = TRUE) { + // URL Decode if needed. + $decoded1 = $url; + $decoded2 = rawurldecode($decoded1); + while ($decoded1 != $decoded2) { + $decoded1 = rawurldecode($decoded2); + $decoded2 = rawurldecode($decoded1); + } + $url = $decoded1; + + // Replace url spaces with a dash. + $filename = str_replace(array('%20', '+'), '-', $url); + + // Remove file system reserved characters + // https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words + // Remove control charters + // http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + // Remove non-printing characters DEL, NO-BREAK SPACE, SOFT HYPHEN + // Remove URI reserved characters + // https://tools.ietf.org/html/rfc3986#section-2.2 + // Remove URL unsafe characters + // https://www.ietf.org/rfc/rfc1738.txt + if ($strict) { + $filename = preg_replace('~[<>:"/\\|?*]|[\x00-\x1F]|[\x7F\xA0\xAD]|[#\[\]@!$&\'()+,;=%]|[{}^\~`]~x', '-', $filename); + } + else { + $filename = preg_replace('~[<>:"\\|?*]|[\x00-\x1F]|[\x7F\xA0\xAD]|[#\[\]@!$&\'()+,;=%]|[{}^\~`]~x', '-', $filename); + } + + // Replce all white spaces with a dash. + $filename = preg_replace('/[\r\n\t -]+/', '-', $filename); + + // Avoid ".", ".." or ".hiddenFiles". + $filename = ltrim($filename, '.-'); + + // Compress spaces in a file name and replace with a dash. + // Compress underscores in a file name and replace with a dash. + // Compress dashes in a file name and replace with a dash. + $filename = preg_replace(array('/ +/', '/_+/', '/-+/'), '-', $filename); + + // Compress dashes and dots in a file name and replace with a dot. + $filename = preg_replace(array('/-*\.-*/', '/\.{2,}/'), '.', $filename); + + // Lowercase for windows/unix interoperability + // http://support.microsoft.com/kb/100625. + $filename = mb_strtolower($filename, 'UTF-8'); + // Remove ? \ .. + $filename = str_replace(array('?', '\\', '..'), '', $filename); + + // ".file-name.-" becomes "file-name". + $filename = trim($filename, '.-'); + + return $filename; +} + +/** + * Given a URI return TRUE if it is external. + * + * @param string $uri + * The uri. + * + * @return bool + * TRUE if external. + */ +function advagg_is_external($uri) { + $http_pos = strpos($uri, 'http://'); + $https_pos = strpos($uri, 'https://'); + $double_slash_pos = strpos($uri, '//'); + if ($http_pos !== 0 + && $https_pos !== 0 + && $double_slash_pos !== 0 + ) { + return FALSE; + } + return TRUE; +} + +/** + * Same as file_get_contents() but will convert string to UTF-8 if needed. + * + * @return mixed + * The files contents or FALSE on failure. + */ +function advagg_file_get_contents() { + // Get the file contents. + $file_contents = call_user_func_array('file_get_contents', func_get_args()); + if ($file_contents === FALSE) { + return $file_contents; + } + + // If a BOM is found, convert the file to UTF-8. + $encoding = advagg_get_encoding_from_bom($file_contents); + if (!empty($encoding)) { + $file_contents = advagg_convert_to_utf8($file_contents, $encoding); + } + + return $file_contents; +} + +/** + * Get the description text based off the library version. + * + * @param string $library_name + * Name of the library. + * @param string $module_name + * Name of the module that contains hook_libraries_info for this library. + * + * @return array + * Description text and info array. + */ +function advagg_get_version_description($library_name, $module_name, $only_remote_ok = FALSE) { + $t = get_t(); + + // Get local and external library version numbers. + $versions = &drupal_static(__FUNCTION__); + if (!isset($versions)) { + $versions = advagg_get_remote_libraries_versions(TRUE); + } + + $description = ''; + if (!empty($versions[$library_name]['remote']) + && (empty($versions[$library_name]['local']) + || $versions[$library_name]['local'] !== $versions[$library_name]['remote'] + )) { + $library = advagg_get_library($library_name, $module_name); + if (empty($versions[$library_name]['local'])) { + $versions[$library_name]['local'] = 'NULL'; + } + if (!empty($library['installed'])) { + $description = $t('Go to the @name page and download the latest version (@remote) into the @lib_path folder. An example valid filename is %version_file. Current Version: %version.', array( + '@name' => $library['name'], + '@lib_path' => $library['library path'], + '@url-page' => $library['vendor url'], + '@url-zip' => $library['download url'], + '@remote' => $versions[$library_name]['remote'], + '%version' => $versions[$library_name]['local'], + '%version_file' => $library['library path'] . '/' . $library['version arguments']['file'], + )); + } + elseif (!$only_remote_ok && is_callable('libraries_load')) { + $description = $t('Go to the @name page and download the latest version (@remote) into the libraries folder (usually sites/all/libraries). An example valid filename is %version_file.', array( + '@name' => $library['name'], + '@url-page' => $library['vendor url'], + '@url-zip' => $library['download url'], + '@remote' => $versions[$library_name]['remote'], + '%version_file' => "sites/all/libraries/$library_name/{$library['version arguments']['file']}", + )); + } + elseif (!$only_remote_ok) { + $description = $t('Install the Libraries API module and then go to the @name page and download the latest version (@remote) into the libraries folder (usually sites/all/libraries). An example valid filename is %version_file.', array( + '@name' => $library['name'], + '@url-page' => $library['vendor url'], + '@url-zip' => $library['download url'], + '@remote' => $versions[$library_name]['remote'], + '%version_file' => "sites/all/libraries/$library_name/{$library['version arguments']['file']}", + '@url-lib-api' => 'https://www.drupal.org/project/libraries', + )); + } + } + $path = drupal_get_path('module', $module_name); + $info = drupal_parse_info_file("{$path}/{$module_name}.info"); + + // Check if library was unzipped with -master. + if (!empty($description) && is_callable('libraries_get_libraries')) { + $libraries_paths = libraries_get_libraries(); + if (!empty($libraries_paths["{$library_name}-master"])) { + $description = $t('Rename @lib_dir_master to @lib_dir at this location: @lib_path_master.', array( + '@lib_dir_master' => "{$library_name}-master", + '@lib_path_master' => $libraries_paths["{$library_name}-master"], + '@lib_dir' => $library_name, + )); + } + } + + return array($description, $info); +} + +/** + * Given a advagg type this will return the most recent aggregate from the db. + * + * @param string $type + * String: css or js. + * + * @return string + * Returns advagg filename or an empty string on failure. + */ +function advagg_generate_advagg_filename_from_db($type) { + // Get the most recently accessed file from the database. + $query = db_select('advagg_aggregates_versions', 'aav'); + $query->join('advagg_aggregates', 'aa', 'aa.aggregate_filenames_hash = + aav.aggregate_filenames_hash'); + $query->join('advagg_files', 'af', 'af.filename_hash = aa.filename_hash AND + af.filetype = :type', array(':type' => $type)); + $query = $query->fields('aav', array('aggregate_filenames_hash', 'aggregate_contents_hash')) + ->orderBy('atime', 'DESC') + ->range(0, 1); + $results = $query->execute(); + if (empty($results)) { + return ''; + } + $hooks_hash = advagg_get_current_hooks_hash(); + foreach ($results as $row) { + return $type . ADVAGG_SPACE . $row->aggregate_filenames_hash . ADVAGG_SPACE . $row->aggregate_contents_hash . ADVAGG_SPACE . $hooks_hash . '.' . $type; + } +} + +/** + * Display a message if there are requirement issues with AdvAgg. + * + * @param array $requirements + * Other requirements to list besides the standard ones. + */ +function advagg_display_message_if_requirements_not_met(array $requirements = array()) { + include_once DRUPAL_ROOT . '/includes/install.inc'; + module_load_include('install', 'advagg'); + $requirements += advagg_install_fast_checks(); + if (!empty($requirements)) { + module_load_include('admin.inc', 'system'); + usort($requirements, '_system_sort_requirements'); + $report = theme('status_report', array('requirements' => $requirements)); + drupal_set_message(t('Go to the status report page and fix the issues that AdvAgg lists there. Sneak peak: !report', array( + '@url' => url('admin/reports/status'), + '!report' => $report, + ))); + } +} + +/** + * Add in the preload header for CSS and JS external files. + * + * @param string $url + * The url of the external host. + * + * @return bool + * TRUE if it was added to the head. + */ +function advagg_add_preload_header($url = '', $as = '') { + // Get defaults and setup static's. + $list = &drupal_static(__FUNCTION__ . ':list', array()); + $output = &drupal_static(__FUNCTION__ . ':output'); + $header_strlen = &drupal_static(__FUNCTION__ . ':strlen', 0); + static $resource_hints_preload_order; + if (!isset($resource_hints_preload_order)) { + $resource_hints_preload_order = advagg_get_resource_hints_preload_settings(); + } + if (!isset($output)) { + $keys = array_keys($resource_hints_preload_order); + $output = array_fill_keys($keys, array()); + } + + // Output headers. + if (empty($url)) { + + // Call hook_advagg_preload_header_alter() + drupal_alter('advagg_preload_header', $output); + + // Build header string. + $header_value = ''; + foreach ($output as $value) { + if (!empty($value)) { + // Remove empty values. + $value = array_filter($value); + foreach ($value as $string) { + $header_strlen += strlen($string) + 2; + // Don't add if over the limit. + if ($header_strlen >= variable_get('advagg_resource_hints_preload_max_size', ADVAGG_RESOURCE_HINTS_PRELOAD_MAX_SIZE)) { + continue; + } + // Add to $header_value string. + if (empty($header_value)) { + $header_value = $string; + } + else { + $header_value .= ',' . $string; + } + } + } + } + if (!empty($header_value)) { + drupal_add_http_header('Link', $header_value, TRUE); + } + return FALSE; + } + + // Check for duplicates. + if (isset($list[$url])) { + return FALSE; + } + + // Fill in missing info. + $payload = "<{$url}>; rel=preload"; + list($as, $type, $crossorigin, $parse) = advagg_get_preload_info_from_url($url, $as); + if (!empty($as) && $as === 'php') { + $list[$url] = FALSE; + return FALSE; + } + + $additional_info = array(); + if (!empty($crossorigin)) { + $additional_info[] = "crossorigin"; + } + if (!empty($type)) { + $additional_info[] = $type; + } + $additional_info = implode('; ', $additional_info); + + // Get settings. + if (!empty($as) && !empty($resource_hints_preload_order[$as])) { + $settings = $resource_hints_preload_order[$as]; + } + elseif (!empty($resource_hints_preload_order['all_others'])) { + $settings = $resource_hints_preload_order['all_others']; + } + + // Apply settings. + if (!$settings['enabled']) { + $list[$url] = FALSE; + return FALSE; + } + if (!$settings['local'] && empty($parse['host'])) { + $list[$url] = FALSE; + return FALSE; + } + if (!$settings['external'] && !empty($parse['host'])) { + $list[$url] = FALSE; + return FALSE; + } + + // Add additional info and queue. + if (!empty($as)) { + $payload .= "; as={$as}"; + } + if (!empty($additional_info)) { + $payload .= "; {$additional_info}"; + } + if (!$settings['push']) { + $payload .= "; nopush"; + } + $list[$url] = TRUE; + $output[$as][] = $payload; +} + +/** + * Given a link get the as, type, and crossorigin attributes. + * + * @param string $url + * Link to the url that will be preloaded. + * @param string $as + * What type of content is this; font, image, video, etc. + * @param string $type + * The mime type of the file. + * @param string $crossorigin + * Preload cross-origin resources; fonts always need to be CORS. + * + * @return array + * An array containing + */ +function advagg_get_preload_info_from_url($url, $as = '', $type = '', $crossorigin = NULL) { + // Get extension. + $parse = @parse_url($url); + if (empty($parse['path'])) { + return FALSE; + } + $file_ext = strtolower(pathinfo($parse['path'], PATHINFO_EXTENSION)); + if (empty($file_ext)) { + $file_ext = basename($parse['path']); + } + + // Detect missing parts. + $list = advagg_preload_list(); + if (empty($as) && !empty($file_ext)) { + foreach ($list as $as_key => $list_type) { + $key = array_search($file_ext, $list_type); + if ($key !== FALSE) { + $as = $as_key; + // Type of font, ext is svg but file doesn't contain string font. + // This will be treated as an image. + if ($as === 'font' + && $file_ext === 'svg' + && stripos($url, 'font') === FALSE + ) { + $as = ''; + } + } + if (!empty($as)) { + break; + } + } + } + if (empty($type) && !empty($as)) { + $type = "$as/$file_ext"; + if ($file_ext === 'svg') { + $type .= '+xml'; + } + if ($file_ext === 'js') { + $type = 'text/javascript'; + } + } + if ($as === 'font' && is_null($crossorigin)) { + $crossorigin = 'anonymous'; + } + return array($as, $type, $crossorigin, $parse); +} + +/** + * Add preload link to the top of the html head. + * + * @param string $url + * Link to the url that will be preloaded. + * @param string $media + * Media types or media queries, allowing for responsive preloading. + * @param string $as + * What type of content is this; font, image, video, etc. + * @param string $type + * The mime type of the file. + * @param string $crossorigin + * Preload cross-origin resources; fonts always need to be CORS. + */ +function advagg_add_preload_link($url, $media = '', $as = '', $type = '', $crossorigin = NULL) { + static $weight = -2000; + $weight += 0.0001; + + $href = advagg_file_create_url($url); + $key = "advagg_preload:{$href}"; + + // Return here if url has already been added. + $stored_head = drupal_static('drupal_add_html_head'); + if (isset($stored_head[$key])) { + return TRUE; + } + + // Add basic attributes. + $attributes = array( + 'rel' => 'preload', + 'href' => $href, + ); + + // Fill in missing info. + list($as, $type, $crossorigin) = advagg_get_preload_info_from_url($url, $as, $type, $crossorigin); + + // Exit if no as. + if (empty($as)) { + return FALSE; + } + // Build up attributes array. + $attributes['as'] = $as; + if (!empty($type)) { + $attributes['type'] = $type; + } + if (!empty($crossorigin)) { + $attributes['crossorigin'] = $crossorigin; + } + if (!empty($media)) { + $attributes['media'] = $media; + } + // Call hook_advagg_preload_link_attributes_alter() + drupal_alter('advagg_preload_link_attributes', $attributes); + + // Add to HTML head. + $element = array( + '#type' => 'html_tag', + '#tag' => 'link', + '#attributes' => $attributes, + '#weight' => $weight, + ); + drupal_add_html_head($element, $key); + return TRUE; +} + +/** + * Generate a list of file types for the as field given the extension. + * + * @return array + * Returns an array of arrays. + */ +function advagg_preload_list() { + $list = array( + 'font' => array( + 'woff2', + 'woff', + 'ttf', + 'otf', + 'eot', + // Need to check if the svg file is in a font folder. + 'svg', + ), + 'image' => array( + 'gif', + 'jpg', + 'jpeg', + 'jpe', + 'jif', + 'jfif', + 'jfi', + 'png', + 'webp', + 'jp2', + 'jpx', + 'jxr', + 'heif', + 'heic', + 'bpg', + 'svg', + ), + 'style' => array( + 'css', + ), + 'script' => array( + 'js', + ), + 'video' => array( + 'mp4', + 'webm', + 'ogg', + ), + ); + + // Call hook_advagg_preload_list_alter() + drupal_alter('advagg_preload_list', $list); + return $list; +} + +/** + * Save form defaults or recommended values. + * + * @param array $element + * Form element or child element. + * + * @return array + * An array of form names and the recommended value for that setting. + */ +function advagg_find_all_recommended_admin_values(array &$element, $key_name = '#recommended_value') { + $results = array(); + $children = element_children($element); + foreach ($children as $key) { + $child = $element[$key]; + if (is_array($child)) { + if (!empty($child['#type']) + && !empty($child['#name']) + && isset($child[$key_name]) + ) { + $results[$child['#name']] = $child[$key_name]; + } + $results = array_merge($results, advagg_find_all_recommended_admin_values($child, $key_name)); + } + unset($child); + } + return $results; +} + +/** + * Get form values that have changed. + * + * @param array $element + * Form element or child element. + * + * @return array + * An array of form names and the recommended value for that setting. + */ +function advagg_find_all_changed_admin_values(array &$element) { + $results = array(); + $children = element_children($element); + foreach ($children as $key) { + $child = $element[$key]; + if (is_array($child)) { + if (!empty($child['#type']) + && !empty($child['#name']) + && isset($child['#default_value']) + && isset($child['#value']) + ) { + if ($child['#type'] === 'checkboxes') { + // Add in not selected by default values. + $child['#value'] += array_diff_assoc($child['#default_value'], $child['#value']); + } + if ($child['#default_value'] != $child['#value']) { + $results[$child['#name']] = array($child['#value'], $child['#default_value']); + } + } + $results = array_merge($results, advagg_find_all_changed_admin_values($child)); + } + unset($child); + } + return $results; +} + +/** + * Get form title and description. + * + * @param array $element + * Form element or child element. + * + * @return array + * An array of form names and the recommended value for that setting. + */ +function advagg_find_title(array &$element) { + $results = array(); + $children = element_children($element); + foreach ($children as $key) { + $child = $element[$key]; + if (is_array($child)) { + if (!empty($child['#type']) + && !empty($child['#name']) + && isset($child['#title']) + && isset($child['#default_value']) + && !isset($results[$child['#name']]) + && $child['#type'] !== 'radio' + ) { + $results[$child['#name']] = $child['#title']; + } + $results = array_merge($results, advagg_find_title($child)); + } + unset($child); + } + return $results; +} + +/** + * Save form defaults or recommended values. + * + * @param array $form_state + * Form state array from drupal form submit. + * @param string $trigger_key + * The key of the setting from the form that controls this. + */ +function advagg_set_admin_form_defaults_recommended(array &$form_state, $trigger_key) { + $changed = array(); + $recommended_values = array(); + + // Set to recommended values. + if ($form_state['values'][$trigger_key] == 2) { + $recommended_values = advagg_find_all_recommended_admin_values($form_state['complete form']); + foreach ($recommended_values as $key => $value) { + if (!isset($form_state['values'][$key])) { + $changed[$key] = array($value); + } + elseif ($value != $form_state['values'][$key]) { + $changed[$key] = array($value, $form_state['values'][$key]); + } + $form_state['values'][$key] = $value; + } + } + + // Set to defaults. + if ($form_state['values'][$trigger_key] == 0 || $form_state['values'][$trigger_key] == 2) { + // Reset to defaults. + foreach ($form_state['values'] as $key => &$value) { + // Skip non advagg settings, trigger key, or if a recommended value. + if (strpos($key, 'advagg_') !== 0 + || $key === $trigger_key + || isset($changed[$key]) + || isset($recommended_values[$key]) + ) { + continue; + } + + // Default to FALSE. + $default = FALSE; + // Get easy defaults. + if (defined(strtoupper($key))) { + $default = constant(strtoupper($key)); + } + + // Get more complex default values. + if ($key === 'advagg_resource_hints_preload_settings') { + $default = advagg_get_resource_hints_preload_settings(TRUE); + foreach ($default as $key => &$values) { + $default[$key]['weight'] = $values['#weight']; + unset($default[$key]['#weight'], $values['#weight'], $default[$key]['title'], $values['title']); + ksort($values); + } + ksort($default); + foreach ($value as $key => &$values) { + ksort($values); + } + ksort($value); + } + if ($key === 'advagg_relocate_css_inline_import_browsers') { + $default = array( + 'woff2' => 'woff2', + 'woff' => 'woff', + 'ttf' => 'ttf', + 'eot' => 0, + 'svg' => 0, + ); + } + + // See if it changed. + if ($default != $value) { + // After, Before. + $changed[$key] = array($default, $value); + $value = $default; + } + } + } + + if ($form_state['values'][$trigger_key] == 4) { + $changed = advagg_find_all_changed_admin_values($form_state['complete form']); + if (isset($changed[$trigger_key])) { + unset($changed[$trigger_key]); + } + } + + $all_titles_descriptions = advagg_find_title($form_state['complete form']); + foreach ($changed as $key => $values) { + + // Remove things that didn't really change. + if (isset($values[1])) { + if ($values[0] == $values[1]) { + unset($changed[$key]); + continue; + } + if (is_string($values[0]) + && is_string($values[1]) + && trim($values[0]) == trim($values[1]) + ) { + unset($changed[$key]); + continue; + } + } + + // Make output nicer. + if (!isset($values[1])) { + $values[1] = NULL; + } + if (is_bool($values[0]) && !is_bool($values[1]) + || !is_bool($values[0]) && is_bool($values[1]) + ) { + $values[0] = (bool) $values[0]; + $values[1] = (bool) $values[1]; + } + if (is_int($values[0]) && !is_int($values[1]) + || !is_int($values[0]) && is_int($values[1]) + ) { + $values[0] = (int) $values[0]; + $values[1] = (int) $values[1]; + } + + // Let user know what changed. + if (empty($all_titles_descriptions[$key])) { + drupal_set_message(t('%before -> %after for %title', array( + '%title' => $key, + '%before' => var_export($values[1], TRUE), + '%after' => var_export($values[0], TRUE), + ))); + } + else { + drupal_set_message(t('%before -> %after for %title', array( + '%title' => $all_titles_descriptions[$key], + '%before' => var_export($values[1], TRUE), + '%after' => var_export($values[0], TRUE), + ))); + } + } + + return $changed; +} + +/** + * Given a list of items see what ones need to be inserted/updated or deleted. + * + * @param array $defaults + * Array of default values, representing a row in the db. + * @param mixed $new_values + * Array of edited values, representing a row in the db. + * + * @return array + * Nested array strucutre; only the diff. + */ +function advagg_diff_multi(array $defaults, $new_values) { + $result = array(); + + foreach ($defaults as $key => $val) { + if (is_array($val) && isset($new_values[$key])) { + $tmp = advagg_diff_multi($val, $new_values[$key]); + if ($tmp) { + $result[$key] = $tmp; + } + } + elseif (!isset($new_values[$key])) { + $result[$key] = NULL; + } + elseif ($val != $new_values[$key]) { + $result[$key] = $new_values[$key]; + } + if (isset($new_values[$key])) { + unset($new_values[$key]); + } + } + + $result = $result + $new_values; + return $result; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.admin.inc new file mode 100644 index 000000000..d7bfd0316 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.admin.inc @@ -0,0 +1,229 @@ + t('Use HTTP 1.1 settings'), + 2 => t('Use HTTP 2.0 settings'), + 4 => t('Use customized settings'), + ); + $form['advagg_bundler_admin_mode'] = array( + '#type' => 'radios', + '#title' => t('AdvAgg Settings'), + '#default_value' => variable_get('advagg_bundler_admin_mode', ADVAGG_BUNDLER_ADMIN_MODE), + '#options' => $options, + '#description' => t("Default settings will mirror core as closely as possible.
Recommended settings are optimized for speed."), + ); + + // Test http2. + $http2_support = 0; + $url = 'https://tools.keycdn.com/http2-query.php?url=' . url('', array('absolute' => TRUE)); + if (filter_var($_SERVER['HTTP_HOST'], FILTER_VALIDATE_IP) === FALSE && $_SERVER['HTTP_HOST'] !== 'localhost') { + $response = drupal_http_request($url, array( + 'timeout' => 3, + )); + } + else { + $response = new stdClass(); + $response->code = 0; + } + if ($response->code == 200 && !empty($response->data)) { + $http2_support = 1; + if (stripos($response->data, 'success') !== FALSE) { + $http2_support = 2; + } + } + if (stripos($_SERVER['SERVER_SOFTWARE'], 'apache') !== FALSE && is_callable('apache_get_modules')) { + $modules = apache_get_modules(); + $key_a = array_search('mod_http2', $modules); + $key_b = array_search('mod_h2', $modules); + if ($key_a !== FALSE || $key_b !== FALSE) { + $http2_support += 4; + } + } + // Display http2 info. + if ($http2_support & 2) { + $description = t('This server supports HTTP 2.0; the test from @url came back ok.', array('@url' => $url)); + } + else { + if ($http2_support == 0) { + $description = t('AdvAgg can not detect if this server supports HTTP 2.0. You can go here to learn how to enable it.', array('@url' => 'https://httpd.apache.org/docs/2.4/mod/mod_http2.html')); + } + elseif ($http2_support & 4) { + $description = t('It appears that this server could support HTTP 2.0 (apache mod_http2 found)', array( + '@url' => 'https://httpd.apache.org/docs/2.4/mod/mod_http2.html', + )); + if ($http2_support & 1) { + $description .= t(', but it may not be configured to do so. The test from here was inconclusive.', array( + '@test' => $url, + )); + } + else { + if (count($response) == 1) { + $description .= t(', but the test from here was not actually done due to the URL being an IP address or localhost.', array( + '@test' => $url, + )); + } + else { + $description .= t(', but the test from here was inconclusive.', array( + '@test' => $url, + )); + } + } + } + else { + $description = t('This server does not support HTTP 2.0.'); + } + $description .= ' ' . t('This guide can help you get http2 enabled on your site.', array('@url' => 'https://geekflare.com/http2-implementation-apache-nginx/')); + } + if (!empty($description)) { + $form['advagg_http2'] = array( + '#markup' => "

{$description}

", + ); + } + + $form['global_container'] = array( + '#type' => 'container', + '#hidden' => TRUE, + '#states' => array( + 'visible' => array( + ':input[name="advagg_bundler_admin_mode"]' => array('value' => '4'), + ), + ), + ); + + $form['global_container']['advagg_bundler_active'] = array( + '#type' => 'checkbox', + '#title' => t('Bundler is Active (recommended)'), + '#default_value' => variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE), + '#description' => t('If not checked, the bundler will passively monitor your site, but it will not split up aggregates.'), + ); + + $options = array( + 0 => 0, + 1 => 1, + 2 => 2, + 3 => 3, + 4 => 4, + 5 => 5, + 6 => 6, + 7 => 7, + 8 => 8, + 9 => 9, + 10 => 10, + 11 => 11, + 12 => 12, + 13 => 13, + 14 => 14, + 15 => 15, + 16 => 16, + 17 => 17, + 18 => 18, + 19 => 19, + 20 => 20, + 21 => 21, + 22 => 22, + 23 => 23, + 24 => 24, + 25 => 25, + ); + $form['global_container']['advagg_bundler_max_css'] = array( + '#type' => 'select', + '#title' => t('Target Number Of CSS Bundles Per Page'), + '#default_value' => variable_get('advagg_bundler_max_css', ADVAGG_BUNDLER_MAX_CSS), + '#options' => $options, + '#description' => t('If 0 is selected then the bundler is disabled. 2 is recommended for HTTP 1.1 and 25 for HTTP 2.0.'), + '#states' => array( + 'disabled' => array( + '#edit-advagg-bundler-active' => array('checked' => FALSE), + ), + ), + '#recommended_value' => 25, + ); + $form['global_container']['advagg_bundler_max_js'] = array( + '#type' => 'select', + '#title' => t('Target Number Of JS Bundles Per Page'), + '#default_value' => variable_get('advagg_bundler_max_js', ADVAGG_BUNDLER_MAX_JS), + '#options' => $options, + '#description' => t('If 0 is selected then the bundler is disabled. 5 is recommended for HTTP 1.1 and 25 for HTTP 2.0.'), + '#states' => array( + 'disabled' => array( + '#edit-advagg-bundler-active' => array('checked' => FALSE), + ), + ), + '#recommended_value' => 25, + ); + + $form['global_container']['advagg_bundler_grouping_logic'] = array( + '#type' => 'radios', + '#title' => t('Grouping logic'), + '#default_value' => variable_get('advagg_bundler_grouping_logic', ADVAGG_BUNDLER_GROUPING_LOGIC), + '#options' => array( + 0 => t('File count'), + 1 => t('File size'), + ), + '#description' => t('If file count is selected then each bundle will try to have a similar number of original files aggregated inside of it. If file size is selected then each bundle will try to have a similar file size.'), + '#states' => array( + 'disabled' => array( + '#edit-advagg-bundler-active' => array('checked' => FALSE), + ), + ), + '#recommended_value' => 0, + ); + + $form['global_container']['info'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Raw Grouping Info'), + ); + module_load_include('inc', 'advagg', 'advagg.admin'); + $analysis = advagg_bundler_analysis('', TRUE); + + $rawtext = print_r($analysis, TRUE); + $form['global_container']['info']['advagg_bundler_info'] = array( + '#type' => 'textarea', + '#title' => t('%count different groupings', array('%count' => count($analysis))), + '#default_value' => $rawtext, + '#rows' => 30, + ); + + // Clear the cache bins on submit. Also remove advagg_bundler_info so it + // doesn't get added to the variable table. + $form['#submit'][] = 'advagg_bundler_admin_settings_form_submit'; + + return system_settings_form($form); +} + +/** + * Submit callback, clear out the advagg cache bin. + * + * @ingroup advagg_forms_callback + */ +function advagg_bundler_admin_settings_form_submit($form, &$form_state) { + // Clear caches. + advagg_cache_clear_admin_submit(); + + // Unset advagg_bundler_info. + if (isset($form_state['values']['advagg_bundler_info'])) { + unset($form_state['values']['advagg_bundler_info']); + } + + // Reset this form to defaults or recommended values; also show what changed. + advagg_set_admin_form_defaults_recommended($form_state, 'advagg_bundler_admin_mode'); +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.advagg.inc b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.advagg.inc new file mode 100644 index 000000000..1fed98a6e --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.advagg.inc @@ -0,0 +1,286 @@ + $max) { + $remerge_candidate = NULL; + $remerge_key = NULL; + $total_count = 0; + foreach ($final_groupings as $key => $groupings) { + if (count($groupings) > 1) { + if (is_null($remerge_candidate)) { + $remerge_candidate = $groupings; + $remerge_key = $key; + } + elseif (count($remerge_candidate) > count($groupings)) { + $remerge_candidate = $groupings; + $remerge_key = $key; + } + } + } + if (is_null($remerge_candidate)) { + break; + } + advagg_bundler_merge($remerge_candidate, count($remerge_candidate) - 1); + $final_groupings[$remerge_key] = $remerge_candidate; + + $total_count = 0; + foreach ($final_groupings as $key => $groupings) { + $total_count += count($groupings); + } + } + + // Replace $files array with new aggregate filenames. + $files = advagg_generate_filenames($final_groupings, $type); +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Merge bundles together if too many where created. + * + * This preserves the order. + * + * @param array $groupings + * Array of requested groups. + * @param int $max + * Max number of grouping. + */ +function advagg_bundler_merge(array &$groupings, $max) { + $group_count = count($groupings); + + if (!empty($max)) { + // Keep going till array has been merged to the desired size. + while ($group_count > $max) { + // Get array sizes. + // Counts the number of files that are placed into each bundle. + $counts = array(); + $group_hash_counts = array(); + foreach ($groupings as $key => $values) { + // Get the group hash counts. + $file_size = 0; + $group_hash_counts[$key] = 0; + foreach ($values as $data) { + // Skip if bundler data is missing. + if (empty($data['bundler'])) { + continue; + } + $file_size += empty($data['bundler']['filesize_processed']) ? $data['bundler']['filesize'] : $data['bundler']['filesize_processed']; + $group_hash_counts[$key] += intval(substr($data['bundler']['group_hash'], 0, 8)); + } + if (variable_get('advagg_bundler_grouping_logic', ADVAGG_BUNDLER_GROUPING_LOGIC) == 0) { + $counts[$key] = count($values); + } + elseif (variable_get('advagg_bundler_grouping_logic', ADVAGG_BUNDLER_GROUPING_LOGIC) == 1) { + $counts[$key] = $file_size; + } + } + + // Create mapping. + // Calculates the file count of potential merges. It only merges with + // neighbors in order to preserve execution order. + $map = array(); + $prev_key = NULL; + foreach ($counts as $key => $val) { + // First run of the foreach loop; populate prev key/values and continue. + // We can't merge with the previous group in this case. + if (is_null($prev_key)) { + $prev_key = $key; + $prev_val = $val; + continue; + } + + // Array key ($prev_val + $val) is the file count of this new group if + // these 2 groups ($prev_key, $key) where to be merged together. + $map[] = array( + ($prev_val + $val) => array($prev_key, $key), + ); + + // Prep for next run. + $prev_key = $key; + $prev_val = $val; + } + + $group_hash_map = array(); + $prev_key = NULL; + foreach ($group_hash_counts as $key => $val) { + // First run of the foreach loop; populate prev key/values and continue. + // We can't merge with the previous group in this case. + if (is_null($prev_key)) { + $prev_key = $key; + $prev_val = $val; + continue; + } + + // Array value ($prev_val + $val) is the hash count of this new group if + // these 2 groups where to be merged together. + $group_hash_map[$prev_key . ' ' . $key] = $prev_val + $val; + + // Prep for next run. + $prev_key = $key; + $prev_val = $val; + } + + // Get best merge candidate. + // We are looking for the smallest file count. $min is populated with a + // large number (15 bits) so it won't be selected in this process. + $min = PHP_INT_MAX; + $first = NULL; + $last = NULL; + $last_min = NULL; + foreach ($map as $v) { + foreach ($v as $key => $values) { + $min = min($min, $key); + // If the min value (number of files in the proposed merged bundle) is + // the same as the key, then get the 2 bundle keys that generated this + // new min value. + if ($min == $key) { + if ($last_min == $min && !is_null($first) && !is_null($last)) { + list($new_first, $new_last) = $values; + // All things being equal pick the smaller count on the hash. + if ($group_hash_map[$first . ' ' . $last] > $group_hash_map[$new_first . ' ' . $new_last]) { + $first = $new_first; + $last = $new_last; + } + } + else { + list($first, $last) = $values; + } + $last_min = $min; + } + } + } + + // Create the new merged set. + $a = $groupings[$first]; + $b = $groupings[$last]; + $new_set = array_merge($a, $b); + + // Rebuild the array with the new set in the correct place. + $new_groupings = array(); + foreach ($groupings as $key => $files) { + if ($key == $first) { + $new_groupings[$first . ' ' . $last] = $new_set; + } + elseif ($key != $last) { + $new_groupings[$key] = $files; + } + } + $groupings = $new_groupings; + --$group_count; + } + } + + // Error prevention below. + // Make sure there isn't a group that has all files that don't exist or empty. + $bad_groups = array(); + foreach ($groupings as $key => $group) { + $missing_counter = 0; + foreach ($group as $data) { + if (empty($data['bundler']['filesize'])) { + ++$missing_counter; + } + } + + // If all files are missing/empty in this group then it is a bad set. + if ($missing_counter == count($group)) { + $bad_groups[$key] = TRUE; + } + } + + // Add the bad groups to the smallest grouping in this set. + if (!empty($bad_groups)) { + $merge_candidate_key = ''; + $merge_candidate_count = PHP_INT_MAX; + $bad_group = array(); + foreach ($groupings as $key => $group) { + if (isset($bad_groups[$key])) { + // Merge all bad groups into one. + $bad_group = array_merge($bad_group, $group); + + // Delete the bad group from the master set. + unset($groupings[$key]); + continue; + } + + // Find the smallest good grouping. + $min = min($merge_candidate_count, count($group)); + if ($min < $merge_candidate_count) { + $merge_candidate_key = $key; + $merge_candidate_count = $min; + } + } + + // Move the bad files into the smallest good group. + $new_set = isset($groupings[$merge_candidate_key]) ? $groupings[$merge_candidate_key] : array(); + $new_set = array_merge($new_set, $bad_group); + $groupings[$merge_candidate_key] = $new_set; + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.info b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.info new file mode 100644 index 000000000..31477cffe --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.info @@ -0,0 +1,13 @@ +name = AdvAgg Bundler +description = Provides intelligent bundling of CSS and JS files by grouping files that belong together. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg + +configure = admin/config/development/performance/advagg/bundler + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.install b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.install new file mode 100644 index 000000000..0cfaf6e4e --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_bundler/advagg_bundler.install @@ -0,0 +1,37 @@ + 'Bundler', + 'description' => 'Adjust Bundler settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_bundler_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg_bundler.admin.inc', + 'weight' => 10, + ); + + return $items; +} + +/** + * Implements hook_advagg_hooks_implemented_alter(). + */ +function advagg_bundler_advagg_hooks_implemented_alter(&$hooks, $all) { + if ($all) { + $hooks['advagg_bundler_analysis_alter'] = array(); + } +} + +/** + * Implements hook_init(). + */ +function advagg_bundler_init() { + if (advagg_bundler_enabled()) { + $GLOBALS['conf']['advagg_core_groups'] = FALSE; + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function advagg_bundler_form_advagg_admin_settings_form_alter(&$form, $form_state) { + if (advagg_bundler_enabled()) { + $form['global']['advagg_core_groups']['#disabled'] = TRUE; + $form['global']['advagg_core_groups']['#description'] = t('The bundler submodule disables core grouping logic.'); + $form['global']['advagg_core_groups']['#states'] = array(); + } +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * Returns TRUE if the bundler will run. + */ +function advagg_bundler_enabled() { + if (variable_get('advagg_bundler_active', ADVAGG_BUNDLER_ACTIVE) && (variable_get('advagg_bundler_max_css', ADVAGG_BUNDLER_MAX_CSS) || variable_get('advagg_bundler_max_js', ADVAGG_BUNDLER_MAX_JS))) { + return TRUE; + } +} + +/** + * Given a filename return a bundle key. + * + * @param string $filename + * Filename. + * @param bool $force + * Bypass the cache and get a fresh version of the analysis. + * @param bool $safesql + * Turn off SQL language that might cause errors. + * @param int $depth + * Used to prevent endless loops. + * + * @return string + * String to be used for the grouping key. + */ +function advagg_bundler_analysis($filename = '', $force = FALSE, $safesql = FALSE, $depth = 0) { + // Cache query in a static. + static $analysis = array(); + if (empty($analysis)) { + // See if we have a cached version of this. Generate cache ID. + $query = db_select('advagg_aggregates_versions', 'aav') + ->condition('aav.root', 1) + ->condition('aav.atime', REQUEST_TIME - max(172800, variable_get('advagg_bundler_outdated', ADVAGG_BUNDLER_OUTDATED), '>'), '>'); + $query->addExpression('COUNT(aggregate_filenames_hash)', 'counter'); + $count = $query->execute()->fetchField(); + $ideal_cid = 'advagg:bundler_analysis:' . $count; + + if (!$force) { + // Generate cache IDs. + $counts = range(max(0, $count - 3), $count + 3); + foreach ($counts as $count) { + $cache_ids[] = 'advagg:bundler_analysis:' . $count; + } + + // Get a range of cached bundler_analysis data. + $cache_hits = cache_get_multiple($cache_ids, 'cache_advagg_aggregates'); + if (!empty($cache_hits)) { + if (isset($cache_hits[$ideal_cid])) { + $cache = $cache_hits[$ideal_cid]; + } + elseif (!$force && module_exists('httprl') && httprl_is_background_callback_capable()) { + // Setup callback options array. + $callback_options = array( + array( + 'function' => 'advagg_bundler_analysis', + ), + $filename, TRUE, + ); + // Queue up the request. + httprl_queue_background_callback($callback_options); + // Execute request. + httprl_send_request(); + + // Use most recent bundler_analysis data. + $max = 0; + foreach ($cache_hits as $data) { + if ($data->created > $max) { + $max = $data->created; + $cache = $data; + } + } + } + } + } + + if ($force || empty($cache->data)) { + try { + $analysis = advagg_bundler_analyisis_query($safesql); + // Save results to the cache. + cache_set($ideal_cid, $analysis, 'cache_advagg_aggregates', CACHE_TEMPORARY); + } + catch (PDOException $e) { + if ($depth > 2) { + throw $e; + } + $depth++; + return advagg_bundler_analysis($filename, TRUE, TRUE, $depth); + } + } + else { + $analysis = $cache->data; + } + } + + // If no filename is given pass back then entire query results. + if (empty($filename)) { + return $analysis; + } + + // Return a key to be used in groupings. + if (!empty($analysis[$filename])) { + return $analysis[$filename]; + } + + // We need to return a value that can be used as an array key if the query + // didn't give us anything. + return 0; +} + +/** + * Run the analysis query and return the analysis array. + * + * "Magic Query"; only needs to run once. Results are cached. + * This is what the raw SQL looks like: + * + * @code + * SELECT + * af.filename AS filename, + * af.filesize AS filesize, + * af.mtime AS mtime, + * af.changes AS changes, + * af.linecount AS linecount, + * af.filename_hash AS filename_hash, + * aa.counter AS counter, + * aa.hashlist AS hashlist + * FROM advagg_files af + * INNER JOIN ( + * SELECT + * aa.filename_hash AS filename_hash, + * LPAD(CAST(COUNT(aav.aggregate_filenames_hash) AS char(8)), 8, '0') AS counter, + * HEX(SHA1(GROUP_CONCAT(DISTINCT aa.aggregate_filenames_hash ORDER BY aa.aggregate_filenames_hash ASC))) AS hashlist + * FROM advagg_aggregates aa + * INNER JOIN advagg_aggregates_versions aav + * ON aav.aggregate_filenames_hash = aa.aggregate_filenames_hash + * AND aav.root = 1 + * AND aav.atime > (UNIX_TIMESTAMP() - 1209600) + * GROUP BY aa.filename_hash + * ) aa ON af.filename_hash = aa.filename_hash + * @endcode + * + * @param bool $safesql + * Turn off SQL language that might cause errors. + * + * @return array + * The analysis array. + */ +function advagg_bundler_analyisis_query($safesql) { + // Return a count of how many root bundles all files are used in. Count is + // padded with eight zeros so the count can be key sorted as a string + // without worrying about it getting put in the wrong order. + // Return the bundle_md5's value; we need something more unique than count + // when grouping together. + // Return the filename. Used for lookup. + // We join the advagg bundles and files together making sure to only use + // root bundles that have been used in the last 2 weeks. This prevents an + // old site structure from influencing new bundles. + // Grouping by the filename gives us the count and makes it so we don't + // return a lot of rows. + $db_type = Database::getConnection()->databaseType(); + $schema = Database::getConnection()->schema(); + if ($safesql) { + $mssql_group_concat = FALSE; + $mssql_lpad = FALSE; + $mssql_md5 = FALSE; + $pg9 = FALSE; + } + else { + $mssql_group_concat = method_exists($schema, 'functionExists') && $schema->functionExists('GROUP_CONCAT'); + $mssql_lpad = method_exists($schema, 'functionExists') && $schema->functionExists('LPAD'); + $mssql_md5 = method_exists($schema, 'functionExists') && $schema->functionExists('MD5'); + if ($db_type === 'pgsql') { + $database_connection = Database::getConnection(); + $pg9 = FALSE; + if (version_compare($database_connection->version(), '9') >= 0) { + $pg9 = TRUE; + } + } + } + + // Create join query for the advagg_aggregates table. + $subquery_aggregates = db_select('advagg_aggregates', 'aa'); + + // Counter column. + $fields = array('counter'); + if ($db_type === 'sqlsrv' && !$mssql_lpad) { + // MS SQL does not support LPAD. + $subquery_aggregates->addExpression("RIGHT(REPLICATE('0',8) + CAST(COUNT(aav.aggregate_filenames_hash) AS char(8)),8)", 'counter'); + } + elseif ($db_type === 'sqlite') { + // SQLite does not support LPAD. + $subquery_aggregates->addExpression("substr('00000000' || CAST(COUNT(aav.aggregate_filenames_hash) AS char(8)), -8, 8)", 'counter'); + } + else { + $subquery_aggregates->addExpression("LPAD(CAST(COUNT(aav.aggregate_filenames_hash) AS char(8)), 8, '0')", 'counter'); + } + + // Hashlist column. + if ($db_type === 'mysql') { + $fields[] = 'hashlist'; + db_query('SET SESSION group_concat_max_len = 65535'); + $subquery_aggregates->addExpression('HEX(SHA1(GROUP_CONCAT(DISTINCT aa.aggregate_filenames_hash ORDER BY aa.aggregate_filenames_hash ASC)))', 'hashlist'); + } + elseif ($db_type === 'pgsql') { + if ($pg9) { + $fields[] = 'hashlist'; + $subquery_aggregates->addExpression("MD5(STRING_AGG(DISTINCT(aa.aggregate_filenames_hash), ',' ORDER BY aa.aggregate_filenames_hash ASC))", 'hashlist'); + } + } + elseif ($db_type === 'sqlite') { + $fields[] = 'hashlist'; + $subquery_aggregates->addExpression('GROUP_CONCAT(DISTINCT aa.aggregate_filenames_hash)', 'hashlist'); + $subquery_aggregates->orderBy("aa.aggregate_filenames_hash", "ASC"); + } + elseif ($db_type === 'sqlsrv' && $mssql_group_concat) { + $fields[] = 'hashlist'; + if ($mssql_md5) { + $subquery_aggregates->addExpression('MD5(GROUP_CONCAT(DISTINCT aa.aggregate_filenames_hash))', 'hashlist'); + } + else { + $subquery_aggregates->addExpression('GROUP_CONCAT(DISTINCT aa.aggregate_filenames_hash)', 'hashlist'); + } + // The ORDER BY clause is invalid in views, inline functions, + // derived tables, subqueries, and common table expressions, unless TOP or + // FOR XML is also specified. So no point in doing an order-by like in the + // other cases. + } + + // Create join for the advagg_aggregates_versions table. + // 1209600 = 2 weeks. + $time = REQUEST_TIME - max(172800, variable_get('advagg_bundler_outdated', ADVAGG_BUNDLER_OUTDATED), '>'); + $subquery_aggregates->join('advagg_aggregates_versions', 'aav', "aav.aggregate_filenames_hash=aa.aggregate_filenames_hash AND aav.root=1 AND aav.atime > $time"); + + $subquery_aggregates = $subquery_aggregates->fields('aa', array('filename_hash')) + ->groupBy('aa.filename_hash'); + + // Create main query for the advagg_files table. + $af_fields = array( + 'filename', + 'filesize', + 'mtime', + 'changes', + 'linecount', + 'filename_hash', + ); + // Make drupal_get_installed_schema_version() available. + include_once DRUPAL_ROOT . '/includes/install.inc'; + if (drupal_get_installed_schema_version('advagg') >= 7211) { + $af_fields[] = 'filesize_processed'; + } + + $query = db_select('advagg_files', 'af'); + $query->join($subquery_aggregates, 'aa', 'af.filename_hash=aa.filename_hash'); + $query = $query->fields('af', $af_fields) + ->fields('aa', $fields); + $query->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + + $analysis = array(); + foreach ($results as $row) { + // Implement slower GROUP_CONCAT functionality for non mysql databases. + if (empty($row->hashlist)) { + $subquery_aggregates_versions = db_select('advagg_aggregates_versions', 'aav') + ->fields('aav') + ->condition('aav.root', 1) + ->condition('aav.atime', REQUEST_TIME - max(172800, variable_get('advagg_bundler_outdated', ADVAGG_BUNDLER_OUTDATED), '>'), '>'); + + $subquery_aggregates = db_select('advagg_aggregates', 'aa'); + $subquery_aggregates->join($subquery_aggregates_versions, 'aav', 'aav.aggregate_filenames_hash=aa.aggregate_filenames_hash'); + $subquery_aggregates = $subquery_aggregates->fields('aa', array('aggregate_filenames_hash')) + ->condition('aa.filename_hash', $row->filename_hash) + ->groupBy('aa.aggregate_filenames_hash') + ->orderBy('aa.aggregate_filenames_hash', 'ASC'); + $subquery_aggregates->comment('Query called from ' . __FUNCTION__ . '()'); + $aa_results = $subquery_aggregates->execute(); + $aa_rows = array(); + foreach ($aa_results as $aa_row) { + $aa_rows[] = $aa_row->aggregate_filenames_hash; + } + $row->hashlist = implode(',', $aa_rows); + } + + $row->hashlist = drupal_hash_base64($row->hashlist); + $analysis[$row->filename] = array( + 'group_hash' => $row->counter . ' ' . $row->hashlist, + 'mtime' => $row->mtime, + 'filesize' => $row->filesize, + 'filesize_processed' => empty($row->filesize_processed) ? $row->filesize : $row->filesize_processed, + 'linecount' => $row->linecount, + 'changes' => $row->changes, + ); + } + arsort($analysis); + + // Invoke hook_advagg_bundler_analysis_alter() to give installed modules a + // chance to alter the analysis array. + drupal_alter('advagg_bundler_analysis', $analysis); + + return $analysis; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.admin.inc new file mode 100644 index 000000000..c9cae0350 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.admin.inc @@ -0,0 +1,381 @@ + ".form-item-lookup{padding-bottom:0;margin-bottom:0;}", + 'type' => 'inline', + ); + $form['add_item']['theme'] = array( + '#type' => 'select', + '#title' => t('Theme'), + '#options' => array_combine($themes, $themes), + '#default_value' => $default_theme, + '#description' => t('Theme Default: %default, Current Theme: %current', array( + '%default' => $default_theme, + '%current' => $global_theme, + )), + ); + $form['add_item']['user'] = array( + '#type' => 'select', + '#title' => t('User type'), + '#default_value' => 0, + '#options' => array( + 'anonymous' => t('anonymous'), + 'authenticated' => t('authenticated'), + 'all' => t('all'), + ), + ); + $type_options = array( + 0 => t('Disabled'), + 2 => t('URL'), + 8 => t('Node Type'), + ); + $form['add_item']['type'] = array( + '#type' => 'select', + '#title' => t('Type of lookup'), + '#default_value' => 2, + '#options' => $type_options, + ); + + $form['add_item']['lookup'] = array( + '#type' => 'textfield', + '#title' => t('Value to lookup'), + '#maxlength' => 255, + '#states' => array( + 'disabled' => array( + ':input[name="type"]' => array('value' => 0), + ), + ), + ); + $form['add_item']['lookup_container_disabled'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="type"]' => array('value' => 0), + ), + ), + ); + $form['add_item']['lookup_container_disabled']['disabled'] = array( + '#markup' => '
', + ); + $form['add_item']['lookup_container_current_path'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="type"]' => array('value' => 2), + ), + ), + ); + $form['add_item']['lookup_container_current_path']['current_path'] = array( + '#markup' => t('%front is the front page; can use internal URLs like %internal or an alias like %here', array( + '%front' => '', + '%internal' => 'node/2', + '%here' => current_path(), + )), + ); + $form['add_item']['lookup_container_node_type'] = array( + '#type' => 'container', + '#states' => array( + 'visible' => array( + ':input[name="type"]' => array('value' => 8), + ), + ), + ); + $form['add_item']['lookup_container_node_type']['node_type'] = array( + '#markup' => t('Node type is the machine name of the node; list of node types: @node_types', array( + '@current_path' => 'https://api.drupal.org/api/drupal/includes%21path.inc/function/current_path/7.x', + '@request_path' => 'https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/request_path/7.x', + '@request_uri' => 'https://api.drupal.org/api/drupal/includes%21bootstrap.inc/function/request_uri/7.x', + '@node_types' => implode(', ', array_keys(node_type_get_names())), + )), + ); + + $form['add_item']['css'] = array( + '#type' => 'textarea', + '#title' => t('Critical CSS'), + '#description' => t('Can be generated via https://www.sitelocity.com/critical-path-css-generator. If this field is empty this entry will be deleted.', array( + '@url' => 'https://www.sitelocity.com/critical-path-css-generator', + )), + '#default_value' => '', + ); + $form['add_item']['dns'] = array( + '#type' => 'textarea', + '#title' => t('Hostnames to lookup'), + '#description' => t('Hosts that will be connected to.'), + '#default_value' => '', + ); + $form['add_item']['pre'] = array( + '#type' => 'textarea', + '#title' => t('Urls to Preload'), + '#description' => t('Assets for the browser that should be downloaded at a high priority.'), + '#default_value' => '', + ); + + // Lookup saved data. + $query = db_select('advagg_critical_css', 'acc') + ->fields('acc') + ->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + // Put results into array. + $counter = 0; + foreach ($results as $row) { + $counter++; + $row = (array) $row; + + foreach ($form['add_item'] as $key => $values) { + // Fix the states array for type. + if (!empty($values['#states'])) { + foreach ($values['#states'] as $states_key => $states_values) { + $states_value = reset($values['#states'][$states_key]); + $values['#states'][$states_key] = array(":input[name=\"{$counter}_type\"]" => $states_value); + } + } + $form['existing_items'][$counter]["{$counter}_{$key}"] = $values; + if (isset($row[$key])) { + $form['existing_items'][$counter]["{$counter}_{$key}"]['#default_value'] = $row[$key]; + } + } + + // Add in css to move the text hint up. + $form['#attached']['css'][] = array( + 'data' => ".form-item-{$counter}-lookup{padding-bottom:0;margin-bottom:0;}", + 'type' => 'inline', + ); + + // Add fieldset. + $filename = advagg_url_to_filename($row['lookup'], FALSE); + $base = drupal_get_path('theme', $row['theme']) . "/critical-css/{$row['user']}/"; + if ($row['type'] == 2) { + $base .= "urls/$filename"; + } + elseif ($row['type'] == 8) { + $base .= "type/$filename"; + } + else { + $base = ''; + } + $form['existing_items'][$counter] += array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('@type @theme @user @lookup', array( + '@theme' => $row['theme'], + '@type' => $type_options[$row['type']], + '@user' => $row['user'], + '@lookup' => $row['lookup'], + )), + ); + + if (!empty($base)) { + $form['existing_items'][$counter]['#description'] = t('If you wish to store this configuration in a file
Critical CSS: @css', array( + '@css' => "$base.css", + )); + if (!empty($row['dns'])) { + $form['existing_items'][$counter]['#description'] .= t('
Hostnames: @dns', array( + '@dns' => "$base.dns", + )); + } + if (!empty($row['pre'])) { + $form['existing_items'][$counter]['#description'] .= t('
Preload: @pre', array( + '@pre' => "$base.pre", + )); + } + } + } + + // Add top level fieldsets. + $form['add_item'] += array( + '#type' => 'fieldset', + '#title' => t('Add Critical CSS'), + '#collapsible' => TRUE, + '#collapsed' => $results->rowCount(), + ); + if (!empty($form['existing_items'])) { + $form['existing_items'] += array( + '#type' => 'fieldset', + '#title' => t('Edit Critical CSS'), + ); + } + + $form['advagg_critical_css_selector_blacklist'] = array( + '#type' => 'textarea', + '#title' => t('Selector Blacklist'), + '#description' => t('Selectors to exclude. Enter one per line. Useful for things like google ads.'), + '#default_value' => variable_get('advagg_critical_css_selector_blacklist', ''), + ); + + // Clear the cache bins on submit. + $form['#submit'][] = 'advagg_critical_css_admin_settings_form_submit'; + + // Most code below taken from system_settings_form(). + $form['actions']['#type'] = 'actions'; + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => t('Save configuration'), + ); + $form['actions']['disable'] = array( + '#type' => 'submit', + '#value' => t('Disable All From Database'), + '#submit' => array('advagg_critical_css_admin_settings_form_submit_disable'), + ); + if (!empty($_POST) && form_get_errors()) { + drupal_set_message(t('The settings have not been saved because of the errors.'), 'error'); + } + // By default, render the form using theme_system_settings_form(). + if (!isset($form['#theme'])) { + $form['#theme'] = 'system_settings_form'; + } + return $form; +} + +/** + * Submit callback, process the advagg_critical_css form. + * + * Also clear out the advagg cache bin. + * + * @ingroup advagg_forms_callback + */ +function advagg_critical_css_admin_settings_form_submit_disable($form, &$form_state) { + $query = db_select('advagg_critical_css', 'acc') + ->fields('acc') + ->comment('Query called from ' . __FUNCTION__ . '()'); + $results = $query->execute(); + // Put results into array. + $insert_update = array(); + foreach ($results as $row) { + $row = (array) $row; + $new_row = $row; + $new_row['type'] = 0; + $insert_update[] = array( + $row, + $new_row, + ); + } + advagg_critical_css_table_insert_update($insert_update); +} + +/** + * Submit callback, process the advagg_critical_css form. + * + * Also clear out the advagg cache bin. + * + * @ingroup advagg_forms_callback + */ +function advagg_critical_css_admin_settings_form_submit($form, &$form_state) { + // Exclude unnecessary elements. + form_state_values_clean($form_state); + + // Save advagg_critical_css_selector_blacklist. + if (!isset($form_state['values']['advagg_critical_css_selector_blacklist'])) { + $form_state['values']['advagg_critical_css_selector_blacklist'] = ''; + } + $advagg_critical_css_selector_blacklist = variable_get('advagg_critical_css_selector_blacklist', ''); + if ($form_state['values']['advagg_critical_css_selector_blacklist'] !== $advagg_critical_css_selector_blacklist) { + variable_set('advagg_critical_css_selector_blacklist', $form_state['values']['advagg_critical_css_selector_blacklist']); + } + unset($form_state['values']['advagg_critical_css_selector_blacklist']); + + // Rearrange form values into key value pairs. + $items = advagg_critical_css_get_rows_from_form($form_state['values']); + // Get default values. + $default_values = advagg_find_all_recommended_admin_values($form_state['complete form'], '#default_value'); + unset($default_values['form_token']); + $default_items = advagg_critical_css_get_rows_from_form($default_values); + + // Get diff, see what items need to be saved. + $diff = advagg_diff_multi($default_items, $items); + $changed_items = array(); + foreach ($diff as $key => $values) { + $changed_items[$key] = $items[$key]; + } + + // Get items to insert/update and delete. + list($insert_update, $delete) = advagg_critical_css_get_db_operations_arrays($changed_items, $default_items); + advagg_critical_css_table_insert_update($insert_update); + advagg_critical_css_table_delete($delete); + + // Clear caches. + advagg_cache_clear_admin_submit(); + drupal_set_message(t('The configuration options have been saved.')); +} + +/** + * Translate from state values into a nested array strucutre. + * + * @param array $form_state_values + * From state values; from $form_state['values']. + * + * @return array + * Nested array strucutre, each index is a row in the db. + */ +function advagg_critical_css_get_rows_from_form(array $form_state_values) { + $items = array(); + $counter = 0; + foreach ($form_state_values as $key => $values) { + // Get the index from the start of the form name. + $matches = array(); + // 1_type turns into $counter = 1 and $key = type. + preg_match('/^(\d)_(.*)/', $key, $matches); + if (!empty($matches)) { + $counter = $matches[1]; + $key = $matches[2]; + } + $items[$counter][$key] = $values; + } + return $items; +} + +/** + * Given a list of items see what ones need to be inserted/updated or deleted. + * + * @param array $items + * Array of values, representing a row in the db. + * + * @return array + * Nested array strucutre, index 0 is the insert update, 1 is the deleted. + */ +function advagg_critical_css_get_db_operations_arrays(array $items, array $old_items) { + $insert_update = array(); + $delete = array(); + foreach ($items as $key => $values) { + // If the css is empty then this needs to be deleted. + if (empty($values['css'])) { + // Do not delete the new items entry (0); it's not in the db currently. + if (!empty($key)) { + $delete[$key] = $values; + } + } + else { + // Pass along the old key value pairs for db_merge. + if (!empty($old_items[$key])) { + $keys = $old_items[$key] + $values; + } + else { + $keys = $values; + } + $insert_update[$key] = array($keys, $values); + } + } + return array($insert_update, $delete); +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.info b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.info new file mode 100644 index 000000000..a6a78db0a --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.info @@ -0,0 +1,14 @@ +name = AdvAgg Critical CSS +description = Control Critical CSS via UI and/or a 3rd party service. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg +dependencies[] = advagg_mod + +configure = admin/config/development/performance/advagg/critical-css + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.install b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.install new file mode 100644 index 000000000..98c954d6f --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.install @@ -0,0 +1,84 @@ + 'The critical css to inline.', + 'fields' => array( + 'theme' => array( + 'description' => 'The theme name.', + 'type' => 'varchar', + 'length' => 255, + 'default' => '', + 'binary' => TRUE, + ), + 'type' => array( + 'description' => 'Type like url or node.', + 'type' => 'int', + 'not null' => TRUE, + 'default' => 0, + ), + 'user' => array( + 'description' => 'User Type or UID.', + 'type' => 'varchar', + 'length' => 255, + 'default' => '', + 'binary' => TRUE, + ), + 'lookup' => array( + 'description' => 'Value from current_path if url or node type if node.', + 'type' => 'varchar', + 'length' => 255, + 'default' => '', + 'binary' => TRUE, + ), + 'css' => array( + 'description' => 'Critical CSS.', + 'type' => 'blob', + 'size' => 'big', + ), + 'dns' => array( + 'description' => 'Hosts for dns lookedup.', + 'type' => 'blob', + 'size' => 'big', + ), + 'pre' => array( + 'description' => 'URLs for preloading.', + 'type' => 'blob', + 'size' => 'big', + ), + 'settings' => array( + 'description' => 'Extra settings if desired.', + 'type' => 'blob', + 'size' => 'big', + 'translatable' => TRUE, + 'serialize' => TRUE, + ), + ), + 'primary key' => array( + 'lookup', + 'user', + 'type', + 'theme', + ), + ); + + return $schema; +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.module b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.module new file mode 100644 index 000000000..6bd3c5727 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_critical_css/advagg_critical_css.module @@ -0,0 +1,239 @@ + 'Critical CSS', + 'description' => 'Control critical css.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_critical_css_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg_critical_css.admin.inc', + 'weight' => 10, + ); + + return $items; +} + +/** + * Implements hook_module_implements_alter(). + */ +function advagg_critical_css_module_implements_alter(&$implementations, $hook) { + // Move critical_css_advagg_mod_critical_css_file_pre_alter to the bottom. + if ($hook === 'critical_css_advagg_mod_critical_css_file_pre_alter' && array_key_exists('advagg_critical_css', $implementations)) { + $item = $implementations['advagg_critical_css']; + unset($implementations['advagg_critical_css']); + $implementations['advagg_critical_css'] = $item; + } +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @addtogroup advagg_hooks + * @{ + */ + +/** + * Implements hook_advagg_mod_critical_css_file_pre_alter(). + */ +function advagg_critical_css_advagg_mod_critical_css_file_pre_alter(&$filename, &$params, &$inline_strings) { + list($dirs, $front_page, $object) = $params; + + // Build query parameters. + $lookup = array($dirs[6]); + if ($front_page) { + $lookup = array(''); + } + $lookup[] = $dirs[9]; + $lookup[] = $dirs[10]; + if (!empty($object->type)) { + $lookup[] = $object->type; + } + $type = array(2, 8); + $users = array(rtrim($dirs[2], '/\\'), rtrim($dirs[3], '/\\')); + + // Get Results. + $result = advagg_critical_css_table_get($GLOBALS['theme'], $type, $lookup, $users); + + // Put into the inline strings array. + if (!empty($result)) { + // Set string values. + $inline_strings[0] = $result['css']; + $inline_strings[1] = $result['dns']; + $inline_strings[2] = $result['pre']; + // Disable file lookup. + $dirs[0] = ''; + $dirs[1] = ''; + } + + // Repack the $params array. + $params = array($dirs, $front_page, $object); +} + +/** + * Implements hook_advagg_mod_critical_css_file_post_alter(). + */ +function advagg_critical_css_advagg_mod_critical_css_file_post_alter(&$filename, &$params, &$inline_strings) { + if (!empty($inline_strings[0])) { + // Remove given css selectors. + $selectors = variable_get('advagg_critical_css_selector_blacklist', ''); + $selectors_array = array_filter(array_map('trim', explode("\n", $selectors))); + foreach ($selectors_array as $pattern) { + $pattern = preg_quote($pattern, '/'); + $pattern = "/([^}]*{$pattern}[^{]*[^}]*\})/s"; + $inline_strings[0] = preg_replace($pattern, '', $inline_strings[0]); + } + } +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Get the db select return object. + * + * @param string $theme + * Name of the current theme. + * @param array $type + * Array of int types to lookup. + * @param array $lookup + * The lookup value. + * @param array $user + * Array of user string values. + * + * @return SelectQuery + * Return the SelectQuery object after it has been executed. + */ +function advagg_critical_css_table_get($theme, array $type, array $lookup, array $user) { + $output = array(); + try { + $results = db_select('advagg_critical_css', 'acc') + ->fields('acc') + ->condition('theme', $theme) + ->condition('type', $type, 'IN') + ->condition('user', $user, 'IN') + ->condition('lookup', $lookup, 'IN') + ->orderBy('type', 'DESC') + ->execute(); + + // Get first result. + $output = $results->fetchAssoc(); + + // Check for a better match in other results if they exist. + foreach ($results as $values) { + $values = (array) $values; + if ($values['type'] < $output['type']) { + $output = $values; + break; + } + if ($values['type'] = $output['type']) { + if (($values['user'] === 'anonymous' || $values['user'] === 'authenticated') + && $output['user'] === 'all' + ) { + $output = $values; + break; + } + if (is_int($values['user'])) { + $output = $values; + break; + } + } + } + } + catch (PDOException $e) { + // Log the error if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + watchdog('advagg_critical_css', 'Development Mode - Caught PDO Exception: @info', array('@info' => $e)); + } + } + + return $output; +} + +/** + * Insert/Update data in the advagg_critical_css table. + * + * @param array $records + * List of rows needed that need to be changed in the db. + * + * @return array + * Return array of booleans if anything was written to the database. + */ +function advagg_critical_css_table_insert_update(array $records) { + $return = array(); + foreach ($records as $values) { + list($keys, $record) = $values; + if (!isset($record['settings'])) { + $record['settings'] = ''; + } + try { + $return[] = db_merge('advagg_critical_css') + ->key(array( + 'theme' => $keys['theme'], + 'user' => $keys['user'], + 'type' => $keys['type'], + 'lookup' => $keys['lookup'], + )) + ->fields($record) + ->execute(); + } + catch (PDOException $e) { + // Log the error if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + watchdog('advagg_critical_css', 'Development Mode - Caught PDO Exception: @info', array('@info' => $e)); + } + } + } + return $return; +} + +/** + * Delete data in the advagg_critical_css table. + * + * @param array $records + * List of rows needed that need to be removed from the db. + * + * @return array + * Return array of booleans if anything was removed from the database. + */ +function advagg_critical_css_table_delete(array $records) { + $return = array(); + foreach ($records as $record) { + try { + $return[] = db_delete('advagg_critical_css') + ->condition('theme', $record['theme']) + ->condition('user', $record['user']) + ->condition('type', $record['type']) + ->condition('lookup', $record['lookup']) + ->execute(); + } + catch (PDOException $e) { + // Log the error if in development mode. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + watchdog('advagg_critical_css', 'Development Mode - Caught PDO Exception: @info', array('@info' => $e)); + } + } + } + return $return; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.info b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.info new file mode 100644 index 000000000..e6e0319a9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.info @@ -0,0 +1,11 @@ +name = AdvAgg CDN CSS +description = Use a shared CDN for CSS libraries, Google Libraries API currently. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.install b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.install new file mode 100644 index 000000000..213c2599c --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.install @@ -0,0 +1,65 @@ +jquery update settings page and select a CDN instead of using this module.', array( + '@settings' => url('admin/config/development/jquery_update', array( + 'fragment' => 'edit-jquery-update-jquery-cdn', + )), + )); + } + else { + $jquery_description = $t('The jquery update module is already configured to use the external CDN "@cdn".', array('@cdn' => $jquery_cdn)); + } + + $requirements['advagg_css_cdn_jquery_update'] = array( + 'title' => $t('Adv CSS CDN - jquery update'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Duplicate Functionality: Use jquery update instead of the advagg_css_cdn sub module.'), + 'description' => $jquery_description . ' ' . $t('You should go to the modules page and disable the "AdvAgg CDN CSS" module.', array( + '@modules' => url('admin/modules', array( + 'fragment' => 'edit-modules-advanced-cssjs-aggregation', + )), + )), + ); + } + + if (empty($requirements)) { + $requirements['advagg_css_cdn'] = array( + 'title' => $t('Adv CSS CDN'), + 'severity' => REQUIREMENT_OK, + 'value' => $t('OK'), + 'description' => $t('jQuery UI CSS should be coming from a CDN.'), + ); + } + + return $requirements; +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.module b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.module new file mode 100644 index 000000000..bd6cd4283 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_cdn/advagg_css_cdn.module @@ -0,0 +1,195 @@ + $values) { + // Only modify if + // advagg_css_cdn_jquery_ui is enabled, + // name is in the $ui_mapping array. + // and type is file. + if (variable_get('advagg_css_cdn_jquery_ui', ADVAGG_CSS_CDN_JQUERY_UI) + && array_key_exists($name, $ui_mapping) + && $css[$name]['type'] === 'file' + ) { + $css[$name]['data'] = '//ajax.googleapis.com/ajax/libs/jqueryui/' . $jquery_ui_version . '/themes/base/jquery.' . $ui_mapping[$name] . '.css'; + $css[$name]['type'] = 'external'; + + // Fallback does not work do to + // "SecurityError: The operation is insecure.". + } + } +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @addtogroup advagg_hooks + * @{ + */ + +/** + * Implements hook_advagg_css_groups_alter(). + */ +function advagg_css_cdn_advagg_css_groups_alter(&$css_groups, $preprocess_css) { + // Work around a bug with seven_css_alter. + // http://drupal.org/node/1937860 + $theme_keys[] = $GLOBALS['theme']; + if (!empty($GLOBALS['base_theme_info'])) { + foreach ($GLOBALS['base_theme_info'] as $base) { + $theme_keys[] = $base->name; + } + } + $match = FALSE; + foreach ($theme_keys as $name) { + if ($name === 'seven') { + $match = TRUE; + } + } + if (empty($match)) { + return; + } + + $target = FALSE; + $last_group = FALSE; + $last_key = FALSE; + $kill_key = FALSE; + $replaced = FALSE; + foreach ($css_groups as $key => $group) { + if (empty($target)) { + if ($group['type'] === 'external' && $group['preprocess'] && $preprocess_css) { + foreach ($group['items'] as $k => $value) { + if ($value['data'] === 'themes/seven/jquery.ui.theme.css') { + // Type should be file and not external (core bug). + $value['type'] = 'file'; + $target = $value; + unset($css_groups[$key]['items'][$k]); + if (empty($css_groups[$key]['items'])) { + unset($css_groups[$key]); + $kill_key = $key; + } + } + } + } + } + else { + $diff = array_merge(array_diff_assoc($group['browsers'], $target['browsers']), array_diff_assoc($target['browsers'], $group['browsers'])); + if ($group['type'] != $target['type'] + || $group['group'] != $target['group'] + || $group['every_page'] != $target['every_page'] + || $group['media'] != $target['media'] + || $group['media'] != $target['media'] + || $group['preprocess'] != $target['preprocess'] + || !empty($diff) + ) { + if (!empty($last_group)) { + $diff = array_merge(array_diff_assoc($last_group['browsers'], $target['browsers']), array_diff_assoc($target['browsers'], $last_group['browsers'])); + if ($last_group['type'] != $target['type'] + || $last_group['group'] != $target['group'] + || $last_group['every_page'] != $target['every_page'] + || $last_group['media'] != $target['media'] + || $last_group['media'] != $target['media'] + || $last_group['preprocess'] != $target['preprocess'] + || !empty($diff) + ) { + // Insert New. + $css_groups[$kill_key] = array( + 'group' => $target['group'], + 'type' => $target['type'], + 'every_page' => $target['every_page'], + 'media' => $target['media'], + 'preprocess' => $target['preprocess'], + 'browsers' => $target['browsers'], + 'items' => array($target), + ); + $replaced = TRUE; + } + else { + // Insert above. + $css_groups[$last_key]['items'][] = $target; + $replaced = TRUE; + } + } + } + else { + // Insert below. + array_unshift($css_groups[$key]['items'], $target); + $replaced = TRUE; + } + } + $last_group = $group; + $last_key = $key; + if ($replaced) { + break; + } + } + ksort($css_groups); +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Return an array of jquery ui files. + */ +function advagg_css_cdn_get_ui_mapping() { + // Replace jQuery UI's CSS, beginning by defining the mapping. + $ui_mapping = array( + 'misc/ui/jquery.ui.accordion.css' => 'ui.accordion', + 'misc/ui/jquery.ui.autocomplete.css' => 'ui.autocomplete', + 'misc/ui/jquery.ui.button.css' => 'ui.button', + 'misc/ui/jquery.ui.core.css' => 'ui.core', + 'misc/ui/jquery.ui.datepicker.css' => 'ui.datepicker', + 'misc/ui/jquery.ui.dialog.css' => 'ui.dialog', + 'misc/ui/jquery.ui.progressbar.css' => 'ui.progressbar', + 'misc/ui/jquery.ui.resizable.css' => 'ui.resizable', + 'misc/ui/jquery.ui.selectable.css' => 'ui.selectable', + 'misc/ui/jquery.ui.slider.css' => 'ui.slider', + 'misc/ui/jquery.ui.tabs.css' => 'ui.tabs', + 'misc/ui/jquery.ui.theme.css' => 'ui.theme', + ); + return $ui_mapping; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.admin.inc new file mode 100644 index 000000000..66e1a1076 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.admin.inc @@ -0,0 +1,157 @@ + '

' . t('The settings below will not have any effect because AdvAgg is currently in development mode. Once the cache settings have been set to normal or aggressive, CSS minification will take place.', array('@devel' => url($config_path . '/advagg', array('fragment' => 'edit-advagg-cache-level')))) . '

', + ); + } + + // Tell user to update library if a new version is available. + $library_name = 'YUI-CSS-compressor-PHP-port'; + $module_name = 'advagg_css_compress'; + list($description) = advagg_get_version_description($library_name, $module_name); + if (!empty($description)) { + $form['advagg_version_msg'] = array( + '#markup' => "

{$description}

", + ); + } + + list($options, $description) = advagg_css_compress_configuration(); + + $form['advagg_css_compressor'] = array( + '#type' => 'radios', + '#title' => t('File Compression: Select a Compressor'), + '#default_value' => variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR), + '#options' => $options, + '#description' => filter_xss($description), + ); + $inline_options = $options; + unset($inline_options[-1]); + $inline_options[0] = t('Disabled'); + $form['advagg_css_compress_inline'] = array( + '#type' => 'radios', + '#title' => t('Inline Compression: Select a Compressor'), + '#default_value' => variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE), + '#options' => $inline_options, + '#description' => filter_xss($description), + ); + $form['advagg_css_compress_inline_if_not_cacheable'] = array( + '#type' => 'checkbox', + '#title' => t('Inline Compression: Use even if this page is not cacheable'), + '#default_value' => variable_get('advagg_css_compress_inline_if_not_cacheable', ADVAGG_CSS_COMPRESS_INLINE_IF_NOT_CACHEABLE), + '#description' => t('By checking this box, all Inline CSS will be compressed regardless of the state of drupal_page_is_cacheable().', array('@link' => 'http://api.drupal.org/api/drupal/includes!bootstrap.inc/function/drupal_page_is_cacheable/7')), + '#states' => array( + 'disabled' => array( + ':input[name="advagg_css_compress_inline"]' => array('value' => "0"), + ), + ), + ); + + $options[ADVAGG_CSS_COMPRESSOR_FILE_SETTINGS] = t('Default'); + ksort($options); + + $form['per_file_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Per File Settings'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + // Get filename and filename_hash. + $results = db_select('advagg_files', 'af') + ->fields('af', array('filename')) + ->condition('filetype', 'css') + ->orderBy('filename', 'ASC') + ->execute(); + $file_settings = variable_get('advagg_css_compressor_file_settings', array()); + foreach ($results as $row) { + $dir = dirname($row->filename); + if (!isset($form['per_file_settings'][$dir])) { + $form['per_file_settings'][$dir] = array( + '#type' => 'fieldset', + '#title' => check_plain($dir), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + } + $form_api_filename = str_replace(array('/', '.'), array('__', '--'), $row->filename); + $form['per_file_settings'][$dir]['advagg_css_compressor_file_settings_' . $form_api_filename] = array( + '#type' => 'radios', + '#title' => t('%filename: Select a Compressor', array('%filename' => $row->filename)), + '#default_value' => isset($file_settings[$form_api_filename]) ? $file_settings[$form_api_filename] : ADVAGG_CSS_COMPRESSOR_FILE_SETTINGS, + '#options' => $options, + ); + if ($form['per_file_settings'][$dir]['advagg_css_compressor_file_settings_' . $form_api_filename]['#default_value'] != ADVAGG_CSS_COMPRESSOR_FILE_SETTINGS) { + $form['per_file_settings'][$dir]['#collapsed'] = FALSE; + $form['per_file_settings']['#collapsed'] = FALSE; + } + } + + // No css files are found. + if (empty($results)) { + $form['per_file_settings']['#description'] = t('No CSS files have been aggregated. You need to enable aggregation. No css files where found in the advagg_files table.'); + } + + // Clear the cache bins on submit. + $form['#submit'][] = 'advagg_css_compress_admin_settings_form_submit'; + + return system_settings_form($form); +} + +/** + * Submit callback that clears out the advagg cache bin. + * + * Also remove default settings inside of the per_file_settings fieldgroup. + * + * @ingroup advagg_forms_callback + */ +function advagg_css_compress_admin_settings_form_submit($form, &$form_state) { + // Clear caches. + advagg_cache_clear_admin_submit(); + + // Get current defaults. + $file_settings = variable_get('advagg_css_compressor_file_settings', array()); + + // Save per file settings. + $new_settings = array(); + foreach ($form_state['values'] as $key => $value) { + // Skip if not advagg_css_compressor_file_settings. + if (strpos($key, 'advagg_css_compressor_file_settings_') === FALSE) { + continue; + } + // Do not process default settings. + if ($value == ADVAGG_CSS_COMPRESSOR_FILE_SETTINGS) { + unset($form_state['values'][$key]); + continue; + } + $new_settings[substr($key, 36)] = $value; + + // Do not save this field into its own variable. + unset($form_state['values'][$key]); + } + if (!empty($new_settings) || !empty($file_settings)) { + if (empty($new_settings)) { + variable_del('advagg_css_compressor_file_settings'); + } + else { + variable_set('advagg_css_compressor_file_settings', $new_settings); + } + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.advagg.inc b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.advagg.inc new file mode 100644 index 000000000..d152670a4 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.advagg.inc @@ -0,0 +1,110 @@ + $settings) { + if (!empty($aggregate_settings['variables']['advagg_css_compressor_file_settings'])) { + $form_api_filename = str_replace(array('/', '.'), array('__', '--'), $filename); + if (isset($aggregate_settings['variables']['advagg_css_compressor_file_settings'][$form_api_filename])) { + $aggregate_settings['variables']['advagg_css_compressor'] = $aggregate_settings['variables']['advagg_css_compressor_file_settings'][$form_api_filename]; + // If one file can not be compressed then the whole aggregrate can not + // be compressed. + if ($aggregate_settings['variables']['advagg_css_compressor'] == 0) { + break; + } + } + } + } + + // Do nothing if the compressor is disabled. + if (empty($aggregate_settings['variables']['advagg_css_compressor'])) { + return; + } + // Do nothing if the cache settings are set to Development. + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + return; + } + + list(, , , $functions) = advagg_css_compress_configuration(); + + if (isset($functions[$aggregate_settings['variables']['advagg_css_compressor']])) { + $run = $functions[$aggregate_settings['variables']['advagg_css_compressor']]; + if (function_exists($run)) { + $functions[$aggregate_settings['variables']['advagg_css_compressor']]($data); + } + } +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Use the CSSmin library from YUI to compress the CSS. + */ +function advagg_css_compress_yui_cssmin(&$data) { + // Try libraries for YUI. + if (is_callable('libraries_load')) { + libraries_load('YUI-CSS-compressor-PHP-port'); + if (class_exists('tubalmartin\CssMin\Minifier')) { + // The "use" alias requires php 5.3. + // @codingStandardsIgnoreLine + $cssmin = new tubalmartin\CssMin\Minifier(); + } + elseif (class_exists('CSSmin')) { + $cssmin = new CSSmin(); + } + } + if (!isset($cssmin)) { + // Load CSSMin.inc if the CSSmin class variable is not set. + if (!class_exists('CSSmin')) { + include drupal_get_path('module', 'advagg_css_compress') . '/yui/CSSMin.inc'; + } + $cssmin = new CSSmin(); + } + if (!isset($cssmin)) { + return; + } + + // Set line break to 4k of text. + if (method_exists($cssmin, 'setLineBreakPosition')) { + $cssmin->setLineBreakPosition(4096); + } + // Compress the CSS splitting lines after 4k of text. + if (method_exists($cssmin, 'run')) { + $compressed = $cssmin->run($data, 4096); + } + if (!empty($compressed)) { + $data = $compressed; + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.info b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.info new file mode 100644 index 000000000..aba0cc76e --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.info @@ -0,0 +1,14 @@ +name = AdvAgg Compress CSS +description = Compress CSS with a 3rd party compressor, YUI currently. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg +recommends[] = libraries + +configure = admin/config/development/performance/advagg/css-compress + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.install b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.install new file mode 100644 index 000000000..b4daf3f6b --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.install @@ -0,0 +1,128 @@ + $t('AdvAgg CSS Compressor'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('AdvAgg CSS Compression is disabled.'), + 'description' => $t('Go to the advagg css compress settings page and select a compressor, or go to the modules page and disable the "AdvAgg Compress CSS" module.', array( + '@settings' => url($config_path . '/advagg/css-compress'), + '@modules' => url('admin/modules', array( + 'fragment' => 'edit-modules-advanced-cssjs-aggregation', + )), + )), + ); + } + } + + // Check version. + $lib_name = 'YUI-CSS-compressor-PHP-port'; + $module_name = 'advagg_css_compress'; + list($description, $info) = advagg_get_version_description($lib_name, $module_name); + if (!empty($description)) { + $requirements["{$module_name}_{$lib_name}_updates"] = array( + 'title' => $t('@module_name', array('@module_name' => $info['name'])), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The @name library needs to be updated.', array('@name' => $lib_name)), + 'description' => $description, + ); + } + + return $requirements; +} + +/** + * Upgrade AdvAgg CSS Compress versions (6.x-1.x and 7.x-1.x) to 7.x-2.x. + */ +function advagg_css_compress_update_7200(&$sandbox) { + // Bail if old DB Table does not exist. + if (!db_table_exists('cache_advagg_css_compress_inline')) { + return t('Nothing needed to happen in AdvAgg CSS Compress.'); + } + + // Remove all old advagg css compress variables. + db_delete('variable') + ->condition('name', 'advagg_css%compress%', 'LIKE') + ->execute(); + + // Remove old schema. + db_drop_table('cache_advagg_css_compress_inline'); + + return t('Upgraded AdvAgg CSS Compress to 7.x-2.x.'); +} + +/** + * Change variable names so they are prefixed with the modules name. + */ +function advagg_css_compress_update_7201(&$sandbox) { + // Rename advagg_css_inline_compressor to advagg_css_compress_inline. + $old = variable_get('advagg_css_inline_compressor', NULL); + if (!is_null($old)) { + variable_del('advagg_css_inline_compressor'); + } + if ($old !== ADVAGG_CSS_COMPRESS_INLINE) { + variable_set('advagg_css_compress_inline', $old); + } + + // Rename advagg_css_inline_compress_if_not_cacheable to + // advagg_css_compress_inline_if_not_cacheable. + $old = variable_get('advagg_css_inline_compress_if_not_cacheable', NULL); + if (!is_null($old)) { + variable_del('advagg_css_inline_compress_if_not_cacheable'); + } + if ($old !== ADVAGG_CSS_COMPRESS_INLINE_IF_NOT_CACHEABLE) { + variable_set('advagg_css_compress_inline_if_not_cacheable', $old); + } +} + +/** + * Remove unused variables from the variable table. + */ +function advagg_css_compress_update_7202(&$sandbox) { + // Remove all old advagg css compress variables. + db_delete('variable') + ->condition('name', 'advagg_css_compressor_file_settings_%', 'LIKE') + ->execute(); +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.module b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.module new file mode 100644 index 000000000..5f6fbb231 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/advagg_css_compress.module @@ -0,0 +1,231 @@ + 'CSS Compression', + 'description' => 'Adjust CSS Compression settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_css_compress_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg_css_compress.admin.inc', + 'weight' => 10, + ); + + return $items; +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @addtogroup advagg_hooks + * @{ + */ + +/** + * Implements hook_advagg_current_hooks_hash_array_alter(). + */ +function advagg_css_compress_advagg_current_hooks_hash_array_alter(&$aggregate_settings) { + $aggregate_settings['variables']['advagg_css_compressor'] = variable_get('advagg_css_compressor', ADVAGG_CSS_COMPRESSOR); + $aggregate_settings['variables']['advagg_css_compressor_file_settings'] = variable_get('advagg_css_compressor_file_settings', array()); +} + +/** + * Implements hook_advagg_modify_css_pre_render_alter(). + * + * Used to compress inline css. + */ +function advagg_css_compress_advagg_modify_css_pre_render_alter(&$children, &$elements) { + // Get variables. + $compressor = variable_get('advagg_css_compress_inline', ADVAGG_CSS_COMPRESS_INLINE); + + // Do nothing if the compressor is disabled. + if (empty($compressor)) { + return; + } + + // Do nothing if the page is not cacheable and inline compress if not + // cacheable is not checked. + if (!variable_get('advagg_css_compress_inline_if_not_cacheable', ADVAGG_CSS_COMPRESS_INLINE_IF_NOT_CACHEABLE) && !drupal_page_is_cacheable()) { + return; + } + + module_load_include('inc', 'advagg_css_compress', 'advagg_css_compress.advagg'); + if ($compressor == 2) { + // Compress any inline CSS with YUI. + foreach ($children as &$values) { + if (!empty($values['#value'])) { + advagg_css_compress_yui_cssmin($values['#value']); + } + } + unset($values); + } +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * @addtogroup 3rd_party_hooks + * @{ + */ + +/** + * Implements hook_libraries_info(). + */ +function advagg_css_compress_libraries_info() { + $libraries['YUI-CSS-compressor-PHP-port'] = array( + // Only used in administrative UI of Libraries API. + 'name' => 'YUI CSS compressor PHP port', + 'vendor url' => 'https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port', + 'download url' => 'https://github.com/tubalmartin/YUI-CSS-compressor-PHP-port/archive/master.zip', + 'version callback' => 'advagg_css_compress_libraries_get_version', + 'version arguments' => array( + 'file' => 'README.md', + 'pattern' => '/###\s+v([0-9a-zA-Z\.-]+)/', + 'lines' => 1000, + 'cols' => 20, + 'default_version' => '2.4.8', + ), + 'local version' => '2.4.8-p10', + 'remote' => array( + 'callback' => 'advagg_get_github_version_txt', + 'url' => 'https://cdn.jsdelivr.net/gh/tubalmartin/YUI-CSS-compressor-PHP-port@master/README.md', + ), + 'versions' => array( + '2' => array( + 'files' => array( + 'php' => array( + 'cssmin.php', + 'data/hex-to-named-color-map.php', + 'data/named-to-hex-color-map.php', + ), + ), + ), + '3' => array( + 'files' => array( + 'php' => array( + 'src/Minifier.php', + 'src/Utils.php', + 'src/Colors.php', + 'src/data/hex-to-named-color-map.php', + 'src/data/named-to-hex-color-map.php', + ), + ), + ), + '4' => array( + 'files' => array( + 'php' => array( + 'src/Minifier.php', + 'src/Utils.php', + 'src/Colors.php', + ), + ), + ), + ), + ); + + return $libraries; +} + +/** + * @} End of "addtogroup 3rd_party_hooks". + */ + +/** + * Try libraries_get_version(), on failure use the passed in default_version. + * + * @param array $library + * An associative array containing all information about the library. + * @param array $options + * An associative array containing options for the version parser. + * + * @return string + * Version number. + */ +function advagg_css_compress_libraries_get_version(array $library, array $options) { + $return = libraries_get_version($library, $options); + if (empty($return) && !empty($options['default_version'])) { + $file = DRUPAL_ROOT . '/' . $library['library path'] . '/' . $options['file']; + if (is_readable($file)) { + return $options['default_version']; + } + } + return $return; +} + +/** + * Generate the js compress configuration. + * + * @return array + * Array($options, $description, $compressors, $functions). + */ +function advagg_css_compress_configuration() { + $description = ''; + $options = array( + -1 => t('Disable Core'), + 0 => t('Core'), + 2 => t('YUI'), + ); + + $compressors = array(); + $functions = array( + 2 => 'advagg_css_compress_yui_cssmin', + ); + + // Allow for other modules to alter this list. + $options_desc = array($options, $description); + drupal_alter('advagg_css_compress_configuration', $options_desc, $compressors, $functions); + list($options, $description) = $options_desc; + + return array($options, $description, $compressors, $functions); +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/yui/CSSMin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/yui/CSSMin.inc new file mode 100644 index 000000000..88df45130 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_css_compress/yui/CSSMin.inc @@ -0,0 +1,1263 @@ +memoryLimit = 128 * 1048576; // 128MB in bytes + $this->pcreBacktrackLimit = 1000 * 1000; + $this->pcreRecursionLimit = 500 * 1000; + + $this->raisePhpLimits = (bool) $raisePhpLimits; + + $this->numRegex = '(?:\+|-)?\d*\.?\d+' . $this->unitsGroupRegex .'?'; + } + + /** + * Minifies a string of CSS + * @param string $css + * @param int|bool $linebreakPos + * @return string + */ + public function run($css = '', $linebreakPos = false) + { + if (empty($css)) { + return ''; + } + + if ($this->raisePhpLimits) { + $this->doRaisePhpLimits(); + } + + $this->comments = array(); + $this->atRuleBlocks = array(); + $this->preservedTokens = array(); + + // process data urls + $css = $this->processDataUrls($css); + + // process comments + $css = preg_replace_callback('/(?chunkLength} chars aprox. + // Reason: PHP's PCRE functions like preg_replace have a "backtrack limit" + // of 100.000 chars by default (php < 5.3.7) so if we're dealing with really + // long strings and a (sub)pattern matches a number of chars greater than + // the backtrack limit number (i.e. /(.*)/s) PCRE functions may fail silently + // returning NULL and $css would be empty. + $charset = ''; + $charsetRegexp = '/(@charset)( [^;]+;)/i'; + $cssChunks = array(); + $l = strlen($css); + + // if the number of characters is <= {$this->chunkLength}, do not chunk + if ($l <= $this->chunkLength) { + $cssChunks[] = $css; + } else { + // chunk css code securely + for ($startIndex = 0, $i = $this->chunkLength; $i < $l; $i++) { + if ($css[$i - 1] === '}' && $i - $startIndex >= $this->chunkLength) { + $cssChunks[] = $this->strSlice($css, $startIndex, $i); + $startIndex = $i; + // Move forward saving iterations when possible! + if ($startIndex + $this->chunkLength < $l) { + $i += $this->chunkLength; + } + } + } + + // Final chunk + $cssChunks[] = $this->strSlice($css, $startIndex); + } + + // Minify each chunk + for ($i = 0, $n = count($cssChunks); $i < $n; $i++) { + $cssChunks[$i] = $this->minify($cssChunks[$i], $linebreakPos); + // Keep the first @charset at-rule found + if (empty($charset) && preg_match($charsetRegexp, $cssChunks[$i], $matches)) { + $charset = strtolower($matches[1]) . $matches[2]; + } + // Delete all @charset at-rules + $cssChunks[$i] = preg_replace($charsetRegexp, '', $cssChunks[$i]); + } + + // Update the first chunk and push the charset to the top of the file. + $cssChunks[0] = $charset . $cssChunks[0]; + + return trim(implode('', $cssChunks)); + } + + /** + * Sets the approximate number of characters to use when splitting a string in chunks. + * @param int $length + */ + public function set_chunk_length($length) + { + $length = (int) $length; + $this->chunkLength = $length < $this->minChunkLength ? $this->minChunkLength : $length; + } + + /** + * Sets the memory limit for this script + * @param int|string $limit + */ + public function set_memory_limit($limit) + { + $this->memoryLimit = $this->normalizeInt($limit); + } + + /** + * Sets the maximum execution time for this script + * @param int|string $seconds + */ + public function set_max_execution_time($seconds) + { + $this->maxExecutionTime = (int) $seconds; + } + + /** + * Sets the PCRE backtrack limit for this script + * @param int $limit + */ + public function set_pcre_backtrack_limit($limit) + { + $this->pcreBacktrackLimit = (int) $limit; + } + + /** + * Sets the PCRE recursion limit for this script + * @param int $limit + */ + public function set_pcre_recursion_limit($limit) + { + $this->pcreRecursionLimit = (int) $limit; + } + + /** + * Tries to configure PHP to use at least the suggested minimum settings + * @return void + */ + private function doRaisePhpLimits() + { + $phpLimits = array( + 'memory_limit' => $this->memoryLimit, + 'max_execution_time' => $this->maxExecutionTime, + 'pcre.backtrack_limit' => $this->pcreBacktrackLimit, + 'pcre.recursion_limit' => $this->pcreRecursionLimit + ); + + // If current settings are higher respect them. + foreach ($phpLimits as $name => $suggested) { + $current = $this->normalizeInt(ini_get($name)); + + if ($current > $suggested) { + continue; + } + + // memoryLimit exception: allow -1 for "no memory limit". + if ($name === 'memory_limit' && $current === -1) { + continue; + } + + // maxExecutionTime exception: allow 0 for "no memory limit". + if ($name === 'max_execution_time' && $current === 0) { + continue; + } + + ini_set($name, $suggested); + } + } + + /** + * Registers a preserved token + * @param $token + * @return string The token ID string + */ + private function registerPreservedToken($token) + { + $this->preservedTokens[] = $token; + return self::TOKEN . (count($this->preservedTokens) - 1) .'___'; + } + + /** + * Gets the regular expression to match the specified token ID string + * @param $id + * @return string + */ + private function getPreservedTokenPlaceholderRegexById($id) + { + return '/'. self::TOKEN . $id .'___/'; + } + + /** + * Registers a candidate comment token + * @param $comment + * @return string The comment token ID string + */ + private function registerComment($comment) + { + $this->comments[] = $comment; + return '/*'. self::COMMENT . (count($this->comments) - 1) .'___*/'; + } + + /** + * Gets the candidate comment token ID string for the specified comment token ID + * @param $id + * @return string + */ + private function getCommentPlaceholderById($id) + { + return self::COMMENT . $id .'___'; + } + + /** + * Gets the regular expression to match the specified comment token ID string + * @param $id + * @return string + */ + private function getCommentPlaceholderRegexById($id) + { + return '/'. $this->getCommentPlaceholderById($id) .'/'; + } + + /** + * Registers an at rule block token + * @param $block + * @return string The comment token ID string + */ + private function registerAtRuleBlock($block) + { + $this->atRuleBlocks[] = $block; + return self::AT_RULE_BLOCK . (count($this->atRuleBlocks) - 1) .'___'; + } + + /** + * Gets the regular expression to match the specified at rule block token ID string + * @param $id + * @return string + */ + private function getAtRuleBlockPlaceholderRegexById($id) + { + return '/'. self::AT_RULE_BLOCK . $id .'___/'; + } + + /** + * Minifies the given input CSS string + * @param string $css + * @param int|bool $linebreakPos + * @return string + */ + private function minify($css, $linebreakPos) + { + // Restore preserved at rule blocks + for ($i = 0, $max = count($this->atRuleBlocks); $i < $max; $i++) { + $css = preg_replace( + $this->getAtRuleBlockPlaceholderRegexById($i), + $this->escapeReplacementString($this->atRuleBlocks[$i]), + $css, + 1 + ); + } + + // strings are safe, now wrestle the comments + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + $comment = $this->comments[$i]; + $commentPlaceholder = $this->getCommentPlaceholderById($i); + $commentPlaceholderRegex = $this->getCommentPlaceholderRegexById($i); + + // ! in the first position of the comment means preserve + // so push to the preserved tokens keeping the ! + if (preg_match('/^!/', $comment)) { + $preservedTokenPlaceholder = $this->registerPreservedToken($comment); + $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1); + // Preserve new lines for /*! important comments + $css = preg_replace('/\R+\s*(\/\*'. $preservedTokenPlaceholder .')/', self::NL.'$1', $css); + $css = preg_replace('/('. $preservedTokenPlaceholder .'\*\/)\s*\R+/', '$1'.self::NL, $css); + continue; + } + + // \ in the last position looks like hack for Mac/IE5 + // shorten that to /*\*/ and the next one to /**/ + if (preg_match('/\\\\$/', $comment)) { + $preservedTokenPlaceholder = $this->registerPreservedToken('\\'); + $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1); + $i = $i + 1; // attn: advancing the loop + $preservedTokenPlaceholder = $this->registerPreservedToken(''); + $css = preg_replace($this->getCommentPlaceholderRegexById($i), $preservedTokenPlaceholder, $css, 1); + continue; + } + + // keep empty comments after child selectors (IE7 hack) + // e.g. html >/**/ body + if (strlen($comment) === 0) { + $startIndex = $this->indexOf($css, $commentPlaceholder); + if ($startIndex > 2) { + if (substr($css, $startIndex - 3, 1) === '>') { + $preservedTokenPlaceholder = $this->registerPreservedToken(''); + $css = preg_replace($commentPlaceholderRegex, $preservedTokenPlaceholder, $css, 1); + continue; + } + } + } + + // in all other cases kill the comment + $css = preg_replace('/\/\*' . $commentPlaceholder . '\*\//', '', $css, 1); + } + + // Normalize all whitespace strings to single spaces. Easier to work with that way. + $css = preg_replace('/\s+/', ' ', $css); + + // Remove spaces before & after newlines + $css = preg_replace('/\s*'. self::NL .'\s*/', self::NL, $css); + + // Fix IE7 issue on matrix filters which browser accept whitespaces between Matrix parameters + $css = preg_replace_callback( + '/\s*filter:\s*progid:DXImageTransform\.Microsoft\.Matrix\(([^)]+)\)/', + array($this, 'processOldIeSpecificMatrixDefinition'), + $css + ); + + // Shorten & preserve calculations calc(...) since spaces are important + $css = preg_replace_callback('/calc(\(((?:[^()]+|(?1))*)\))/i', array($this, 'processCalc'), $css); + + // Replace positive sign from numbers preceded by : or a white-space before the leading space is removed + // +1.2em to 1.2em, +.8px to .8px, +2% to 2% + $css = preg_replace('/((? -9.0 to -9 + $css = preg_replace('/((?+()\]~=,])/', '$1', $css); + + // Restore spaces for !important + $css = preg_replace('/!important/i', ' !important', $css); + + // bring back the colon + $css = preg_replace('/'. self::CLASSCOLON .'/', ':', $css); + + // retain space for special IE6 cases + $css = preg_replace_callback('/:first-(line|letter)(\{|,)/i', array($this, 'lowercasePseudoFirst'), $css); + + // no space after the end of a preserved comment + $css = preg_replace('/\*\/ /', '*/', $css); + + // lowercase some popular @directives + $css = preg_replace_callback( + '/@(document|font-face|import|(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?keyframes|media|namespace|page|' . + 'supports|viewport)/i', + array($this, 'lowercaseDirectives'), + $css + ); + + // lowercase some more common pseudo-elements + $css = preg_replace_callback( + '/:(active|after|before|checked|disabled|empty|enabled|first-(?:child|of-type)|focus|hover|' . + 'last-(?:child|of-type)|link|only-(?:child|of-type)|root|:selection|target|visited)/i', + array($this, 'lowercasePseudoElements'), + $css + ); + + // lowercase some more common functions + $css = preg_replace_callback( + '/:(lang|not|nth-child|nth-last-child|nth-last-of-type|nth-of-type|(?:-(?:moz|webkit)-)?any)\(/i', + array($this, 'lowercaseCommonFunctions'), + $css + ); + + // lower case some common function that can be values + // NOTE: rgb() isn't useful as we replace with #hex later, as well as and() is already done for us + $css = preg_replace_callback( + '/([:,( ]\s*)(attr|color-stop|from|rgba|to|url|-webkit-gradient|' . + '(?:-(?:atsc|khtml|moz|ms|o|wap|webkit)-)?(?:calc|max|min|(?:repeating-)?(?:linear|radial)-gradient))/iS', + array($this, 'lowercaseCommonFunctionsValues'), + $css + ); + + // Put the space back in some cases, to support stuff like + // @media screen and (-webkit-min-device-pixel-ratio:0){ + $css = preg_replace_callback('/(\s|\)\s)(and|not|or)\(/i', array($this, 'processAtRulesOperators'), $css); + + // Remove the spaces after the things that should not have spaces after them. + $css = preg_replace('/([!{}:;>+(\[~=,])\s+/S', '$1', $css); + + // remove unnecessary semicolons + $css = preg_replace('/;+\}/', '}', $css); + + // Fix for issue: #2528146 + // Restore semicolon if the last property is prefixed with a `*` (lte IE7 hack) + // to avoid issues on Symbian S60 3.x browsers. + $css = preg_replace('/(\*[a-z0-9\-]+\s*:[^;}]+)(\})/', '$1;$2', $css); + + // Shorten zero values for safe properties only + $css = $this->shortenZeroValues($css); + + // Shorten font-weight values + $css = preg_replace('/(font-weight:)bold\b/i', '${1}700', $css); + $css = preg_replace('/(font-weight:)normal\b/i', '${1}400', $css); + + // Shorten suitable shorthand properties with repeated non-zero values + $css = preg_replace( + '/(margin|padding):('.$this->numRegex.') ('.$this->numRegex.') (?:\2) (?:\3)(;|\}| !)/i', + '$1:$2 $3$4', + $css + ); + $css = preg_replace( + '/(margin|padding):('.$this->numRegex.') ('.$this->numRegex.') ('.$this->numRegex.') (?:\3)(;|\}| !)/i', + '$1:$2 $3 $4$5', + $css + ); + + // Shorten colors from rgb(51,102,153) to #336699, rgb(100%,0%,0%) to #ff0000 (sRGB color space) + // Shorten colors from hsl(0, 100%, 50%) to #ff0000 (sRGB color space) + // This makes it more likely that it'll get further compressed in the next step. + $css = preg_replace_callback('/rgb\s*\(\s*([0-9,\s\-.%]+)\s*\)(.{1})/i', array($this, 'rgbToHex'), $css); + $css = preg_replace_callback('/hsl\s*\(\s*([0-9,\s\-.%]+)\s*\)(.{1})/i', array($this, 'hslToHex'), $css); + + // Shorten colors from #AABBCC to #ABC or shorter color name. + $css = $this->shortenHexColors($css); + + // Shorten long named colors: white -> #fff. + $css = $this->shortenNamedColors($css); + + // shorter opacity IE filter + $css = preg_replace('/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/i', 'alpha(opacity=', $css); + + // Find a fraction that is used for Opera's -o-device-pixel-ratio query + // Add token to add the "\" back in later + $css = preg_replace('/\(([a-z\-]+):([0-9]+)\/([0-9]+)\)/i', '($1:$2'. self::QUERY_FRACTION .'$3)', $css); + + // Patch new lines to avoid being removed when followed by empty rules cases + $css = preg_replace('/'. self::NL .'/', self::NL .'}', $css); + + // Remove empty rules. + $css = preg_replace('/[^{};\/]+\{\}/S', '', $css); + + // Restore new lines for /*! important comments + $css = preg_replace('/'. self::NL .'}/', "\n", $css); + + // Add "/" back to fix Opera -o-device-pixel-ratio query + $css = preg_replace('/'. self::QUERY_FRACTION .'/', '/', $css); + + // Replace multiple semi-colons in a row by a single one + // See SF bug #1980989 + $css = preg_replace('/;;+/', ';', $css); + + // Lowercase all uppercase properties + $css = preg_replace_callback('/(\{|;)([A-Z\-]+)(:)/', array($this, 'lowercaseProperties'), $css); + + // Some source control tools don't like it when files containing lines longer + // than, say 8000 characters, are checked in. The linebreak option is used in + // that case to split long lines after a specific column. + if ($linebreakPos !== false && (int) $linebreakPos >= 0) { + $linebreakPos = (int) $linebreakPos; + for ($startIndex = $i = 1, $l = strlen($css); $i < $l; $i++) { + if ($css[$i - 1] === '}' && $i - $startIndex > $linebreakPos) { + $css = $this->strSlice($css, 0, $i) . "\n" . $this->strSlice($css, $i); + $l = strlen($css); + $startIndex = $i; + } + } + } + + // restore preserved comments and strings in reverse order + for ($i = count($this->preservedTokens) - 1; $i >= 0; $i--) { + $css = preg_replace( + $this->getPreservedTokenPlaceholderRegexById($i), + $this->escapeReplacementString($this->preservedTokens[$i]), + $css, + 1 + ); + } + + // Trim the final string for any leading or trailing white space but respect newlines! + $css = preg_replace('/(^ | $)/', '', $css); + + return $css; + } + + /** + * Searches & replaces all data urls with tokens before we start compressing, + * to avoid performance issues running some of the subsequent regexes against large string chunks. + * @param string $css + * @return string + */ + private function processDataUrls($css) + { + // Leave data urls alone to increase parse performance. + $maxIndex = strlen($css) - 1; + $appenIndex = $index = $lastIndex = $offset = 0; + $sb = array(); + $pattern = '/url\(\s*(["\']?)data:/i'; + + // Since we need to account for non-base64 data urls, we need to handle + // ' and ) being part of the data string. Hence switching to indexOf, + // to determine whether or not we have matching string terminators and + // handling sb appends directly, instead of using matcher.append* methods. + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->indexOf($css, $m[0], $offset); + $lastIndex = $index + strlen($m[0]); + $startIndex = $index + 4; // "url(".length() + $endIndex = $lastIndex - 1; + $terminator = $m[1]; // ', " or empty (not quoted) + $terminatorFound = false; + + if (strlen($terminator) === 0) { + $terminator = ')'; + } + + while ($terminatorFound === false && $endIndex+1 <= $maxIndex) { + $endIndex = $this->indexOf($css, $terminator, $endIndex + 1); + // endIndex == 0 doesn't really apply here + if ($endIndex > 0 && substr($css, $endIndex - 1, 1) !== '\\') { + $terminatorFound = true; + if (')' !== $terminator) { + $endIndex = $this->indexOf($css, ')', $endIndex); + } + } + } + + // Enough searching, start moving stuff over to the buffer + $sb[] = $this->strSlice($css, $appenIndex, $index); + + if ($terminatorFound) { + $token = $this->strSlice($css, $startIndex, $endIndex); + // Remove all spaces only for base64 encoded URLs. + $token = preg_replace_callback( + '/.+base64,.+/s', + array($this, 'removeSpacesFromDataUrls'), + trim($token) + ); + $preservedTokenPlaceholder = $this->registerPreservedToken($token); + $sb[] = 'url('. $preservedTokenPlaceholder .')'; + $appenIndex = $endIndex + 1; + } else { + // No end terminator found, re-add the whole match. Should we throw/warn here? + $sb[] = $this->strSlice($css, $index, $lastIndex); + $appenIndex = $lastIndex; + } + + $offset = $lastIndex; + } + + $sb[] = $this->strSlice($css, $appenIndex); + + return implode('', $sb); + } + + /** + * Shortens all zero values for a set of safe properties + * e.g. padding: 0px 1px; -> padding:0 1px + * e.g. padding: 0px 0rem 0em 0.0pc; -> padding:0 + * @param string $css + * @return string + */ + private function shortenZeroValues($css) + { + $unitsGroupReg = $this->unitsGroupRegex; + $numOrPosReg = '('. $this->numRegex .'|top|left|bottom|right|center)'; + $oneZeroSafeProperties = array( + '(?:line-)?height', + '(?:(?:min|max)-)?width', + 'top', + 'left', + 'background-position', + 'bottom', + 'right', + 'border(?:-(?:top|left|bottom|right))?(?:-width)?', + 'border-(?:(?:top|bottom)-(?:left|right)-)?radius', + 'column-(?:gap|width)', + 'margin(?:-(?:top|left|bottom|right))?', + 'outline-width', + 'padding(?:-(?:top|left|bottom|right))?' + ); + $nZeroSafeProperties = array( + 'margin', + 'padding', + 'background-position' + ); + + $regStart = '/(;|\{)'; + $regEnd = '/i'; + + // First zero regex start + $oneZeroRegStart = $regStart .'('. implode('|', $oneZeroSafeProperties) .'):'; + + // Multiple zeros regex start + $nZerosRegStart = $regStart .'('. implode('|', $nZeroSafeProperties) .'):'; + + $css = preg_replace( + array( + $oneZeroRegStart .'0'. $unitsGroupReg . $regEnd, + $nZerosRegStart . $numOrPosReg .' 0'. $unitsGroupReg . $regEnd, + $nZerosRegStart . $numOrPosReg .' '. $numOrPosReg .' 0'. $unitsGroupReg . $regEnd, + $nZerosRegStart . $numOrPosReg .' '. $numOrPosReg .' '. $numOrPosReg .' 0'. $unitsGroupReg . $regEnd + ), + array( + '$1$2:0', + '$1$2:$3 0', + '$1$2:$3 $4 0', + '$1$2:$3 $4 $5 0' + ), + $css + ); + + // Remove background-position + array_pop($nZeroSafeProperties); + + // Replace 0 0; or 0 0 0; or 0 0 0 0; with 0 for safe properties only. + $css = preg_replace( + '/('. implode('|', $nZeroSafeProperties) .'):0(?: 0){1,3}(;|\}| !)'. $regEnd, + '$1:0$2', + $css + ); + + // Replace 0 0 0; or 0 0 0 0; with 0 0 for background-position property. + $css = preg_replace('/(background-position):0(?: 0){2,3}(;|\}| !)'. $regEnd, '$1:0 0$2', $css); + + return $css; + } + + /** + * Shortens all named colors with a shorter HEX counterpart for a set of safe properties + * e.g. white -> #fff + * @param string $css + * @return string + */ + private function shortenNamedColors($css) + { + $patterns = array(); + $replacements = array(); + $longNamedColors = array( + 'aliceblue' => '#f0f8ff', + 'antiquewhite' => '#faebd7', + 'aquamarine' => '#7fffd4', + 'black' => '#000', + 'blanchedalmond' => '#ffebcd', + 'blueviolet' => '#8a2be2', + 'burlywood' => '#deb887', + 'cadetblue' => '#5f9ea0', + 'chartreuse' => '#7fff00', + 'chocolate' => '#d2691e', + 'cornflowerblue' => '#6495ed', + 'cornsilk' => '#fff8dc', + 'darkblue' => '#00008b', + 'darkcyan' => '#008b8b', + 'darkgoldenrod' => '#b8860b', + 'darkgray' => '#a9a9a9', + 'darkgreen' => '#006400', + 'darkgrey' => '#a9a9a9', + 'darkkhaki' => '#bdb76b', + 'darkmagenta' => '#8b008b', + 'darkolivegreen' => '#556b2f', + 'darkorange' => '#ff8c00', + 'darkorchid' => '#9932cc', + 'darksalmon' => '#e9967a', + 'darkseagreen' => '#8fbc8f', + 'darkslateblue' => '#483d8b', + 'darkslategray' => '#2f4f4f', + 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', + 'darkviolet' => '#9400d3', + 'deeppink' => '#ff1493', + 'deepskyblue' => '#00bfff', + 'dodgerblue' => '#1e90ff', + 'firebrick' => '#b22222', + 'floralwhite' => '#fffaf0', + 'forestgreen' => '#228b22', + 'fuchsia' => '#f0f', + 'gainsboro' => '#dcdcdc', + 'ghostwhite' => '#f8f8ff', + 'goldenrod' => '#daa520', + 'greenyellow' => '#adff2f', + 'honeydew' => '#f0fff0', + 'indianred' => '#cd5c5c', + 'lavender' => '#e6e6fa', + 'lavenderblush' => '#fff0f5', + 'lawngreen' => '#7cfc00', + 'lemonchiffon' => '#fffacd', + 'lightblue' => '#add8e6', + 'lightcoral' => '#f08080', + 'lightcyan' => '#e0ffff', + 'lightgoldenrodyellow' => '#fafad2', + 'lightgray' => '#d3d3d3', + 'lightgreen' => '#90ee90', + 'lightgrey' => '#d3d3d3', + 'lightpink' => '#ffb6c1', + 'lightsalmon' => '#ffa07a', + 'lightseagreen' => '#20b2aa', + 'lightskyblue' => '#87cefa', + 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#b0c4de', + 'lightyellow' => '#ffffe0', + 'limegreen' => '#32cd32', + 'mediumaquamarine' => '#66cdaa', + 'mediumblue' => '#0000cd', + 'mediumorchid' => '#ba55d3', + 'mediumpurple' => '#9370db', + 'mediumseagreen' => '#3cb371', + 'mediumslateblue' => '#7b68ee', + 'mediumspringgreen' => '#00fa9a', + 'mediumturquoise' => '#48d1cc', + 'mediumvioletred' => '#c71585', + 'midnightblue' => '#191970', + 'mintcream' => '#f5fffa', + 'mistyrose' => '#ffe4e1', + 'moccasin' => '#ffe4b5', + 'navajowhite' => '#ffdead', + 'olivedrab' => '#6b8e23', + 'orangered' => '#ff4500', + 'palegoldenrod' => '#eee8aa', + 'palegreen' => '#98fb98', + 'paleturquoise' => '#afeeee', + 'palevioletred' => '#db7093', + 'papayawhip' => '#ffefd5', + 'peachpuff' => '#ffdab9', + 'powderblue' => '#b0e0e6', + 'rebeccapurple' => '#663399', + 'rosybrown' => '#bc8f8f', + 'royalblue' => '#4169e1', + 'saddlebrown' => '#8b4513', + 'sandybrown' => '#f4a460', + 'seagreen' => '#2e8b57', + 'seashell' => '#fff5ee', + 'slateblue' => '#6a5acd', + 'slategray' => '#708090', + 'slategrey' => '#708090', + 'springgreen' => '#00ff7f', + 'steelblue' => '#4682b4', + 'turquoise' => '#40e0d0', + 'white' => '#fff', + 'whitesmoke' => '#f5f5f5', + 'yellow' => '#ff0', + 'yellowgreen' => '#9acd32' + ); + $propertiesWithColors = array( + 'color', + 'background(?:-color)?', + 'border(?:-(?:top|right|bottom|left|color)(?:-color)?)?', + 'outline(?:-color)?', + '(?:text|box)-shadow' + ); + + $regStart = '/(;|\{)('. implode('|', $propertiesWithColors) .'):([^;}]*)\b'; + $regEnd = '\b/iS'; + + foreach ($longNamedColors as $colorName => $colorCode) { + $patterns[] = $regStart . $colorName . $regEnd; + $replacements[] = '$1$2:$3'. $colorCode; + } + + // Run at least 4 times to cover most cases (same color used several times for the same property) + for ($i = 0; $i < 4; $i++) { + $css = preg_replace($patterns, $replacements, $css); + } + + return $css; + } + + /** + * Compresses HEX color values of the form #AABBCC to #ABC or short color name. + * + * DOES NOT compress CSS ID selectors which match the above pattern (which would break things). + * e.g. #AddressForm { ... } + * + * DOES NOT compress IE filters, which have hex color values (which would break things). + * e.g. filter: chroma(color="#FFFFFF"); + * + * DOES NOT compress invalid hex values. + * e.g. background-color: #aabbccdd + * + * @param string $css + * @return string + */ + private function shortenHexColors($css) + { + // Look for hex colors inside { ... } (to avoid IDs) and + // which don't have a =, or a " in front of them (to avoid filters) + $pattern = + '/(=\s*?["\']?)?#([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])(\}|[^0-9a-f{][^{]*?\})/iS'; + $_index = $index = $lastIndex = $offset = 0; + $longHexColors = array( + '#f0ffff' => 'azure', + '#f5f5dc' => 'beige', + '#ffe4c4' => 'bisque', + '#a52a2a' => 'brown', + '#ff7f50' => 'coral', + '#ffd700' => 'gold', + '#808080' => 'gray', + '#008000' => 'green', + '#4b0082' => 'indigo', + '#fffff0' => 'ivory', + '#f0e68c' => 'khaki', + '#faf0e6' => 'linen', + '#800000' => 'maroon', + '#000080' => 'navy', + '#fdf5e6' => 'oldlace', + '#808000' => 'olive', + '#ffa500' => 'orange', + '#da70d6' => 'orchid', + '#cd853f' => 'peru', + '#ffc0cb' => 'pink', + '#dda0dd' => 'plum', + '#800080' => 'purple', + '#f00' => 'red', + '#fa8072' => 'salmon', + '#a0522d' => 'sienna', + '#c0c0c0' => 'silver', + '#fffafa' => 'snow', + '#d2b48c' => 'tan', + '#008080' => 'teal', + '#ff6347' => 'tomato', + '#ee82ee' => 'violet', + '#f5deb3' => 'wheat' + ); + $sb = array(); + + while (preg_match($pattern, $css, $m, 0, $offset)) { + $index = $this->indexOf($css, $m[0], $offset); + $lastIndex = $index + strlen($m[0]); + $isFilter = $m[1] !== null && $m[1] !== ''; + + $sb[] = $this->strSlice($css, $_index, $index); + + if ($isFilter) { + // Restore, maintain case, otherwise filter will break + $sb[] = $m[1] .'#'. $m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]; + } else { + if (strtolower($m[2]) == strtolower($m[3]) && + strtolower($m[4]) == strtolower($m[5]) && + strtolower($m[6]) == strtolower($m[7])) { + // Compress. + $hex = '#'. strtolower($m[3] . $m[5] . $m[7]); + } else { + // Non compressible color, restore but lower case. + $hex = '#'. strtolower($m[2] . $m[3] . $m[4] . $m[5] . $m[6] . $m[7]); + } + // replace Hex colors with shorter color names + $sb[] = array_key_exists($hex, $longHexColors) ? $longHexColors[$hex] : $hex; + } + + $_index = $offset = $lastIndex - strlen($m[8]); + } + + $sb[] = $this->strSlice($css, $_index); + + return implode('', $sb); + } + + // --------------------------------------------------------------------------------------------- + // CALLBACKS + // --------------------------------------------------------------------------------------------- + + private function processComments($matches) + { + $match = !empty($matches[1]) ? $matches[1] : ''; + return $this->registerComment($match); + } + + private function processStrings($matches) + { + $match = $matches[0]; + $quote = substr($match, 0, 1); + $match = $this->strSlice($match, 1, -1); + + // maybe the string contains a comment-like substring? + // one, maybe more? put'em back then + if (($pos = strpos($match, self::COMMENT)) !== false) { + for ($i = 0, $max = count($this->comments); $i < $max; $i++) { + $match = preg_replace( + $this->getCommentPlaceholderRegexById($i), + $this->escapeReplacementString($this->comments[$i]), + $match, + 1 + ); + } + } + + // minify alpha opacity in filter strings + $match = preg_replace('/progid:DXImageTransform\.Microsoft\.Alpha\(Opacity=/i', 'alpha(opacity=', $match); + + $preservedTokenPlaceholder = $this->registerPreservedToken($match); + return $quote . $preservedTokenPlaceholder . $quote; + } + + private function processAtRuleBlocks($matches) + { + return $this->registerAtRuleBlock($matches[0]); + } + + private function processCalc($matches) + { + $token = preg_replace( + '/\)([+\-]{1})/', + ') $1', + preg_replace( + '/([+\-]{1})\(/', + '$1 (', + trim(preg_replace('/\s*([*\/(),])\s*/', '$1', $matches[2])) + ) + ); + $preservedTokenPlaceholder = $this->registerPreservedToken($token); + return 'calc('. $preservedTokenPlaceholder .')'; + } + + private function processOldIeSpecificMatrixDefinition($matches) + { + $preservedTokenPlaceholder = $this->registerPreservedToken($matches[1]); + return 'filter:progid:DXImageTransform.Microsoft.Matrix('. $preservedTokenPlaceholder .')'; + } + + private function processColon($matches) + { + return preg_replace('/\:/', self::CLASSCOLON, $matches[0]); + } + + private function removeSpacesFromDataUrls($matches) + { + return preg_replace('/\s+/', '', $matches[0]); + } + + private function rgbToHex($matches) + { + $hexColors = array(); + $rgbColors = explode(',', $matches[1]); + + // Values outside the sRGB color space should be clipped (0-255) + for ($i = 0, $l = count($rgbColors); $i < $l; $i++) { + $hexColors[$i] = sprintf("%02x", $this->clampNumberSrgb($this->rgbPercentageToRgbInteger($rgbColors[$i]))); + } + + // Fix for issue #2528093 + if (!preg_match('/[\s,);}]/', $matches[2])) { + $matches[2] = ' '. $matches[2]; + } + + return '#'. implode('', $hexColors) . $matches[2]; + } + + private function hslToHex($matches) + { + $hslValues = explode(',', $matches[1]); + + $rgbColors = $this->hslToRgb($hslValues); + + return $this->rgbToHex(array('', implode(',', $rgbColors), $matches[2])); + } + + private function processAtRulesOperators($matches) + { + return $matches[1] . strtolower($matches[2]) .' ('; + } + + private function lowercasePseudoFirst($matches) + { + return ':first-'. strtolower($matches[1]) .' '. $matches[2]; + } + + private function lowercaseDirectives($matches) + { + return '@'. strtolower($matches[1]); + } + + private function lowercasePseudoElements($matches) + { + return ':'. strtolower($matches[1]); + } + + private function lowercaseCommonFunctions($matches) + { + return ':'. strtolower($matches[1]) .'('; + } + + private function lowercaseCommonFunctionsValues($matches) + { + return $matches[1] . strtolower($matches[2]); + } + + private function lowercaseProperties($matches) + { + return $matches[1] . strtolower($matches[2]) . $matches[3]; + } + + // --------------------------------------------------------------------------------------------- + // HELPERS + // --------------------------------------------------------------------------------------------- + + /** + * Clamps a number between a minimum and a maximum value. + * @param int|float $n the number to clamp + * @param int|float $min the lower end number allowed + * @param int|float $max the higher end number allowed + * @return int|float + */ + private function clampNumber($n, $min, $max) + { + return min(max($n, $min), $max); + } + + /** + * Clamps a RGB color number outside the sRGB color space + * @param int|float $n the number to clamp + * @return int|float + */ + private function clampNumberSrgb($n) + { + return $this->clampNumber($n, 0, 255); + } + + /** + * Escapes backreferences such as \1 and $1 in a regular expression replacement string + * @param $string + * @return string + */ + private function escapeReplacementString($string) + { + return addcslashes($string, '\\$'); + } + + /** + * Converts a HSL color into a RGB color + * @param array $hslValues + * @return array + */ + private function hslToRgb($hslValues) + { + $h = floatval($hslValues[0]); + $s = floatval(str_replace('%', '', $hslValues[1])); + $l = floatval(str_replace('%', '', $hslValues[2])); + + // Wrap and clamp, then fraction! + $h = ((($h % 360) + 360) % 360) / 360; + $s = $this->clampNumber($s, 0, 100) / 100; + $l = $this->clampNumber($l, 0, 100) / 100; + + if ($s == 0) { + $r = $g = $b = $this->roundNumber(255 * $l); + } else { + $v2 = $l < 0.5 ? $l * (1 + $s) : ($l + $s) - ($s * $l); + $v1 = (2 * $l) - $v2; + $r = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h + (1/3))); + $g = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h)); + $b = $this->roundNumber(255 * $this->hueToRgb($v1, $v2, $h - (1/3))); + } + + return array($r, $g, $b); + } + + /** + * Tests and selects the correct formula for each RGB color channel + * @param $v1 + * @param $v2 + * @param $vh + * @return mixed + */ + private function hueToRgb($v1, $v2, $vh) + { + $vh = $vh < 0 ? $vh + 1 : ($vh > 1 ? $vh - 1 : $vh); + + if ($vh * 6 < 1) { + return $v1 + ($v2 - $v1) * 6 * $vh; + } + + if ($vh * 2 < 1) { + return $v2; + } + + if ($vh * 3 < 2) { + return $v1 + ($v2 - $v1) * ((2 / 3) - $vh) * 6; + } + + return $v1; + } + + /** + * PHP port of Javascript's "indexOf" function for strings only + * Author: Tubal Martin + * + * @param string $haystack + * @param string $needle + * @param int $offset index (optional) + * @return int + */ + private function indexOf($haystack, $needle, $offset = 0) + { + $index = strpos($haystack, $needle, $offset); + + return ($index !== false) ? $index : -1; + } + + /** + * Convert strings like "64M" or "30" to int values + * @param mixed $size + * @return int + */ + private function normalizeInt($size) + { + if (is_string($size)) { + $letter = substr($size, -1); + $size = intval($size); + switch ($letter) { + case 'M': + case 'm': + return (int) $size * 1048576; + case 'K': + case 'k': + return (int) $size * 1024; + case 'G': + case 'g': + return (int) $size * 1073741824; + } + } + return (int) $size; + } + + /** + * Converts a string containing and RGB percentage value into a RGB integer value i.e. '90%' -> 229.5 + * @param $rgbPercentage + * @return int + */ + private function rgbPercentageToRgbInteger($rgbPercentage) + { + if (strpos($rgbPercentage, '%') !== false) { + $rgbPercentage = $this->roundNumber(floatval(str_replace('%', '', $rgbPercentage)) * 2.55); + } + + return intval($rgbPercentage, 10); + } + + /** + * Rounds a number to its closest integer + * @param $n + * @return int + */ + private function roundNumber($n) + { + return intval(round(floatval($n)), 10); + } + + /** + * PHP port of Javascript's "slice" function for strings only + * Author: Tubal Martin + * + * @param string $str + * @param int $start index + * @param int|bool $end index (optional) + * @return string + */ + private function strSlice($str, $start = 0, $end = false) + { + if ($end !== false && ($start < 0 || $end <= 0)) { + $max = strlen($str); + + if ($start < 0) { + if (($start = $max + $start) < 0) { + return ''; + } + } + + if ($end < 0) { + if (($end = $max + $end) < 0) { + return ''; + } + } + + if ($end <= $start) { + return ''; + } + } + + $slice = ($end === false) ? substr($str, $start) : substr($str, $start, $end - $start); + return ($slice === false) ? '' : $slice; + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.admin.inc new file mode 100644 index 000000000..ec9457a1c --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.admin.inc @@ -0,0 +1,83 @@ + 'fieldset', + '#title' => t('@title', array('@title' => $params[1])), + ); + $form[$params[0]]['cmd'] = array( + '#type' => 'fieldset', + '#title' => t('Command Line'), + ); + + $description = t('{%CWD%} = DRUPAL_ROOT.
{%IN%} = input file.
{%IN_URL_ENC%} = url pointing to the input file that has been url encoded.
{%OUT%} = output file.

'); + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + $description .= ' ' . t('Example using the Microsoft Ajax Minifier.

@code1

', array( + '@link1' => 'http://ajaxmin.codeplex.com/', + '@code1' => 'AjaxMinifier {%IN%} -o {%OUT%}', + )); + } + + if ($params[0] === 'js') { + $description .= ' ' . t('Example using the Google Closure Compiler.

@code1

', array( + '@link1' => 'https://developers.google.com/closure/compiler/docs/gettingstarted_app', + '@code1' => 'java -jar compiler.jar --js {%CWD%}/{%IN%} --js_output_file {%OUT%}', + )); + + $description .= ' ' . t('Example using curl to compress via the Online Google Closure Compiler.

@code1

', array( + '@link1' => 'https://developers.google.com/closure/compiler/docs/api-ref', + '@code1' => 'curl -o {%OUT%} -d output_info=compiled_code -d code_url={%IN_URL_ENC%} http://closure-compiler.appspot.com/compile', + )); + } + if ($params[0] === 'css') { + $description .= ' ' . t('Example using the YUI Compressor.

@code1

', array( + '@link1' => 'http://yui.github.io/yuicompressor/', + '@code1' => 'java -jar yuicompressor-x.y.z.jar --type css --line-break 4096 {%CWD%}/{%IN%} -o {%OUT%}', + )); + + $description .= ' ' . t('Example using curl to compress via an online CSS Compressor.

@code1

', array( + '@link1' => 'http://cnvyr.io/', + '@code1' => 'curl -o {%OUT%} -F \'files0=@{%IN%}\' http://srv.cnvyr.io/v1?min=css', + )); + } + + $form[$params[0]]['cmd']['advagg_ext_compress_' . $params[0] . '_cmd'] = array( + '#type' => 'textfield', + '#title' => t('Command to run'), + '#default_value' => variable_get('advagg_ext_compress_' . $params[0] . '_cmd', ''), + '#description' => $description, + ); +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.info b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.info new file mode 100644 index 000000000..955a0c570 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.info @@ -0,0 +1,13 @@ +name = AdvAgg External Compression +description = Compress Javascript and/or CSS with a command line compressor. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg + +configure = admin/config/development/performance/advagg/ext-compress + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.module b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.module new file mode 100644 index 000000000..5bfe00f4b --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_ext_compress/advagg_ext_compress.module @@ -0,0 +1,241 @@ + 'External Compression', + 'description' => 'Adjust External Compression settings.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_ext_compress_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg_ext_compress.admin.inc', + 'weight' => 10, + ); + + return $items; +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @addtogroup advagg_hooks + * @{ + */ + +/** + * Implements hook_advagg_js_compress_configuration_alter(). + */ +function advagg_ext_compress_advagg_js_compress_configuration_alter(&$options_desc, &$compressors, &$functions) { + list($options, $description) = $options_desc; + + $key = 10; + while (isset($options[$key])) { + $key++; + } + $options[$key] = t('AdvAgg Command Line Compressor'); + $compressors[$key] = 'advagg_cmdline'; + $functions[$key] = 'advagg_ext_compress_js_compress'; + + $options_desc = array($options, $description); +} + +/** + * Implements hook_advagg_css_compress_configuration_alter(). + */ +function advagg_ext_compress_advagg_css_compress_configuration_alter(&$options_desc, &$compressors, &$functions) { + list($options, $description) = $options_desc; + + $key = 10; + while (isset($options[$key])) { + $key++; + } + $options[$key] = t('AdvAgg Command Line Compressor'); + $functions[$key] = 'advagg_ext_compress_css_compress'; + + $options_desc = array($options, $description); +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Compress Javascript using via command line. + * + * @param string $input_file + * The file containing the uncompressed css/js data. + * @param string $ext + * The string css or js. + * @param array $debug + * Optional debug array. + * + * @return string + * The filename containing the compressed css/js data. + */ +function advagg_ext_compress_execute_cmd($input_file, $ext = '', array &$debug = array()) { + $run = variable_get("advagg_ext_compress_{$ext}_cmd", ''); + if (empty($run)) { + return FALSE; + } + + // Get file extension. + if (empty($ext)) { + $ext = strtolower(pathinfo($input_file, PATHINFO_EXTENSION)); + if ($ext !== 'css' && $ext !== 'js') { + // Get the $ext from the database. + $row = db_select('advagg_files', 'af') + ->fields('af') + ->condition('filename', $input_file) + ->execute()->fetchAssoc(); + if (!empty($row['filetype'])) { + $ext = $row['filetype']; + } + if ($ext === 'less') { + $ext = 'css'; + } + } + } + + // Generate temp file. + $temp_file = drupal_tempnam('temporary://', 'advagg_file_'); + $new_temp_file = $temp_file . '.' . basename($input_file); + @rename($temp_file, $new_temp_file); + // Set the permissions on the temp file. + drupal_chmod($new_temp_file); + $output = advagg_get_relative_path($new_temp_file); + + // Create command to run. + $cmd = str_replace(array( + '{%CWD%}', + '{%IN%}', + '{%IN_URL_ENC%}', + '{%OUT%}', + ), array( + DRUPAL_ROOT, + $input_file, + urlencode(file_create_url($input_file)), + escapeshellarg(realpath($output)), + ), $run); + + // Run command and return the output file. + $shell_output = array(); + $return_var = 0; + $shell = exec($cmd, $shell_output, $return_var); + $debug = array($cmd, $shell_output, $return_var, $shell); + + // Cleanup leftover files. + if (file_exists($temp_file)) { + @unlink($temp_file); + } + return $output; +} + +/** + * Compress Javascript using via command line. + * + * @param string $contents + * The JavaScript to compress. + * @param bool $log_errors + * TRUE to log errors. + * + * @return bool + * FALSE if this failed. + */ +function advagg_ext_compress_js_compress(&$contents, $log_errors) { + return advagg_ext_compress_string($contents, 'js', $log_errors); +} + +/** + * Compress CSS using via command line. + * + * @param string $contents + * The CSS to compress. + * @param bool $log_errors + * TRUE to log errors. + * + * @return bool + * FALSE if this failed. + */ +function advagg_ext_compress_css_compress(&$contents, $log_errors) { + return advagg_ext_compress_string($contents, 'css', $log_errors); +} + +/** + * Compress CSS using via command line. + * + * @param string $contents + * The data to compress. + * @param string $type + * Should be css or js. + * @param bool $log_errors + * TRUE to log errors. + * + * @return bool + * TRUE on success. + */ +function advagg_ext_compress_string(&$contents, $type, $log_errors) { + list($css_path, $js_path) = advagg_get_root_files_dir(); + if ($type === 'css') { + $dir = $css_path[0]; + } + else { + $dir = $js_path[0]; + } + $new_temp_file = $dir . '/advagg_file_' . drupal_hash_base64(microtime(TRUE) . mt_rand()) . '.' . $type; + $temp_file_full = advagg_get_relative_path($new_temp_file); + + file_put_contents($new_temp_file, $contents); + // Set the permissions on the temp file. + drupal_chmod($new_temp_file); + $debug = array(); + $output = advagg_ext_compress_execute_cmd($temp_file_full, $type, $debug); + if (empty($output)) { + return FALSE; + } + $new_contents = advagg_file_get_contents($output); + if (strpos($new_contents, 'Error') === 0) { + if ($log_errors) { + watchdog('advagg_ext_compress', "@a \n
\n
@b", array( + // Only log 4k of data. + '@a' => substr($new_contents, 0, 4096), + '@b' => print_r($debug, TRUE), + )); + } + $return = FALSE; + } + else { + $contents = $new_contents; + $return = TRUE; + } + + // Cleanup. + if (file_exists($new_temp_file)) { + unlink($new_temp_file); + } + if (file_exists($temp_file_full)) { + unlink($temp_file_full); + } + if (file_exists($output)) { + unlink($output); + } + return $return; +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.admin.inc b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.admin.inc new file mode 100644 index 000000000..32406e585 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.admin.inc @@ -0,0 +1,209 @@ + t('Disabled'), + 6 => t('Externally load the latest from github (version: @version)', array('@version' => $version)), + ); + $description = t('This will use the fallback font until the font has been downloaded. See fontfaceobserver for more info.', array( + '@link' => $library['vendor url'], + )); + if (function_exists('libraries_info')) { + if ($library['installed']) { + $options += array( + 2 => t('Inline javascript (version: @version)', array('@version' => $library['version'])), + 4 => t('Local file included in aggregate (version: @version)', array('@version' => $library['version'])), + ); + } + elseif (!is_readable('sites/all/libraries/fontfaceobserver/fontfaceobserver.js')) { + $description .= ' ' . t('To use fontfaceobserver locally fontfaceobserver needs to be placed inside the sites/all/libraries directory so sites/all/libraries/fontfaceobserver/fontfaceobserver.js and package.json can be found at that location.', array( + '@url' => 'https://www.drupal.org/project/libraries', + )); + } + else { + $description .= ' ' . t('Go to the library report page and make sure fontfaceobserver is installed correctly.', array( + '@url' => url('admin/reports/libraries'), + )); + } + } + elseif (is_readable('sites/all/libraries/fontfaceobserver/fontfaceobserver.js')) { + $description .= ' ' . t('To use fontfaceobserver locally the libraries api module needs to be installed.', array( + '@url' => 'https://www.drupal.org/project/libraries', + )); + } + else { + $description .= ' ' . t('To use fontfaceobserver locally the libraries api module needs to be installed and then fontfaceobserver needs to be placed inside the sites/all/libraries directory so sites/all/libraries/fontfaceobserver/fontfaceobserver.js and package.json can be found at that location.', array( + '@url' => 'https://www.drupal.org/project/libraries', + )); + } + + ksort($options); + $form['advagg_font_fontfaceobserver'] = array( + '#type' => 'radios', + '#title' => t('Use font face observer to load fonts asynchronously.'), + '#default_value' => variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER), + '#options' => $options, + '#description' => $description, + ); + + $form['container'] = array( + '#type' => 'container', + '#states' => array( + 'invisible' => array( + ':input[name="advagg_font_fontfaceobserver"]' => array('value' => '0'), + ), + ), + ); + $form['container']['advagg_font_storage'] = array( + '#type' => 'checkbox', + '#title' => t('Use localStorage so the flash of unstyled text (FOUT) only happens once.'), + '#default_value' => variable_get('advagg_font_storage', ADVAGG_FONT_STORAGE), + '#description' => t('Data is stored in localStorage under advagg_fonts. If this is a problem you can disable localStorage from being used; if doing so the FOUT (Flash of Unstyled Text) will happen on every page load if cookies is also not being used.', array('@cookie' => 'advaggfont_pt-sans=PT Sans')), + ); + $form['container']['advagg_font_cookie'] = array( + '#type' => 'checkbox', + '#title' => t('Set a cookie so the flash of unstyled text (FOUT) only happens once.'), + '#default_value' => variable_get('advagg_font_cookie', ADVAGG_FONT_COOKIE), + '#description' => t('Cookies are name like @cookie. If this is a problem you can disable cookies from being set; if doing so the FOUT (Flash of Unstyled Text) will happen on every page load if localStorage is also not being used.', array('@cookie' => 'advaggfont_pt-sans=PT Sans')), + ); + $form['container']['advagg_font_no_fout'] = array( + '#type' => 'checkbox', + '#title' => t('Prevent the Flash of Unstyled Text.'), + '#default_value' => variable_get('advagg_font_no_fout', ADVAGG_FONT_NO_FOUT), + '#description' => t('The font will not be changed unless the browser already has the font downloaded. Font gets downloaded on the first page view.', array('@cookie' => 'advaggfont_pt-sans=PT Sans')), + '#states' => array( + 'disabled' => array( + '#edit-advagg-font-cookie' => array('checked' => FALSE), + '#edit-advagg-font-storage' => array('checked' => FALSE), + ), + ), + ); + + // Get all css files and scan for quoted fonts. + $form['fonts'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('Async Loaded fonts in CSS'), + '#description' => t('Assumes quoted fonts will be downloaded and unquoted fonts are fallbacks.'), + ); + $form['fonts_not_async'] = array( + '#type' => 'fieldset', + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#title' => t('NOT Async Loaded in CSS'), + '#description' => t('Assumes quoted fonts will be downloaded and unquoted fonts are fallbacks. If there is no fallback it will apear below.'), + ); + // Get filename, filename_hash, and changes. + $results = db_select('advagg_files', 'af') + ->fields('af', array('filename', 'filename_hash', 'changes')) + ->condition('filetype', 'css') + ->orderBy('filename', 'ASC') + ->execute(); + while ($row = $results->fetchAssoc()) { + if (!file_exists($row['filename'])) { + continue; + } + // Get the file contents. + $file_contents = (string) @advagg_file_get_contents($row['filename']); + // Get font names. + list($replacements, $fonts_with_no_replacements) = advagg_font_get_replacements_array($file_contents); + if (!empty($replacements)) { + $fonts = array(); + foreach ($replacements as $key => $replacement) { + // Do not display !important after the fallback font name. + $replacement[5] = str_replace(' !important', '', $replacement[5]); + $fonts[$key . ' ' . $replacement[3]] = $replacement[5]; + } + + $form['fonts'][$row['filename_hash']] = array( + '#markup' => '
' . t('%file - @replacements
', array( + '@replacements' => str_ireplace('array', '', print_r($fonts, TRUE)), + '%file' => $row['filename'], + )) . '
', + ); + } + if (!empty($fonts_with_no_replacements)) { + $fonts = array(); + foreach ($fonts_with_no_replacements as $key => $replacement) { + // Do not display !important after the fallback font name. + $replacement = str_replace(' !important', '', $replacement); + $fonts[$key . ' ' . $replacement] = $replacement; + } + + $form['fonts_not_async'][$row['filename_hash']] = array( + '#markup' => '
' . t('%file - @replacements
', array( + '@replacements' => str_ireplace('array', '', print_r($fonts, TRUE)), + '%file' => $row['filename'], + )) . '
', + ); + } + } + $children = element_children($form['fonts']); + + // If no fonts are found; disable this module. + if (count($children) == 0) { + $form['advagg_font_fontfaceobserver']['#default_value'] = 0; + $form['advagg_font_fontfaceobserver']['#disabled'] = TRUE; + + if (empty($results)) { + $form['fonts'] = array( + '#type' => 'fieldset', + '#title' => t('No CSS files have been aggregated.'), + '#description' => t('You need to enable aggregation. No css files where found in the advagg_files table.'), + ); + } + else { + $form['fonts'] = array( + '#type' => 'fieldset', + '#title' => t('No CSS files with external fonts found.'), + '#description' => t('Currently this module is not doing anything. Recommend uninstalling it as advagg is not processing any css files that use an external font file.'), + ); + } + } + + // Clear the cache bins on submit. + $form['#submit'][] = 'advagg_font_admin_settings_form_submit'; + + return system_settings_form($form); +} + +/** + * Submit callback, clear out the advagg cache bin. + * + * @ingroup advagg_forms_callback + */ +function advagg_font_admin_settings_form_submit($form, &$form_state) { + // Clear caches. + advagg_cache_clear_admin_submit(); + + // Disable cookie and local storage if ffo is disabled. + if (empty($form_state['values']['advagg_font_fontfaceobserver'])) { + $form_state['values']['advagg_font_cookie'] = 0; + $form_state['values']['advagg_font_storage'] = 0; + } + // Disable no fout if cookies and local storage are disabled. + if (empty($form_state['values']['advagg_font_cookie']) + && empty($form_state['values']['advagg_font_storage']) + ) { + $form_state['values']['advagg_font_no_fout'] = 0; + } +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.advagg.inc b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.advagg.inc new file mode 100644 index 000000000..ef1ef3a04 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.advagg.inc @@ -0,0 +1,63 @@ += current_time) { + // Only allow alpha numeric class names. + window.document.documentElement.className += ' ' + key.replace(/[^a-zA-Z0-9-]/g, ''); + } + } + } +} + + +// Check cookies ASAP and set class. +advagg_font_inline(); diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.install b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.install new file mode 100644 index 000000000..24081aba9 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.install @@ -0,0 +1,61 @@ + $t('AdvAgg Font'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('AdvAgg async font loading is disabled.'), + 'description' => $t('Go to the AdvAgg Async Font Loader settings page and select an option other than disabled, or go to the modules page and disable the "AdvAgg Async Font Loader" module.', array( + '@settings' => url($config_path . '/advagg/font'), + '@modules' => url('admin/modules', array( + 'fragment' => 'edit-modules-advanced-cssjs-aggregation', + )), + )), + ); + } + + // Check version. + $lib_name = 'fontfaceobserver'; + $module_name = 'advagg_css_compress'; + list($description, $info) = advagg_get_version_description($lib_name, $module_name); + if (!empty($description)) { + $requirements["{$module_name}_{$lib_name}_updates"] = array( + 'title' => $t('@module_name', array('@module_name' => $info['name'])), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('The @name library needs to be updated.', array('@name' => $lib_name)), + 'description' => $description, + ); + } + + return $requirements; +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.js b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.js new file mode 100644 index 000000000..8b031464f --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.js @@ -0,0 +1,110 @@ +/** + * @file + * Used to add a class to the top level element when an external font is ready. + */ + +/* global Drupal:false */ + +/** + * Run the check. + * + * @param {string} key + * The class name to add to the html tag. + * @param {string} value + * The font name. + */ +function advagg_run_check(key, value) { + 'use strict'; + // Only run if window.FontFaceObserver is defined. + if (window.FontFaceObserver) { + // Only alpha numeric value. + key = key.replace(/[^a-zA-Z0-9-]/g, ''); + if (typeof window.FontFaceObserver.prototype.load === 'function') { + new window.FontFaceObserver(value).load().then(function () { + advagg_run_check_inner(key, value); + }, function () {}); + } + else { + new window.FontFaceObserver(value).check().then(function () { + advagg_run_check_inner(key, value); + }, function () {}); + } + } + else { + // Try again in 100 ms. + window.setTimeout(function () { + advagg_run_check(key, value); + }, 100); + } +} + +/** + * Run the check. + * + * @param {string} key + * The class name to add to the html tag. + * @param {string} value + * The font name. + */ +function advagg_run_check_inner(key, value) { + 'use strict'; + // Set Class. + if (parseInt(Drupal.settings.advagg_font_no_fout, 10) !== 1) { + window.document.documentElement.className += ' ' + key; + } + + // Set for a day. + var expire_date = new Date().getTime() + 86400 * 1000; + + if (Storage !== void 0 && parseInt(Drupal.settings.advagg_font_storage, 10) === 1) { + // Use local storage. + var fonts = JSON.parse(localStorage.getItem('advagg_fonts')); + if (!fonts) { + fonts = {}; + } + fonts[key] = expire_date; + localStorage.setItem('advagg_fonts', JSON.stringify(fonts)); + } + else if (parseInt(Drupal.settings.advagg_font_cookie, 10) === 1) { + // Use cookies if enabled and local storage not available. + expire_date = new Date(expire_date).toUTCString(); + document.cookie = 'advaggfont_' + key + '=' + value + + '; expires=' + expire_date + + '; domain=.' + document.location.hostname + + '; path=/'; + } +} + +/** + * Get the list of fonts to check for. + */ +function advagg_font_add_font_classes_on_load() { + 'use strict'; + for (var key in Drupal.settings.advagg_font) { + if (Drupal.settings.advagg_font.hasOwnProperty(key)) { + var html_class = (' ' + window.document.documentElement.className + ' ').indexOf(' ' + key + ' '); + // If the class already exists in the html element do nothing. + if (html_class === -1) { + // Wait till the font is downloaded, then set cookie & class. + advagg_run_check(key, Drupal.settings.advagg_font[key]); + } + } + } +} + +/** + * Make sure window.Drupal.settings.advagg_font is defined before running. + */ +function advagg_font_check() { + 'use strict'; + if (window.Drupal && window.Drupal.settings && window.Drupal.settings.advagg_font) { + advagg_font_add_font_classes_on_load(); + } + else { + // Try again in 20 ms. + window.setTimeout(advagg_font_check, 20); + } +} + +// Start the process. +advagg_font_check(); diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.module b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.module new file mode 100644 index 000000000..9f3a9c868 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_font/advagg_font.module @@ -0,0 +1,542 @@ + array(), + 'advagg_font_storage' => variable_get('advagg_font_storage', ADVAGG_FONT_STORAGE), + 'advagg_font_cookie' => variable_get('advagg_font_cookie', ADVAGG_FONT_COOKIE), + 'advagg_font_no_fout' => variable_get('advagg_font_no_fout', ADVAGG_FONT_NO_FOUT), + ), array('type' => 'setting')); + + // Add inline script for reading the cookies and adding the fonts already + // loaded to the html class. + if (variable_get('advagg_font_cookie', ADVAGG_FONT_COOKIE) || variable_get('advagg_font_storage', ADVAGG_FONT_STORAGE)) { + $inline_script_min = 'for(var fonts=document.cookie.split("advaggf"),i=0;i=current_time&&(window.document.documentElement.className+=" "+key.replace(/[^a-zA-Z0-9\-]/g,""))}'; + drupal_add_js($inline_script_min, array( + 'type' => 'inline', + 'group' => JS_LIBRARY - 1, + 'weight' => -50000, + 'scope' => 'above_css', + 'scope_lock' => TRUE, + 'movable' => FALSE, + 'no_defer' => TRUE, + )); + } + + // Get library data for fontfaceobserver. + $library = advagg_get_library('fontfaceobserver', 'advagg_font'); + // If libraries_load() does not exist load library externally. + if (!is_callable('libraries_load')) { + $advagg_font_ffo = 6; + } + // Add fontfaceobserver.js. + if ($advagg_font_ffo != 6 && empty($library['installed'])) { + // The fontfaceobserver library is not installed; use external variant. + $advagg_font_ffo = 6; + } + if ($advagg_font_ffo == 6) { + // Use the external variant. + foreach ($library['variants']['external']['files']['js'] as $data => $options) { + drupal_add_js($data, $options); + } + } + else { + // Load the fontfaceobserver library. + if ($advagg_font_ffo == 2) { + // Use the inline variant. + libraries_load('fontfaceobserver', 'inline'); + } + else { + libraries_load('fontfaceobserver'); + } + } + + // Add advagg_font.js; sets cookie and changes the class of the top level + // element once a font has been downloaded. + $file_path = drupal_get_path('module', 'advagg_font') . '/advagg_font.js'; + drupal_add_js($file_path, array( + 'async' => TRUE, + 'defer' => TRUE, + )); +} + +/** + * Implements hook_css_alter(). + */ +function advagg_css_alter(&$css) { + // Skip if advagg is disabled. + if (!advagg_enabled()) { + return; + } + + // Skip if fontface is disabled. + if (empty(variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER))) { + return; + } + + // Skip if fonts added to critical css is disabled. + if (empty(variable_get('advagg_font_add_to_critical_css', ADVAGG_FONT_ADD_TO_CRITICAL_CSS))) { + return; + } + + $critical_css_key = NULL; + foreach ($css as $key => $values) { + if (!empty($values['critical-css']) && $values['type'] === 'inline') { + $critical_css_key = $key; + } + } + + // Skip if no critical css. + if (is_null($critical_css_key)) { + return; + } + + module_load_include('inc', 'advagg', 'advagg'); + $css_to_add = ''; + foreach ($css as $key => $values) { + if ($values['type'] === 'file') { + $info = advagg_get_info_on_file($key); + if (!empty($info['advagg_font'])) { + // Get the file contents. + $file_contents = (string) @advagg_file_get_contents($info['data']); + if (empty($file_contents)) { + continue; + } + list($replacements) = advagg_font_get_replacements_array($file_contents); + foreach ($replacements as $replace) { + $css_to_add .= $replace[2]; + } + } + } + } + if (!empty($css_to_add)) { + $css[$critical_css_key]['data'] .= "\n{$css_to_add}"; + } +} + +/** + * Implements hook_menu(). + */ +function advagg_font_menu() { + $file_path = drupal_get_path('module', 'advagg_font'); + $config_path = advagg_admin_config_root_path(); + + $items[$config_path . '/advagg/font'] = array( + 'title' => 'Async Font Loader', + 'description' => 'Load external fonts in a non blocking manner.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('advagg_font_admin_settings_form'), + 'type' => MENU_LOCAL_TASK, + 'access arguments' => array('administer site configuration'), + 'file path' => $file_path, + 'file' => 'advagg_font.admin.inc', + 'weight' => 10, + ); + + return $items; +} + +/** + * @} End of "addtogroup hooks". + */ + +/** + * @addtogroup 3rd_party_hooks + * @{ + */ + +/** + * Implements hook_libraries_info(). + */ +function advagg_font_libraries_info() { + $libraries['fontfaceobserver'] = array( + // Only used in administrative UI of Libraries API. + 'name' => 'fontfaceobserver', + 'vendor url' => 'https://github.com/bramstein/fontfaceobserver', + 'download url' => 'https://github.com/bramstein/fontfaceobserver/archive/master.zip', + 'version arguments' => array( + 'file' => 'package.json', + // 1.50. : "version": "1.5.0". + 'pattern' => '/"version":\\s+"([0-9\.]+)"/', + 'lines' => 10, + ), + 'remote' => array( + 'callback' => 'advagg_get_github_version_json', + 'url' => 'https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@master/package.json', + ), + 'files' => array( + 'js' => array( + 'fontfaceobserver.js' => array( + 'type' => 'file', + 'group' => JS_LIBRARY, + 'async' => TRUE, + 'defer' => TRUE, + ), + ), + ), + 'variants' => array(), + ); + // Get the latest tagged version for external file loading. + $version = advagg_get_remote_libraries_version('fontfaceobserver', $libraries['fontfaceobserver']); + $libraries['fontfaceobserver']['variants'] += array( + 'external' => array( + 'files' => array( + 'js' => array( + "https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@v{$version}/fontfaceobserver.js" => array( + 'type' => 'external', + 'data' => "https://cdn.jsdelivr.net/gh/bramstein/fontfaceobserver@v{$version}/fontfaceobserver.js", + 'async' => TRUE, + 'defer' => TRUE, + ), + ), + ), + ), + ); + // Inline if local js is there. + $libraries_paths = array(); + if (is_callable('libraries_get_libraries')) { + $libraries_paths = libraries_get_libraries(); + } + if (!empty($libraries_paths['fontfaceobserver']) && is_readable($libraries_paths['fontfaceobserver'] . '/fontfaceobserver.js')) { + $libraries['fontfaceobserver']['variants'] += array( + 'inline' => array( + 'files' => array( + 'js' => array( + 'loadCSS_inline' => array( + 'type' => 'inline', + 'data' => (string) @advagg_file_get_contents($libraries_paths['fontfaceobserver'] . '/fontfaceobserver.js'), + 'no_defer' => TRUE, + ), + ), + ), + ), + ); + } + + return $libraries; +} + +/** + * @} End of "addtogroup 3rd_party_hooks". + */ + +/** + * @addtogroup advagg_hooks + * @{ + */ + +/** + * Implements hook_advagg_current_hooks_hash_array_alter(). + */ +function advagg_font_advagg_current_hooks_hash_array_alter(&$aggregate_settings) { + $aggregate_settings['variables']['advagg_font_fontfaceobserver'] = variable_get('advagg_font_fontfaceobserver', ADVAGG_FONT_FONTFACEOBSERVER); +} + +/** + * @} End of "addtogroup advagg_hooks". + */ + +/** + * Get the replacements array for the css. + * + * @param string $css_string + * String of CSS. + * + * @return array + * An array containing the replacemnts and the font class name. + */ +function advagg_font_get_replacements_array($css_string) { + // Get the CSS that contains a font-family rule. + $length = strlen($css_string); + $property_position = 0; + $property = 'font'; + $property_alt = 'font-family'; + $replacements = array(); + $fonts_with_no_replacements = array(); + $lower = strtolower($css_string); + $safe_fonts_list = array( + 'georgia' => TRUE, + 'palatino' => TRUE, + 'times new roman' => TRUE, + 'times' => TRUE, + + 'arial' => TRUE, + 'helvetica' => TRUE, + 'gadget' => TRUE, + 'verdana' => TRUE, + 'geneva' => TRUE, + 'tahoma' => TRUE, + 'garamond' => TRUE, + 'bookman' => TRUE, + 'comic sans ms' => TRUE, + 'cursive' => TRUE, + 'trebuchet ms' => TRUE, + 'arial black' => TRUE, + 'impact' => TRUE, + 'charcoal' => TRUE, + + 'courier new' => TRUE, + 'courier' => TRUE, + 'monaco' => TRUE, + + 'system' => TRUE, + ); + + while (($property_position = strpos($lower, $property, $property_position)) !== FALSE) { + // Find the start of the values for the property. + $start_of_values = strpos($css_string, ':', $property_position); + // Get the property at this location of the css. + $property_in_loop = trim(substr($css_string, $property_position, ($start_of_values - $property_position))); + + // Make sure this property is one of the ones we're looking for. + if ($property_in_loop !== $property && $property_in_loop !== $property_alt) { + $property_position += strlen($property); + continue; + } + + // Get position of the last closing bracket plus 1 (start of this section). + $start = strrpos($css_string, '}', -($length - $property_position)); + if ($start === FALSE) { + // Property is in the first selector and a declaration block (full rule + // set). + $start = 0; + } + else { + // Add one to start after the }. + $start++; + } + + // Get closing bracket (end of this section). + $end = strpos($css_string, '}', $property_position); + if ($end === FALSE) { + // The end is the end of this file. + $end = $length; + } + + // Get closing ; in order to get the end of the declaration of the property. + $declaration_end_a = strpos($css_string, ';', $property_position); + $declaration_end_b = strpos($css_string, '}', $property_position); + if ($declaration_end_a === FALSE) { + $declaration_end = $declaration_end_b; + } + else { + $declaration_end = min($declaration_end_a, $declaration_end_b); + } + if ($declaration_end > $end) { + $declaration_end = $end; + } + // Add one in order to capture the } when we ge the full rule set. + $end++; + // Advance position for the next run of the while loop. + $property_position = $end; + + // Get values assigned to this property. + $values_string = substr($css_string, $start_of_values + 1, $declaration_end - ($start_of_values + 1)); + // Parse values string into an array of values. + $values_array = explode(',', $values_string); + if (empty($values_array)) { + continue; + } + + // Values array, first element is a quoted string. + $dq = strpos($values_array[0], '"'); + $sq = strpos($values_array[0], "'"); + $quote_pos = ($sq !== FALSE) ? $sq : $dq; + // Skip if the first font is not quoted. + if ($quote_pos === FALSE) { + continue; + } + + $values_array[0] = trim($values_array[0]); + // Skip if only one font is listed. + if (count($values_array) === 1) { + $fonts_with_no_replacements[$values_array[0]] = ''; + continue; + } + + // Save the first value to a variable; starting at the quote. + $removed_value_original = substr($values_array[0], max($quote_pos - 1, 0)); + + // Resave first value. + if ($quote_pos > 1) { + $values_array[0] = trim(substr($values_array[0], 0, $quote_pos - 1)); + } + + // Get value as a classname. Remove quotes, trim, lowercase, and replace + // spaces with dashes. + $removed_value_classname = strtolower(trim(str_replace(array('"', "'"), '', $removed_value_original))); + $removed_value_classname = str_replace(' ', '-', $removed_value_classname); + + // Remove value if it contains a quote. + $values_array_copy = $values_array; + foreach ($values_array as $key => $value) { + if (strpos($value, '"') !== FALSE || strpos($value, "'") !== FALSE) { + unset($values_array[$key]); + } + elseif ($key !== 0) { + break; + } + } + + if (empty($values_array)) { + // See if there's a "safe" fallback that is quoted. + $values_array = $values_array_copy; + foreach ($values_array as $key => $value) { + if (strpos($value, '"') !== FALSE || strpos($value, "'") !== FALSE) { + if ($key !== 0) { + $lower_key = trim(trim(strtolower(trim($value)), '"'), "'"); + if (!empty($safe_fonts_list[$lower_key])) { + break; + } + } + unset($values_array[$key]); + } + elseif ($key !== 0) { + break; + } + } + if (empty($values_array)) { + // No unquoted values left; do not modify the css. + $key = array_shift($values_array_copy); + $fonts_with_no_replacements[$key] = implode(',', $values_array_copy); + continue; + } + } + + $extra = ''; + if (isset($values_array[0])) { + $extra = $values_array[0] . ' '; + unset($values_array[0]); + } + // Rezero the keys. + $values_array = array_values($values_array); + // Save next value. + $next_value_original = trim($values_array[0]); + // Create the values string. + $new_values_string = $extra . implode(',', $values_array); + + // Get all selectors. + $end_of_selectors = strpos($css_string, '{', $start); + $selectors = substr($css_string, $start, $end_of_selectors - $start); + // Ensure selectors is not a media query. + if (stripos($selectors, "@media") !== FALSE) { + // Move the start to the end of the media query. + $start = $end_of_selectors + 1; + // Get the selectors again. + $end_of_selectors = strpos($css_string, '{', $start); + $selectors = substr($css_string, $start, $end_of_selectors - $start); + } + + // From advagg_load_stylesheet_content(). + // Perform some safe CSS optimizations. + // Regexp to match comment blocks. + // Regexp to match double quoted strings. + // Regexp to match single quoted strings. + $comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'; + $double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"'; + $single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'"; + // Strip all comment blocks, but keep double/single quoted strings. + $selectors_stripped = preg_replace( + "<($double_quot|$single_quot)|$comment>Ss", + "$1", + $selectors + ); + + // Add css class to all the selectors. + $selectors_array = explode(',', $selectors_stripped); + foreach ($selectors_array as &$selector) { + // Remove extra whitespace. + $selector = trim($selector); + $selector = " .{$removed_value_classname} {$selector}"; + } + $new_selectors = implode(',', $selectors_array); + + // Get full rule set. + $full_rule_set = substr($css_string, $start, $end - $start); + // Replace values. + $new_values_full_rule_set = str_replace($values_string, $new_values_string, $full_rule_set); + // Add in old rule set with new selectors. + $new_selectors_full_rule_set = $new_selectors . '{' . $property_in_loop . ': ' . $values_string . ';}'; + + // Record info. + $replacements[] = array( + $full_rule_set, + $new_values_full_rule_set, + $new_selectors_full_rule_set, + $removed_value_original, + $removed_value_classname, + $next_value_original, + ); + } + return array($replacements, $fonts_with_no_replacements); +} diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.info b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.info new file mode 100644 index 000000000..d7a6735cf --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.info @@ -0,0 +1,11 @@ +name = AdvAgg CDN Javascript +description = Use a shared CDN for javascript libraries, Google Libraries API currently. +package = Advanced CSS/JS Aggregation +core = 7.x +dependencies[] = advagg + +; Information added by Drupal.org packaging script on 2020-11-19 +version = "7.x-2.35" +core = "7.x" +project = "advagg" +datestamp = "1605792717" diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.install b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.install new file mode 100644 index 000000000..39502ea5c --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.install @@ -0,0 +1,69 @@ +jquery update settings page and select a CDN instead of using this module.', array( + '@settings' => url('admin/config/development/jquery_update', array( + 'fragment' => 'edit-jquery-update-jquery-cdn', + )), + )); + } + else { + $jquery_description = $t('The jquery update module is already configured to use the external CDN "@cdn".', array('@cdn' => $jquery_cdn)); + } + + $requirements['advagg_js_cdn_jquery_update'] = array( + 'title' => $t('Adv JS CDN - jquery update'), + 'severity' => REQUIREMENT_WARNING, + 'value' => $t('Duplicate Functionality: Use jquery update instead of the advagg_js_cdn sub module.'), + 'description' => $jquery_description . ' ' . $t('You should go to the modules page and disable the "AdvAgg CDN JS" module.', array( + '@modules' => url('admin/modules', array( + 'fragment' => 'edit-modules-advanced-cssjs-aggregation', + )), + )), + ); + } + + if (empty($requirements)) { + $description = ''; + if (variable_get('advagg_cache_level', ADVAGG_CACHE_LEVEL) < 0) { + $description = $t('Will be using the unminified version due to AdvAgg being in Development mode.'); + } + $requirements['advagg_js_cdn'] = array( + 'title' => $t('Adv JS CDN'), + 'severity' => REQUIREMENT_OK, + 'value' => $t('OK'), + 'description' => $t('jQuery and jQuery UI JS should be coming from a CDN.') . ' ' . $description, + ); + } + + return $requirements; +} + +/** + * @} End of "addtogroup hooks". + */ diff --git a/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.module b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.module new file mode 100644 index 000000000..bcb164d83 --- /dev/null +++ b/frontend/drupal/sites/all/modules/advagg/advagg_js_cdn/advagg_js_cdn.module @@ -0,0 +1,162 @@ + $values) { + // Only modify if + // advagg_js_cdn_jquery is enabled, + // name is misc/jquery.js, + // and type is file. + if (variable_get('advagg_js_cdn_jquery', ADVAGG_JS_CDN_JQUERY) + && $name === 'misc/jquery.js' + && $javascript[$name]['type'] === 'file' + ) { + // Add in backup. + $values['weight'] += 0.00001; + $values['data'] = 'window.jQuery || document.write("