first import

This commit is contained in:
Bachir Soussi Chiadmi
2015-04-08 11:40:19 +02:00
commit 1bc61b12ad
8435 changed files with 1582817 additions and 0 deletions

View File

@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.

View File

@@ -0,0 +1,219 @@
Core Library
============
This module changes the Drupal core aggregation mecanism process. It greatly
reduces I/O and aggregated number of files, and improves chances of client
cache hit, therefore while it produces bigger aggregated files, it reduces
greatly the bandwidth utilization while users browse.
This is achieved by bypassing the dynamic CSS and JS inclusion. Instead of
including and aggregating only needed files on a per-page basis, it learns
files being used site-wide while user browse, then is able to produce larger
therefore more revelant files that aggregate all those atomic files whether
or not they are being used on the page.
Over time, the number of aggregated files reduce to achieve a stable state
where all site JS files are being aggregated in one file only, and all site
CSS are being aggretated in only two files (libraries into one side, theme
files into another, can be more if there are browser-specific files).
Once the files are stable, site administrator can then enable the full bypass
mode, which will only uses the actual saved state and won't ever do any more
I/O for CSS and JS inclusion, based on the cached state.
On production sites that do not changes nor update their module often, the
performance boost is significant because no dynamic inclusion is being done
anymore (therefore no I/O are made at all), as the bandwidth consumption is
greatly reduced (because clients will cache these files at first hit and won't
download them on the server after that).
This module produces really a few side-effects, depeing on the theme coding
mainly. We bypass core mecanism, but keep sanefully files weighting which will
avoid most potential conflicts.
Motivation
----------
Drupal 7 does an hard and heavy work about JS and CSS aggregation. It is able
to separate aggregated files into multiple groups. This is a good thing for
lazzy site admins, the first reason is by doing finer groups, those per-group
aggregated files will have greater chances of getting static file hits.
Therefore, there is a main disadvantage: because groups are hardcoded, even on
a site where the files does not change on a per-page basis, the site will ever
aggregate at least 3 JS files, and 3 CSS files, which is not quite elegant.
On larger scale sites, site admin would want to bypass this ugly aggregation
style which would cause something like 4 to even a lot more useless HTTP
requests, on almost each client hit because aggregated files will be differnt
on a per-page basis.
The ideal case would be to have only one sitewide JS file, as only one sitewide
CSS file. This won't happen because:
- JS are split between libraries and Drupal locale translations.
- CSS are split between libraries and theme files (we do that, for a good
reason).
So, our ideal case will be to have only two sitewide files of each.
Original goal and methodology
-----------------------------
This module intend to allow site administrators to manually set which files
among all core libraries and module specific files should be aggregated as
core immutable libraries.
This is not true anymore because of the learning mecanism, which will be
enabled per default. Therefore, the suicidal tendencies of site admins tell
me that some of them will still use the manual UI in order to build their own
aggregation rules. In fact, the module has been built for this, and this is
a totally legal, moreover totally legitimate thing to do.
When using this manual mode, each one of the selected JS file candidate for
sitewide aggregation will be considered as a library and will be forced to
get to the JS_LIBRARY group. by doing this we open the door to file weight
conflict between group, thus, we have an override mecanism that will consider
each group as a weight addition and will pragmatically add an indescent weight
factor to files that core does not expose itself as a library. This will help
keeping things in order.
JS minification HOWTO
---------------------
This module can use the JSMin library PHP port in order to minify JS files.
Right now, we won't use any other contrib module in order to remain dependency
agnostic. This is important because this module has one and only one simple
goal, and it shouldn't rely on any other module that add extensive execution
overhead.
All is about performances, and also leaving the choice for the site admin to
do what he want to do.
So, we don't use any library handling module, sorry, it may change, but right
now the only good way of making it work is by adding the jsmin.php file into
this folder:
sites/all/libraries/jsmin/
You can download it there:
https://github.com/rgrove/jsmin-php/
Once you downloaded it, you can enable minification on the Core Library module
configuration page, and the form won't revert the option automatically anymore.
We do not provide this library because its licence may conflict with the GPL
one, sorry about that, but you are on your own for downloading it.
Potential known side effect with CSS
------------------------------------
CSS files are not processed the same way, because administration screens and
frontend pages won't have the same theme, we can't merge groups else we would
totally break the aggregation benefit of having only one large file. Solution
is a bit ugly, but will work on most sites: we pragmatically override the
CSS_DEFAULT group and add module specific CSS into the CSS_SYSTEM group, using
the same weight alteration mecanism as we use for JS.
The side effect of this (there is always one) is that Drupal, per default, will
put CSS_DEFAULT files after CSS_THEME theme specific files. We reverse order
and set the CSS_DEFAULT into the CSS_SYSTEM which break this behavior. This can
lead to CSS directive conflicts for theme that mess up with module specific CSS
files. Because of this statement, the CSS group merge remains optional.
But it seems we are lucky! Because theme CSS files are ordered before the module
specifics, it happens that theme developers are forced to do proper CSS override
using CSS directive specialization instead of relying onto order, which make our
reverse algorithm to almost always work as-is with well coded themes.
Auto-configuration
------------------
This module provide a learning mode. Three different profiles are provided for
this learning mode, they are described in administration section. These modes
all works quite the same:
1. At JS or CSS alteration time, unknown files that should be displayed
on page are found.
2. Once all these files have been found, the manual variable is populated with
the new files, and saved.
3. Then, the alteration goes, also using new file found for override, thus
aggregating them the first time.
4. When a new page is hit, chances that a new file is found is naturally
lowered, the more users browse, the faster you'll reach a state where your
Drupal site naturally aggregates all the files in one and only one file.
Once the configuration actually fits you (you have to do some profiling on your
own for that) you can then switch back to manual or bypass mode in order for
the learning process to stop. The module will still continue to force file
aggregation, using the earlier learnt files.
You can always go back, enable the UI module again and customize the automated
configuration on your own then.
Bypass mode
-----------
When the configuration is OK, site admin should switch to full bypass mode.
This particular mode alter the 'styles' and 'scripts' element types element
info, and forces it to be rendered with our own functions.
When creating CSS aggregated files using the bypass mode, the CSS grouping
mode will gracefully be set to 'dual' mode. This ensures that in case one
or more themes are enabled, there will always be only one included in the
page and will avoid CSS conflicts between themes.
This also ensures that each theme will have its own aggregated CSS file that
won't never be modified anymore until the admin switches back to another mode.
Once the bypass mode is set, aggregated files are generated only once using
the current learnt files state. Those files, once aggregated, won't regenerate
themselves if the files are being manually deleted on the system, so if it
happens, an specific button on the administration page will allow you to force
the module to rebuild these.
Bypass mode and drupal_add_TYPE()
---------------------------------
When using bypass mode, the hook_library_TYPE_alter() hooks will still be used
during page construction, this ensures the files' weights to be the right one,
we don't store the weight and we won't.
These functions may still do some file_exists() I/O that we should definitely
get rid off. This will be a future challenge.
Because drupal_add_TYPE() function will still be run whatever we do (we can't
take over module's code dynamically), why not use their result anyway?
UI and stat collection systems (Core Library Advanced UI module)
----------------------------------------------------------------
When in manual mode, aggregation rules can be built over administrative file
listings. We can get a list of system known libraries quite easily and expose
it to the site admin.
Nevertheless we have problems with modules' arbitrary set files, that the
system itself can't guess before those files are being processed at least
once.
We put in place a statistics collection system (which has really heavy and
indencent performance impact) that allows developers to play with a development
site and let the system discover files while browsing. Once all files (the only
thing the developer can do is hope that all files have been processed at least
once) have been discovered, another configuration screen is populated with what
we call the 'orphan files'. Those files are displayed side by side with number
of hits for each one of them.
This 'orphan' screen can then help decide which files the site admin would want
to force being processed and aggregated site wide.
Future
------
We intend to add a JS minification mecanism to enfore files to get through more
than aggregation, but also minification. This will save up a bit of bandwidth.
Some badly coded JS files won't pass through the minification and will cause
errors on client side, so we will make this mecanism site wide optional, at
first, then per file optional then.

View File

@@ -0,0 +1,146 @@
<?php
/**
* @file
* Core Library module administration pages.
*/
/**
* Main administration screen.
*/
function core_library_admin_settings($form, &$form_state) {
$form['bypass'] = array(
'#type' => 'fieldset',
'#title' => t("Drupal aggregation mecanism bypass"),
'#collapsible' => FALSE,
);
$form['bypass']['core_library_bypass'] = array(
'#type' => 'radios',
'#options' => array(
NULL => t("Disabled") . '<br/><small>' . t("Let Drupal core do what it has to do. If you select this, you should then disable this module.") . '</small>',
LIBRARY_MODE_MANUAL => t("Manual") . '<br/><small>' . t("Let the site admin choose which file should go through the aggregation process. This needs the Core Library Advanced UI module to be enabled at least to choose the files.") . '</small>',
LIBRARY_MODE_LEARNING_ANONYMOUS => t("Anonymous only") . '<br/><small>' . t("Automatically learn files that are being used for anymous users only.") . '</small>',
LIBRARY_MODE_LEARNING_NO_ADMIN => t("All, no admin") . '<br/><small>' . t("Automatically learn files to aggregate while users browse, excluding the administration pages.") . '</small>',
LIBRARY_MODE_LEARNING_ALL => t("All <cite>Pinage!</cite>") . '<br/><small>' . t("Automatically learn files to aggregate while users browse, including administration pages.") . '</small>',
LIBRARY_MODE_BYPASS => t("Full bypass <cite>I'm done playing!</cite>") . '<br/><small>' . t("As you select this option, you consider that auto learning did its job, and you don't want to let Drupal core mess up anymore with aggregated files. This means that it won't never ever see new CSS and JS files until you revert this option, but you will save a lot of I/O bypassing its aggregation mecanism, good for production sites, totally wrong for development or testing sites.") . '</small>',
),
'#default_value' => variable_get('core_library_bypass', NULL),
);
$form['css'] = array(
'#type' => 'fieldset',
'#title' => t("CSS grouping mode"),
'#collapsible' => FALSE,
);
$form['css']['core_library_css_group'] = array(
'#type' => 'radios',
'#options' => array(
NULL => t("Disabled") . '<br/><small>' . t("Let Drupal core group CSS as it intend to. This means that you will always get (at least) 3 CSS files, one per group.") . '</small>',
LIBRARY_CSS_GROUP_DUAL => t("Dual mode") . '<br/><small>' . t("System libraries, and arbitrary module files in one single file, followed by theme specifics into another file.") . '</small>',
),
'#default_value' => variable_get('core_library_css_group', NULL),
);
$form['behavior'] = array(
'#type' => 'fieldset',
'#title' => t("Behavior options"),
'#collapsible' => FALSE,
);
$form['behavior']['core_library_respectful_preprocess'] = array(
'#type' => 'checkbox',
'#title' => t("Respect modules preprocess settings"),
'#default_value' => variable_get('core_library_respectful_preprocess', TRUE),
'#description' => t("Modules can decide weither or not their JavaScript or CSS files should preprocessed or not. Some of them are complex video players, such as the Media Element script and need multiple files packaged with them. When a JavaScript is preprocessed, it is moved to another location which can prevent it from finding additional files, if you experience JavaScript problems try to enable this option."),
);
$form['advanced'] = array(
'#type' => 'fieldset',
'#title' => t("Advanced options"),
'#collapsible' => FALSE,
);
$form['advanced']['core_library_js_minify'] = array(
'#type' => 'checkbox',
'#title' => t("Minify JS"),
'#default_value' => variable_get('core_library_js_minify', FALSE),
'#description' => t("If checked, the system will attempt to minify JS files. This can lead to some JS crashing since a lot of custom JS files may not have been well formatted. Browsers are tolerant while minifier is not on JS syntax."),
);
$form = system_settings_form($form);
$form['actions']['submit']['#submit'] = $form['#submit'];
unset($form['#submit']);
$form['actions']['submit']['#submit'][] = 'core_library_admin_settings_submit_prepare_cache';
$form['actions']['clear'] = array(
'#type' => 'submit',
'#value' => t("Reset learnt files"),
'#submit' => array('core_library_admin_settings_submit_clear'),
);
$form['actions']['rebuild'] = array(
'#type' => 'submit',
'#value' => t("Rebuild files"),
'#attributes' => array('title' => t("This will force the system to rebuild aggregated and minified files.")),
'#submit' => array('core_library_admin_settings_submit_rebuild'),
);
return $form;
}
/**
* Prepare cache submit handler.
*/
function core_library_admin_settings_submit_prepare_cache($form, &$form_state) {
$error = NULL;
$error_variables = array();
$directory = 'public://static';
// Check our final static folder is valid.
if (!file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
$error = "Failed to create 'static-aggregation' directory, reverting aggregation bypass mode to manual.";
}
if ($error) {
// Error must not break the Drupal site. In case of any error, revert to
// manual mode, so the module will continue to work, but Drupal will also
// continue to built its own final html head rendering.
watchdog('core_library', $error, $error_variables, WATCHDOG_ERROR);
drupal_set_message(t($error, $error_variables), 'error');
// Revert variable, and set the error message for the end user.
variable_set('core_library_bypass', LIBRARY_MODE_MANUAL);
}
// Also check for the JSMin library to be present, if the option is checked.
if (variable_get('core_library_js_minify', TRUE) && !file_exists(core_library_js_minify_library_path())) {
drupal_set_message(t("JSMin library was not found, see README.txt"), 'error');
variable_set('core_library_js_minify', FALSE);
}
}
/**
* Clear learnt files submit handler.
*/
function core_library_admin_settings_submit_clear($form, &$form_state) {
variable_del('library_aggregation_settings');
variable_del('library_aggregation_orphans');
variable_del('core_library_css_status');
variable_del('core_library_updated');
drupal_set_message(t("Current aggregation settings have been removed."));
$current_mode = variable_get('core_library_bypass', NULL);
if ($current_mode == LIBRARY_MODE_BYPASS || $current_mode == LIBRARY_MODE_MANUAL) {
variable_set('core_library_bypass', NULL);
drupal_set_message(t("Aggregation mode was bypass or manual, set back to default."));
}
}
/**
* Rebuild aggregated files submit handler.
*/
function core_library_admin_settings_submit_rebuild($form, &$form_state) {
variable_del('core_library_css_status');
variable_del('core_library_js_minified');
drupal_set_message(t("Aggregated files marked for rebuild."));
drupal_set_message(t("Minified files cache destroyed."));
}

View File

@@ -0,0 +1,183 @@
<?php
/**
* @file
* Core Library bypass mode specific functions.
*/
/**
* Rebuild files for the given CSS group.
*
* This function is mainly a proxy to drupal_build_css_cache() which does its
* job quite well.
*
* Because this function will name files using an ugly hash and does heavy I/O,
* ours will call it only if really necessary, and will use non-randomized file
* names for HTML inclusion, therefore will proceed to a file copy right after
* each aggregated file has been created.
*
* @param array $map
* Mapped filename.
* @param array $medias
* Key value pairs, keys are CSS media property, values are Drupal core CSS
* description array, weight ordered, suitable for building the aggregated
* file.
*
* @return mixed
* Suitable value for stored status array. If there was any during aggregated
* file creation or copy, this will break any remaining files to aggregate
* return NULL, this will force a new attempt on next page hits.
*
* @see core_library_element_style_pre_render()
* @see drupal_build_css_cache()
*/
function _core_library_element_style_rebuild_group($map, $css) {
// Best case scenario, we have no files.
if (empty($css)) {
return -1;
}
// Build the file using core function.
if ($file_uri = drupal_build_css_cache($css)) {
if (file_unmanaged_copy($file_uri, 'public://static/' . $map . '.css', FILE_EXISTS_REPLACE)) {
// Mark the file as built.
watchdog('core_library', "CSS file for mapping " . $map . " has been sucessfully built", NULL, WATCHDOG_NOTICE);
return TRUE;
}
else {
watchdog('core_library', "CSS file for mapping " . $map . " could not be copied after being built", NULL, WATCHDOG_ERROR);
return NULL;
}
}
else {
// Built has failed, therefore we cannot continue.
watchdog('core_library', "CSS file for mapping " . $map . " could not be built", NULL, WATCHDOG_ERROR);
return NULL;
}
}
/**
* drupal_pre_render_styles() replacement.
*
* This function pass through multiple steps:
* 1. Fetch the current aggregated files status in the $status array.
* 2. Fetch the user set groups configuration in the $groups array.
* 3. Check we have files matching in the status array for each group, mark
* each missing file for rebuild in the $rebuild array.
* 4. Iterate over the $rebuild array, and rebuild missing files (if they are
* files matching the group).
* 5. If any modification has been done to $status, save it for next hits.
* 6. Build the drupal_render() final result using the current $status array
* which has been modified or not.
* 7. Remove items we consumed from the final list, and let external files
* being processed by core.
*
* During the whole process, we use a $mapping array. This array contain, for
* each group, an alphanumerical filename prefix for future aggregated files.
*
* Filenames are being built using this mapping array and this convention:
* - <group_prefix>-<media>.css
*
* This function aggregates all theme files in their own group, whatever is the
* user configuration, to ensure no theme conflicts if the site as more than
* one themes (most sites will have at least two, frontend and backend themes).
*
* Ideal case is, all files have already been built, therefore $status and
* $groups array matches each other. In this particular case, no I/O at all is
* being done and only the drupal_render() array is built pragmatically using
* the $status array content.
*/
function core_library_element_style_pre_render($elements) {
global $theme_key;
// Status array contains filename as keys, and numeric keys as values which
// can be any of those:
// - -1 : No files, no need to rebuild.
// - TRUE : File already built.
// - empty : Nothing done, or built failed, need rebuild.
$status = variable_get('core_library_css_status');
// Group to filename mapping. Each group can have one or more medias, we will
// distinguish this later in the algorithm.
$mapping = array(
CSS_SYSTEM => 'system',
CSS_THEME => 'theme-' . str_replace('_', '-', $theme_key),
CSS_DEFAULT => 'default',
);
// What do we need to rebuild, this array will contain groups as keys, and
// $css sub-array for each of those groups.
$rebuild = array();
// Determine how to build groups. This depends on the user configuration.
// User configuration has been taken into account by hook_library_css_alter()
// earlier during the normal execution flow. We only have to regroup those
// using the core grouping algorithm to ensure we have different groups for
// browser specific files.
foreach (drupal_group_css($elements['#items']) as $core_group) {
$map = NULL;
// Scan for external file, we won't handle ourselves.
if ($core_group['type'] == 'external') {
// FIXME: Do something!
}
// Scan for browser specific rules. Remember that
else if (!$core_group['browsers']['!IE']) {
$map = $mapping[$core_group['group']] . '-' . str_replace(' ', '', $core_group['browsers']['IE']) . '-' . $core_group['media'];
}
// Normal processing (all browsers).
else {
$map = $mapping[$core_group['group']] . '-' . $core_group['media'];
}
if ($map) {
// Populate our group array, using core given groups. Those groups reflect
// what we aggregated before at hook_library_css_alter() time, adding
// browser information we omitted or let live with defaults.
$groups[$map] = $core_group;
// Also mark the current for rebuild if needed.
if (!isset($status[$map]) || !$status[$map]) {
$rebuild[] = $map;
}
}
}
// Rebuild files we have to.
if (!empty($rebuild)) {
// Iterate over files, and rebuild the one we need.
foreach ($rebuild as $map) {
$status[$map] = _core_library_element_style_rebuild_group($map, $groups[$map]['items']);
}
// Save our status (do not rebuild CSS later).
variable_set('core_library_css_status', $status);
}
// Build the final result for drupal_render().
foreach ($groups as $map => $data) {
// Do not include empty or non existing files. All files should be empty
// or built when we get in this part of the algorithm.
if ($status[$map] == TRUE) {
// Compute the real filename using the same convention than the
// _core_library_element_style_rebuild_group() function.
$file = $map . '.css';
// Add the real element for drupal_render() function to include the tag.
$elements[] = array(
'#type' => 'html_tag',
'#tag' => 'link',
'#attributes' => array(
'type' => 'text/css',
'rel' => 'stylesheet',
'href' => file_create_url('public://static/' . $file),
'media' => $data['media'],
),
'#browsers' => $data['browsers'],
);
}
}
// All ok.
return $elements;
}

View File

@@ -0,0 +1,15 @@
name = Core Library
description = Extends Drupal core and allows you to override other modules' libraries definition and aggregation rules.
package = Performance
version = VERSION
core = 7.x
configure = admin/config/development/library
; Module files.
files[] = core_library.module
files[] = core_library.admin.inc
; Information added by drupal.org packaging script on 2012-01-04
version = "7.x-1.0-beta11"
core = "7.x"
project = "core_library"
datestamp = "1325679340"

View File

@@ -0,0 +1,100 @@
<?php
/**
* @file
* Core Library module schema.
*/
/**
* Implements hook_schema().
*/
function core_library_schema() {
$schema = array();
$schema['core_library_stat'] = array(
'fields' => array(
'lid' => array(
'description' => 'Primary identifier',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'file' => array(
'description' => 'File path',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'hits' => array(
'description' => 'Hits count',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'hits_anonymous' => array(
'description' => 'Anonymous hit',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'hits_admin' => array(
'description' => 'Admin hit',
'type' => 'int',
'not null' => TRUE,
'default' => 0,
),
'module' => array(
'description' => 'Module name, full path will be determined using its path',
'type' => 'varchar',
'length' => 64,
'not null' => TRUE,
'default' => '',
),
'type' => array(
'description' => 'File type, can be CSS or JS, maybe something else',
'type' => 'varchar',
'length' => 32,
'not null' => TRUE,
'default' => '',
),
),
'primary key' => array('lid'),
'unique keys' => array('file' => array('file')),
);
$schema['core_library_pages'] = array(
'fields' => array(
'lid' => array(
'description' => 'Primary identifier',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
),
'path' => array(
'description' => 'Drupal path',
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
),
),
'primary key' => array('lid', 'path'),
);
return $schema;
}
/**
* Add the 'exclude' flag into already learnt files to avoid PHP warnings.
*/
function core_library_update_7001() {
variable_set('core_library_updated', TRUE);
$files = variable_get('library_aggregation_orphans', array());
if (!empty($files)) {
foreach ($files as $type => $array) {
foreach ($array as $file => $options) {
$files[$type][$file]['exclude'] = FALSE;
}
}
variable_set('library_aggregation_orphans', $files);
}
}

View File

@@ -0,0 +1,90 @@
<?php
/**
* @file
* Core Library manual mode specifics functions.
*
* This code will be used only in manual inclusion mode. When the site admin
* decides that his configuration is solid enough, he will set the bypass mode
* and these functions won't ever be called anymore.
*
* This code tends to disappear in the future, but this is still solid code and
* a nice PoC.
*/
function core_library_manual_init() {
// Include required file, we may be the first to get here.
require_once DRUPAL_ROOT . '/includes/common.inc';
// Fetch site admin manually set settings.
$settings = variable_get('library_aggregation_settings', array());
foreach ($settings as $name => $values) {
if ($values['mode'] == LIBRARY_AGGREGATE_ALL) {
core_library_manual_process($values['module'], $name);
}
}
}
/**
* Process library depedencies.
*
* This function will be used only in manual mode. If you are in learning mode
* all library files will be found and overriden at hook_library_TYPE_alter()
* time.
*
* Gather all libraries override for all pages including. Pragmatically set
* all of them using drupal_add_js(), but set the drupal_static() cache of
* drupal_add_library() first to avoid any hook calls for theses.
*/
function core_library_manual_process($module, $name) {
// Bypass drupal_static() anti-pattern. All javascript and CSS we are going
// to add will exists for the full runtime.
static $processed = array(), $added;
if (!isset($added)) {
// We will populate the drupal_add_library() static cache ourself by adding
// javascript and CSS files with our own algorithm.
$added = &drupal_static('drupal_add_library', array());
}
// Security check, sometimes, $module is an array (but why?).
if (!is_string($module) || !is_string($name)) {
return;
}
// This will trigger the drupal_alter() over libraries.
$library = drupal_get_library($module, $name);
// Process dependencies recursively. Because dependencies will always remains
// harcoded, the result will always be the same resulting in the same files
// being aggregated at each page hit.
if (isset($library['dependencies'])) {
foreach ($library['dependencies'] as $dependency) {
list($_module, $_name) = $dependency;
if (!isset($processed[$_module . '-' . $_name])) {
core_library_manual_process($_module, $_name);
}
}
}
// Force settings for JS and CSS files to ensure the aggregation.
if (isset($library['js'])) {
foreach ($library['js'] as $file => $data) {
core_library_defaults_js($data);
drupal_add_js($file, $data);
}
}
if (isset($library['css'])) {
foreach ($library['css'] as $file => $data) {
$data['data'] = $file;
core_library_defaults_css($data);
drupal_add_css($file, $data);
}
}
// Register the library to the core.
$added[$module][$name] = TRUE;
// Circular dependency and infinite recursivity breaker.
$processed[$module . '-' . $name] = TRUE;
}

View File

@@ -0,0 +1,516 @@
<?php
/**
* @file
* Core Library module.
*/
/**
* Aggregate file on all pages.
*/
define('LIBRARY_AGGREGATE_ALL', 1);
/**
* Aggregate file on a per-path basis.
*/
define('LIBRARY_AGGREGATE_PATH', 2);
/**
* Let core follow its own rules for this file aggregation.
*/
define('LIBRARY_AGGREGATE_DEFAULT', 3);
/**
* Site admin defined aggregation rules.
*/
define('LIBRARY_MODE_MANUAL', 1);
/**
* Learn and aggregate all files in anonymous browsing.
*/
define('LIBRARY_MODE_LEARNING_ANONYMOUS', 2);
/**
* Learn and aggregate all files in browsing, admin pages excluded.
*/
define('LIBRARY_MODE_LEARNING_NO_ADMIN', 3);
/**
* Learn and aggregate all files.
*/
define('LIBRARY_MODE_LEARNING_ALL', 4);
/**
* Do not, ever, let the core aggregate files, and use the latest one whatever
* happens. This means all new files will be ignored.
*/
define('LIBRARY_MODE_BYPASS', 5);
/**
* Group CSS in 2 groups, all around in one side, theme specifics on the other.
*/
define('LIBRARY_CSS_GROUP_DUAL', 1);
/**
* Implements hook_menu().
*/
function core_library_menu() {
$items = array();
$items['admin/config/development/library'] = array(
'title' => "Library",
'description' => "Manage external JS and CSS libraries and allows to set different aggregation rules.",
'page callback' => 'drupal_get_form',
'page arguments' => array('core_library_admin_settings'),
'access arguments' => array('administer site configuration'),
'type' => MENU_NORMAL_ITEM,
'file' => 'core_library.admin.inc',
);
return $items;
}
/**
* Get JSMin path.
*
* FIXME: Sorry, this is hardcoded right now, will change in the future.
*
* @return string
* Found library, NULL if not found.
*/
function core_library_js_minify_library_path() {
if ($path = variable_get('core_library_jsmin_path', NULL)) {
return $path;
}
else {
return 'sites/all/libraries/jsmin/jsmin.php';
}
}
/**
* Minify given JS if not already done.
*/
function core_library_js_minify(&$options) {
static $minified, $included = FALSE;
if (!isset($minified)) {
$minified = variable_get('core_library_js_minified', array());
}
// Code readability, my friend.
$source = $options['data'];
// We should allow exclusion here, user configured.
if (!array_key_exists($source, $minified)) {
// Do not compress already compressed files.
if (!strpos($source, '.min.js')) {
// Attempt to use jsmin library if present.
if (!$included) {
$libpath = core_library_js_minify_library_path();
require_once DRUPAL_ROOT . '/' . $libpath;
$included = TRUE;
}
// Why does this functions is not wrapped in file.inc? It should, since we
// are supposed to manipulate every file using stream wrappers.
$buf = file_get_contents($source);
$buf = JSMin::minify($buf);
// Compute new filename.
$parts = explode('/', $source);
$filename = end($parts);
$file_uri = 'public://static/' . str_replace('.js', '.min.js', $filename);
// Save it.
if ($file_uri = file_unmanaged_save_data($buf, $file_uri, FILE_EXISTS_REPLACE)) {
if ($wrapper = file_stream_wrapper_get_instance_by_uri($file_uri)) {
// Protected getTarget() function emulation.
list($scheme, $target) = explode('://', $file_uri, 2);
$target = trim($target, '\/');
$file_uri = $wrapper->getDirectoryPath() . '/' . $target;
}
watchdog('core_library', "File '@src' minified successfully to '@dest'.", array(
'@src' => $source,
'@dest' => $file_uri,
));
// Save is ok, put the new filename in the $options array
$options['data'] = $file_uri;
// Tell the system we minified a new file.
$minified[$source] = $file_uri;
variable_set('core_library_js_minified', $minified);
}
}
}
else {
// We have already minified this file in the past, let's reuse the cached
// result path for alteration.
$options['data'] = $minified[$source];
}
}
/**
* Modify the given JavaScript description to enforce it to go into the main
* libraries group, so the core will create only one big aggregated file.
*/
function core_library_defaults_js(&$data) {
// Custom override.
static $minify_enabled = NULL, $defaults = array(
'every_page' => TRUE,
'preprocess' => TRUE,
'cache' => TRUE,
'defer' => FALSE,
'type' => 'file',
'scope' => 'header',
);
// Ignore 'inline' or 'external' content.
// Not set type means we are getting this entry from our own cache.
if (isset($data['type']) && 'file' !== $data['type']) {
return;
}
// Prepare some variables at first hit.
if (!isset($minify_enabled)) {
$minify_enabled = variable_get('core_library_js_minify', FALSE);
}
// Set default weight for every file. If we don't drupal_get_css() will
// fail at uasort() time on custom added JS files.
if (!array_key_exists('weight', $data)) {
$data['weight'] = 0;
}
// First thing to do is to force the current group to be libraries one.
if (!array_key_exists('group', $data) || $data['group'] != JS_LIBRARY) {
$data['group'] = JS_LIBRARY;
// Once we done that, we have to alter weight to ensure no outsider will
// take precedence over real libraries.
$data['weight'] += 1000;
}
// Override data.
foreach ($defaults as $name => $value) {
$data[$name] = $value;
}
// Minify, if enabled.
if ($minify_enabled) {
core_library_js_minify($data);
}
}
/**
* Modify the given CSS description to enforce it to go into the main
* system group, so the core will create only one big aggregated file.
*/
function core_library_defaults_css(&$data) {
// Defaults won't handle the 'media' key. Modules and libraries do specify
// their own value for this (we only set the 'all' value when no value is
// given).
static $please_group_css, $defaults = array(
'type' => 'file',
'every_page' => TRUE,
'preprocess' => TRUE,
);
// Ignore 'inline' or 'external' content.
// Not set type means we are getting this entry from our own cache.
if (isset($data['type']) && 'file' !== $data['type']) {
return;
}
// Statically initialize variables used for the rest of the algorithm.
if (!isset($please_group_css)) {
$css_grouping_mode = variable_get('core_library_css_group', FALSE);
$please_group_css = (bool) $css_grouping_mode;
}
// Assign a default group for files that does not have any. This can happen
// within our own execution workflow because we override a lot of stuff from
// core.
if (!array_key_exists('group', $data)) {
$data['group'] = CSS_DEFAULT;
}
// In some case, browsers are not specified, which make the aggregation
// algorithm to have a specific group for those. We don't want that, no
// browser specified means all browsers, like any other files.
// This is a weird behavior since it happens only for some core files
// included with the drupal_add_css() $every_page parameter set to TRUE,
// see system_init() implementation for example.
if (empty($data['browsers'])) {
$data['browsers'] = array(
'IE' => TRUE,
'!IE' => TRUE,
);
}
// Same bug as above, for media.
if (empty($data['media'])) {
$data['media'] = 'all';
}
// Set default weight for every file. If we don't drupal_get_css() will
// fail at uasort() time on custom added JS files.
if (!array_key_exists('weight', $data)) {
$data['weight'] = 0;
}
// First thing to do is to force the current group to be libraries one.
if ($please_group_css) {
if ($data['group'] != CSS_THEME) {
$data['group'] = CSS_SYSTEM;
// Once we done that, we have to alter weight to ensure no outsider will
// take precedence over real libraries.
$data['weight'] += 1000;
}
}
// Override data.
foreach ($defaults as $name => $value) {
$data[$name] = $value;
}
}
function core_library_bypass_mode() {
// Compat fix with overlay module.
// FIXME: A better solution should be developped here.
if (module_exists('overlay') && user_access('access-overlay')) {
return NULL;
}
return variable_get('core_library_bypass', NULL);
}
/**
* Implements hook_element_info_alter().
*
* Depending on the actual aggregation mode we are, we will change the current
* JS / CSS aggregation functions to ensure Drupal won't ever, ever attempt any
* file_exists() or hash compute while we are sure our CSS and JS files are
* stable and cover the full site.
*/
function core_library_element_info_alter(&$type) {
// Change the 'style' element type rendering if needed.
if (core_library_bypass_mode() == LIBRARY_MODE_BYPASS) {
// Force our file to be included if needed.
require_once drupal_get_path('module', 'core_library') . '/core_library.bypass.inc';
$type['styles'] = array(
'#items' => array(),
'#pre_render' => array('core_library_element_style_pre_render'),
);
}
}
/**
* Implements hook_init().
*/
function core_library_init() {
// We will aggregate libraries files only if are not in learning mode.
// Learning mode will find them as classic files, without having to known
// if they are part of a library or not.
if (core_library_bypass_mode() == LIBRARY_MODE_MANUAL) {
require_once drupal_get_path('module', 'core_library') . '/core_library.manual.inc';
core_library_manual_init();
}
}
/**
* Single point where all exclusion rules happen.
*
* @param string $type
* Library type (e.g. 'js' or 'css').
* @param string $file
* Original library key in libraries array.
* @param array $options
* Original library options.
*
* @return bool
* TRUE if should not preprocess this file ourselves.
*/
function _core_library_must_exclude($type, $file, $options) {
static $respectful_preprocess;
if (!isset($js_respect_preprocess)) {
$respectful_preprocess = variable_get('core_library_respectful_preprocess', TRUE);
}
if ($respectful_preprocess && isset($options['preprocess']) && !$options['preprocess']) {
return TRUE;
}
return FALSE;
}
/**
* Merge the given array using the given type.
*
* This is the magic function where all learning happens.
*/
function _core_library_merge(&$array, $type) {
global $theme_key;
// Following static variables are pure optimization.
static $do_learn, $theme_path, $theme_len;
// Set the current learning mode.
if (!isset($do_learn)) {
switch (core_library_bypass_mode()) {
case LIBRARY_MODE_LEARNING_ANONYMOUS:
$do_learn = user_is_anonymous();
break;
case LIBRARY_MODE_LEARNING_NO_ADMIN:
$do_learn = !drupal_match_path($_GET['q'], "admin\nadmin/*");
break;
case LIBRARY_MODE_LEARNING_ALL:
$do_learn = TRUE;
break;
case LIBRARY_MODE_BYPASS:
// In bypass mode, we still need libraries to be ordered in order for
// our custom style pre_render function to build correct CSS files.
$do_learn = FALSE;
break;;
default:
// We should never get here, but always keep a fallback.
$exclude_path = "*";
break;
}
// For later use.
$theme_path = drupal_get_path('theme', $theme_key);
$theme_len = strlen($theme_path);
}
$orphans = variable_get('library_aggregation_orphans', array());
// Callback for defaults.
$function = 'core_library_defaults_' . $type;
// Failsafe.
if (!is_callable($function)) {
return;
}
if (!isset($orphans[$type])) {
// array_diff_key() will require an array here when in learning mode, else
// the function will fail returning an empty result.
$orphans[$type] = array();
}
// In case we are in learning mode, add the new file to manual override
// and save result. The few first hit will cause some SQL overhead and
// some cache rebuild while users browse, but when all files will be
// found, this won't happen anymore.
// Once the maximum files are being found, you should then switch to manual
// mode, it will use the exact same variables we are being populating here
// and will won't attempt to do the array difference here.
// We do it before the real file alteration to ensure that newly found files
// will also be aggregated the first hit.
if ($do_learn) {
$unknown = array_diff_key($array, $orphans[$type]);
// Avoid to store JS settings array, this could cause some unwanted JS
// settings to displayed on wrong pages.
unset($unknown['settings']);
// Proceed only if array is not empty.
if (!empty($unknown)) {
foreach ($unknown as $file => $options) {
$data = array(
'mode' => LIBRARY_AGGREGATE_ALL,
'group' => $options['group'],
'exclude' => _core_library_must_exclude($type, $file, $options),
);
// No exclusion mode forces us to keep the full original options
// instead of allowing us to override it. This also means that the
// file will still always be included whatever happens.
if ($data['exclude']) {
$data += $options;
}
$orphans[$type][$file] = $data;
}
// Save new orphans.
variable_set('library_aggregation_orphans', $orphans);
}
}
// Merge array with our own values.
foreach ($orphans[$type] as $file => $options) {
// Whatever happens, do not include files from another theme. We must
// proceed this check as a side effect of learning all files, including
// those from other themes.
if ('css' === $type && isset($options['group']) && CSS_THEME === $options['group'] && strncmp($theme_path, $file, $theme_len)) {
unset($array[$file]);
}
else {
// If entry does not already exists, add it in order for the core to use
// it for full aggregation.
if (!isset($array[$file])) {
$array[$file] = $options + array('data' => $file);
}
if (!$options['exclude']) {
// Merge already loaded with our own options to force aggregation.
$function($array[$file]);
// File destination may have changed.
if ($array[$file]['data'] != $file) {
$array[$array[$file]['data']] = $array[$file];
unset($array[$file]);
}
}
}
}
}
/**
* Implements hook_js_alter().
*/
function core_library_js_alter(&$javascript) {
// We are going to use this hook instead of the hook_init() implementation
// in order to force some settings, because other drupal_add_js() calls will
// override our changes.
if (core_library_bypass_mode()) {
_core_library_merge($javascript, 'js');
}
}
/**
* Implements hook_css_alter().
*/
function core_library_css_alter(&$css) {
// We are going to use this hook instead of the hook_init() implementation
// in order to force some settings, because other drupal_add_js() calls will
// override our changes.
if (core_library_bypass_mode()) {
_core_library_merge($css, 'css');
}
}
/**
* Implements hook_help().
*/
function core_library_help($path, $arg) {
switch ($path) {
case 'admin/config/development/library':
$messages[] = t("This page allows you to configure advanced CSS and JS files aggregation rules. The Core Library module assumes that aggregating all CSS and JS files together on every page -even if they are not being always used- will provide better CSS and JS files cache hit over a long term usage and save more bandwith than core conditional aggregation algorithm.");
$messages[] = t("Notice that when changing the aggregation mode you need to refresh your aggressive cache page, if enabled. The module won't do it by itself and will let you do so to ensure it won't happen during frequentation pikes.");
if (variable_get('core_library_updated', FALSE)) {
drupal_set_message(t("The Core Library module has been updated and requires you to reset the learnt files so far. If you don't experience PHP notices, CSS malfunctions, or JS errors you can safely ignore this message."), 'error');
}
break;
}
}

View File

@@ -0,0 +1,382 @@
<?php
/**
* @file
* Core Library advanced UI module administration pages.
*/
/**
* Get known orphans by removing the known library files.
*/
function _core_library_get_known_orphans() {
// Gather all already known files for orphans exclusion.
$known = array();
$hook = 'library';
foreach (module_implements('library') as $module) {
foreach (module_invoke($module, $hook) as $name => $library) {
if (isset($library['js'])) {
foreach ($library['js'] as $file => $data) {
$file = drupal_get_path('module', $module) . '/' . $file;
$known[$file] = TRUE;
}
}
if (isset($library['css'])) {
foreach ($library['css'] as $file => $data) {
$file = drupal_get_path('module', $module) . '/' . $file;
$known[$file] = TRUE;
}
}
}
}
$select = db_select('core_library_stat', 'l');
$select->join('system', 's', 's.name = l.module');
$select->addField('s', 'type', 'owner_type');
return $select
->fields('l')
->condition('file', array_keys($known), 'NOT IN')
->orderBy('l.module', 'ASC')
->orderBy('l.type', 'ASC')
->orderBy('l.file', 'ASC')
->execute()
->fetchAll();
}
/**
* Orphans administration screen.
*/
function core_library_admin_orphans($form, &$form_state) {
$form['#tree'] = TRUE;
// Abitrary do some packages. We will populate it then build a full array
// for HTML build.
$form['orphans'] = array();
// Known orphans.
$orphans = variable_get('library_aggregation_orphans', array());
// Core fixed files that will exists on all pages whatever happens.
$fixed = array('misc/drupal.js');
foreach (_core_library_get_known_orphans() as $row) {
$subform = array();
// Rows that come back from database are module path agnostic, we need to
// convert them to fit our real site here.
if ($row->module != 'core') {
$row->file = drupal_get_path($row->owner_type, $row->module) . '/' . $row->file;
}
// Some metadata for display purpose only.
$subform['row'] = array('#type' => 'value', '#value' => $row);
// Other needed stuff.
$subform['type'] = array('#type' => 'value', '#value' => $row->type);
$subform['module'] = array('#type' => 'value', '#value' => $row->module);
// The most important stuff: the mode selection.
$subform['mode'] = array(
'#type' => 'select',
'#title' => t("Aggregation mode"),
'#options' => array(
LIBRARY_AGGREGATE_ALL => t("All"),
// FIXME: Disabled, this was a (not so) good idea.
// LIBRARY_AGGREGATE_PATH => t("Page"),
LIBRARY_AGGREGATE_DEFAULT => t("Default"),
),
'#default_value' => isset($orphans[$row->type][$row->file]['mode']) ? $orphans[$row->type][$row->file]['mode'] : LIBRARY_AGGREGATE_DEFAULT,
);
// Disable some fixed libraries (that core will append to aggregation
// whatever happens).
if (in_array($row->file, $fixed)) {
$subform['mode']['#default_value'] = LIBRARY_AGGREGATE_ALL;
$subform['mode']['#disabled'] = TRUE;
}
// All done.
$form['orphans'][$row->module][$row->file] = $subform;
}
$form['buttons'] = array();
$form['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t("Save"),
);
return $form;
}
function core_library_admin_orphans_submit($form, &$form_state) {
$orphans = array();
foreach ($form_state['values']['orphans'] as $module => $files) {
foreach ($files as $file => $values) {
// Add to our variable only if we need to.
if ($values['mode'] != LIBRARY_AGGREGATE_DEFAULT) {
$orphans[$values['type']][$file] = array(
'mode' => $values['mode'],
'module' => $module,
);
}
}
}
variable_set('library_aggregation_orphans', $orphans);
drupal_set_message(t("Libraries aggregation settings have been saved"));
}
/**
* Libraries administration screen.
*/
function core_library_admin_libraries($form, &$form_state) {
$form['#tree'] = TRUE;
// Abitrary do some packages. We will populate it then build a full array
// for HTML build.
$form['libraries'] = array(
'core' => array(),
'effect' => array(),
'ui' => array(),
'contrib' => array(),
);
// Get current existing settings.
$settings = variable_get('library_aggregation_settings', array());
$hook = 'library';
foreach (module_implements('library') as $module) {
foreach (module_invoke($module, $hook) as $name => $library) {
$subform = array();
$subform['mode'] = array(
'#type' => 'select',
'#title' => t("Aggregation mode"),
'#options' => array(
LIBRARY_AGGREGATE_ALL => t("All"),
LIBRARY_AGGREGATE_PATH => t("Page"),
LIBRARY_AGGREGATE_DEFAULT => t("Default"),
),
'#default_value' => isset($settings[$name]['mode']) ? $settings[$name]['mode'] : LIBRARY_AGGREGATE_DEFAULT,
);
$subform['description'] = array(
'#type' => 'value',
'#value' => $library,
);
$subform['module'] = array(
'#type' => 'value',
'#value' => $module,
);
// Find out type.
if ($name == 'ui' || substr($name, 0, 3) == 'ui.') {
$type = 'ui';
}
else if ($name == 'effects' || substr($name, 0, 8) == 'effects.') {
$type = 'effect';
}
else if ($module == 'system') {
$type = 'core';
}
else {
$type = 'contrib';
}
// All done.
$form['libraries'][$type][$name] = $subform;
}
}
$form['buttons'] = array();
$form['buttons']['submit'] = array(
'#type' => 'submit',
'#value' => t("Save"),
);
return $form;
}
function core_library_admin_libraries_submit($form, &$form_state) {
$settings = array();
foreach ($form_state['values']['libraries'] as $type => $libraries) {
foreach ($libraries as $name => $values) {
$settings[$name] = array(
'mode' => $values['mode'],
'skip_alter' => isset($values['skip_alter']) ? $values['skip_alter'] : FALSE,
'module' => $values['module'],
);
}
}
variable_set('library_aggregation_settings', $settings);
drupal_set_message(t("Libraries aggregation settings have been saved"));
}
/**
* Export profile page.
*/
function core_library_admin_export_form($form, &$form_state, $callback) {
$form['export'] = array(
'#type' => 'markup',
'#prefix' => '<pre id="export-text" style="background: white; color: black; padding: 0.5em 1em; border: 1px solid black;">',
'#suffix' => '</pre>',
'#markup' => is_callable($callback) ? $callback() : '',
);
if (module_exists('oox')) {
$form['#attached'] = array('js' => array(
drupal_get_path('module', 'oox') . '/js/jquery.copy.min.js',
drupal_get_path('module', 'oox') . '/js/copy.js',
));
$form['clip'] = array(
'#type' => 'button',
'#value' => t("Copy to clipboard"),
'#attributes' => array('class' => array('copy-to-clipboard'), 'rel' => 'export-text'),
);
}
return $form;
}
/**
* Export all known files.
*/
function core_library_known_file_export() {
$code = array();
$code[] = "; This file was generated using the Core Library module.";
$code[] = "; Built list of already known files for later use.";
foreach (_core_library_get_known_orphans() as $row) {
$code[] = $row->module . "[" . $row->type . "][] = " . $row->file;
}
return implode("\n", $code);
}
/**
* Export current profile.
*/
function core_library_profile_export_current() {
$code = array();
$settings = variable_get('library_aggregation_settings', array());
$orphans = variable_get('library_aggregation_orphans', array());
$code[] = "; This file was generated using the Core Library module.";
$code[] = "; Libraries.";
uasort($settings, '_core_library_sort_by_module');
foreach ($settings as $name => $options) {
if ($options['mode'] == LIBRARY_AGGREGATE_ALL) {
$code[] = "libraries[" . $options['module'] . "][] = " . $name;
}
}
foreach ($orphans as $type => $items) {
$code[] = "; " . strtoupper($type) . " files.";
foreach ($items as $file => $options) {
if ($options['mode'] == LIBRARY_AGGREGATE_ALL) {
$code[] = $type . "[] = " . $file;
}
}
}
return implode("\n", $code);
}
/**
* Sort options array by module.
*/
function _core_library_sort_by_module($a, $b) {
$a_module = is_array($a) && isset($a['module']) ? $a['module'] : '';
$b_module = is_array($b) && isset($b['module']) ? $b['module'] : '';
return strcasecmp($a_module, $b_module);
}
/**
* Format a percent value for some tables.
*/
function _core_library_format_percent($max, $value) {
return $value . " (" . round($value / $max * 100) . "%)";
}
/**
* Orphan settings form theming function.
*/
function theme_core_library_admin_orphans(&$variables) {
$elements = array('#type' => 'vertical_tabs');
$form = &$variables['form'];
// Headers will be the same for each table.
$headers = array(t("File"), t("Module"), t("Type"), t("Mode"), t("Hits"), t("Admin hits"), t("Anonymous hits"));
$tables = array();
foreach (element_children($form['orphans']) as $module) {
foreach (element_children($form['orphans'][$module]) as $file) {
$frow = $form['orphans'][$module][$file]['row']['#value'];
$row = array('data' => array());
$row['data'][] = $file;
$row['data'][] = $module;
unset($form['orphans'][$module][$file]['mode']['#title']);
$row['data'][] = $frow->type;
$row['data'][] = drupal_render($form['orphans'][$module][$file]['mode']);
$row['data'][] = $frow->hits;
$row['data'][] = _core_library_format_percent($frow->hits, $frow->hits_admin);
$row['data'][] = _core_library_format_percent($frow->hits, $frow->hits_anonymous);
$tables[$frow->owner_type][] = $row;
}
}
// New elements.
foreach ($tables as $owner_type => $rows) {
$elements[$owner_type] = array(
'#type' => 'fieldset',
'#title' => t(ucfirst($owner_type)),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'owner_type',
'content' => array(
// F-ing theme system. Why wouldn't it use my theme function here? Because
// it changes the variable to something other than the theme function will
// understand, with absolutely no reasons!
//'#type' => 'markup',
// '#theme' => 'table',
'#markup' => theme('table', array('header' => $headers, 'rows' => $rows))
// 'header' => $headers,
// 'rows' => $rows,
),
);
}
return drupal_render($elements) . drupal_render_children($form);
}
/**
* Libraries settings form theming function.
*/
function theme_core_library_admin_libraries(&$variables) {
$elements = array('#type' => 'vertical_tabs');
$form = &$variables['form'];
// Headers will be the same for each table.
$headers = array(t("Library"), t("Identifier"), t("Module"), t("Mode"));
// Put different packages in different tables.
foreach (element_children($form['libraries']) as $type) {
$rows = array();
foreach (element_children($form['libraries'][$type]) as $name) {
$row = array('data' => array());
$row['data'][] = $form['libraries'][$type][$name]['description']['#value']['title'];
$row['data'][] = $name;
$row['data'][] = $form['libraries'][$type][$name]['module']['#value'];
unset($form['libraries'][$type][$name]['mode']['#title']);
unset($form['libraries'][$type][$name]['skip_alter']['#title']);
$row['data'][] = drupal_render($form['libraries'][$type][$name]['mode']);
$rows[] = $row;
}
// New element.
$elements[$type] = array(
'#type' => 'fieldset',
'#title' => $type,
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#group' => 'library_types',
'content' => array('#markup' => theme('table', array('header' => $headers, 'rows' => $rows))),
);
}
return drupal_render($elements) . drupal_render_children($form);
}

View File

@@ -0,0 +1,16 @@
name = Core Library Advanced UI
description = Provide configuration screens for the Core Library module. You should enable this module the first time you use it, then disable it once in production.
package = Performance
version = VERSION
core = 7.x
configure = admin/config/development/library
; Module files.
files[] = core_library_ui.module
files[] = core_library_ui.admin.inc
; Information added by drupal.org packaging script on 2012-01-04
version = "7.x-1.0-beta11"
core = "7.x"
project = "core_library"
datestamp = "1325679340"

View File

@@ -0,0 +1,260 @@
<?php
/**
* @file
* Core Library advanced UI module.
*/
/**
* Implements hook_menu().
*/
function core_library_ui_menu() {
$items = array();
$items['admin/config/development/library/options'] = array(
'title' => "Settings",
'type' => MENU_DEFAULT_LOCAL_TASK,
);
$items['admin/config/development/library/libraries'] = array(
'title' => "Libraries",
'page callback' => 'drupal_get_form',
'page arguments' => array('core_library_admin_libraries'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
'file' => 'core_library_ui.admin.inc',
);
$items['admin/config/development/library/orphans'] = array(
'title' => "Orphans",
'page callback' => 'drupal_get_form',
'page arguments' => array('core_library_admin_orphans'),
'access arguments' => array('administer site configuration'),
'type' => MENU_LOCAL_TASK,
'file' => 'core_library_ui.admin.inc',
);
// Profile handling.
$items['admin/config/development/library/profile/export'] = array(
'title' => "Current profile export",
'page callback' => 'drupal_get_form',
'page arguments' => array('core_library_admin_export_form', 'core_library_profile_export_current'),
'access arguments' => array('administer site configuration'),
'type' => MENU_CALLBACK,
'file' => 'core_library_ui.admin.inc',
);
$items['admin/config/development/library/files/export'] = array(
'title' => "Known files export",
'page callback' => 'drupal_get_form',
'page arguments' => array('core_library_admin_export_form', 'core_library_known_file_export'),
'access arguments' => array('administer site configuration'),
'type' => MENU_CALLBACK,
'file' => 'core_library_ui.admin.inc',
);
return $items;
}
/**
* Implementation of hook_theme().
*/
function core_library_ui_theme() {
$items = array();
$items['core_library_admin_libraries'] = array(
'file' => 'core_library_ui.admin.inc',
'render element' => 'form',
);
$items['core_library_admin_orphans'] = array(
'file' => 'core_library_ui.admin.inc',
'render element' => 'form',
);
return $items;
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function core_library_ui_form_core_library_admin_settings_alter(&$form, &$form_state) {
$form['advanced']['core_library_collect'] = array(
'#type' => 'checkbox',
'#title' => t("Collect statistics"),
'#default_value' => variable_get('core_library_collect', FALSE),
'#description' => t("This will collect statistics on JS and CSS files usage. Please do not enable this in production, it could have a major performance impact."),
);
if (module_exists('core_library_ui')) {
$form['actions']['export_profile'] = array(
'#type' => 'markup',
'#markup' => l(t("Export profile"), 'admin/config/development/library/profile/export'),
);
}
}
/**
* Find file owner.
*
* This function may be unperformant, because we use the module list and theme
* list from Drupal core, and store them statically, which means we are using
* huge arrays duplicated in memory.
*
* I'm not quite happy with this function, but at least it works.
*
* @param string &$filename
* Complete filename relative to Drupal root.
*
* @return array
* Indexed values array:
* - First value is the owner name, from system table
* - Second value is the munge filename, relative to owner path, third value
* - Third value is the owner type ('theme', 'module', or 'core')
*/
function _core_library_find_owner($filepath) {
static $modules, $themes;
// Build static cache.
if (!isset($modules)) {
// First, load module list.
$modules = array();
foreach(module_list() as $module) {
$modules[$module] = drupal_get_path('module', $module);
}
// Also check for themes.
$themes = array();
foreach (list_themes() as $theme => $info) {
$themes[$theme] = drupal_get_path('theme', $theme);
}
}
// Prep. variables.
$owner = $owner_type = NULL;
// Ok, so we are treating an existing file, let's find out what is its real
// module behind, it may be either a module, a theme, or the core itself.
foreach ($modules as $module => $path) {
if (substr($filepath, 0, strlen($path)) == $path) {
$owner = $module;
$owner_type = 'module';
$filepath = substr($filepath, strlen($path) + 1);
break;
}
}
// Second, test with theme.
if (!$owner) {
foreach ($themes as $theme => $path) {
if (substr($filepath, 0, strlen($path)) == $path) {
$owner = $theme;
$owner_type = 'theme';
$filepath = substr($filepath, strlen($path) + 1);
break;
}
}
}
// If we have no module, consider this file belongs to core itself as
// a misc. file.
if (!$owner) {
$owner = $owner_type = 'core';
}
// Build return array.
return array($owner, $filepath, $owner_type);
}
/**
* Update statistics for given library file.
*
* This function seems to be really slow to run, this is probably due to the
* number of update or insert requests being done. We are working on a varchar
* indexed table which makes those requests even slower when using MySQL with
* the InnoDB engine.
*
* In order to provide a powerful and usable statistic counter, we need to
* rework this part.
*
* Maybe doing bulk insert at hook_exit() time would be better.
*/
function core_library_update_stats($file, $data, $type = 'unknown') {
static $done = array(), $known;
// Skip settings.
if ($file == 'settings') {
return;
}
// Do not proceed the same file twice.
if (isset($done[$file])) {
return;
}
// Prepare some stuff.
if (!isset($known)) {
// This is a pure performance patch, load full list in order to avoid one
// SQL query per file. The choice of consumming more memory in order to keep
// performances have been made here.
$known = array();
$results = db_select('core_library_stat', 'cls')
->fields('cls', array('file', 'module', 'lid'))
->execute()
->fetchAll();
foreach ($results as $data) {
$known[$data->module][$data->file] = $data->lid;
}
}
// Find owner.
list($owner, $file) = _core_library_find_owner($file);
$is_admin = drupal_match_path($_GET['q'], "admin\nadmin/*");
$is_anonymous = user_is_anonymous();
// Insert new file record, if not already set.
if (!isset($known[$owner][$file])) {
$record = array(
'file' => $file,
'hits' => 1,
'hits_anonymous' => $is_anonymous ? 1 : 0,
'hits_admin' => $is_admin ? 1 : 0,
'module' => $owner,
'type' => $type,
);
drupal_write_record('core_library_stat', $record);
$lid = $record['lid'];
$known[$owner][$file] = $lid;
}
else {
$lid = $known[$owner][$file];
// Increment the counter if the file already exists.
$sets = array();
$sets[] = "hits = hits + 1";
if ($is_anonymous) {
$sets[] = "hits_anonymous = hits_anonymous + 1";
}
if ($is_admin) {
$sets[] = "hits_admin = hits_admin + 1";
}
db_query("UPDATE {core_library_stat} SET " . implode(", ", $sets) . " WHERE lid = :lid", array(':lid' => $lid));
}
$done[$file] = TRUE;
}
/**
* Implements hook_js_alter().
*/
function core_library_ui_js_alter(&$javascript) {
// Aggregate statistics if configured for.
if (variable_get('core_library_collect', FALSE)) {
foreach ($javascript as $file => $data) {
if ('file' === $data['type']) {
core_library_update_stats($file, $data, 'js');
}
}
}
}
/**
* Implements hook_css_alter().
*/
function core_library_ui_css_alter(&$css) {
// Aggregate statistics if configured for.
if (variable_get('core_library_collect', FALSE)) {
foreach ($css as $file => $data) {
if ('file' === $data['type']) {
core_library_update_stats($file, $data, 'css');
}
}
}
}